tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
UserAuthority.cs
Go to the documentation of this file.
1using System;
4using System.Linq;
7
8using GreenDonut;
9
11
15
28
30{
33 {
38
43
48
53
58
63
68
73
84 IDatabaseContext databaseContext,
85 CancellationToken cancellationToken)
86 {
87 ArgumentNullException.ThrowIfNull(ids);
88 ArgumentNullException.ThrowIfNull(databaseContext);
89
90 return databaseContext
91 .Users
92 .AsQueryable()
93 .Where(x => ids.Contains(x.Id!.Value))
94 .ToDictionaryAsync(user => user.Id!.Value, cancellationToken);
95 }
96
104 [DataLoader]
105 public static async ValueTask<ILookup<long, GraphQL.Types.OAuth.OAuthConnection>> GetOAuthConnections(
107 IDatabaseContext databaseContext,
108 CancellationToken cancellationToken)
109 {
110 ArgumentNullException.ThrowIfNull(userIds);
111 ArgumentNullException.ThrowIfNull(databaseContext);
112
113 var list = await databaseContext
115 .AsQueryable()
116 .Where(x => userIds.Contains(x.User!.Id!.Value))
117 .ToListAsync(cancellationToken);
118
119 return list.ToLookup(
121 x => new GraphQL.Types.OAuth.OAuthConnection(x.ExternalUserId!, x.Provider));
122 }
123
131 {
133 if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name)))
134 return BadRequest<User>(ErrorCode.UserMissingName);
135
136 model.Name = model.Name?.Trim();
137 if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
138 return BadRequest<User>(ErrorCode.UserColonInName);
139 return null;
140 }
141
157 IAuthenticationContext authenticationContext,
158 IDatabaseContext databaseContext,
168 : base(
169 authenticationContext,
170 databaseContext,
171 logger)
172 {
181 }
182
194 {
195 if (createRequest.OAuthConnections?.Any(x => x == null) == true)
196 {
197 failResponse = BadRequest<User>(ErrorCode.ModelValidationFailure);
198 return true;
199 }
200
203 var hasOAuthConnections = (createRequest.OAuthConnections?.Count > 0) == true;
206 {
207 failResponse = BadRequest<User>(ErrorCode.UserMismatchPasswordSid);
208 return true;
209 }
210
211 var hasZeroLengthPassword = createRequest.Password?.Length == 0;
213 {
215 {
216 if (createRequest.OAuthConnections == null)
217 throw new InvalidOperationException($"Expected {nameof(UserCreateRequest.OAuthConnections)} to be set here!");
218
219 if (createRequest.OAuthConnections.Count == 0)
220 {
221 failResponse = BadRequest<User>(ErrorCode.ModelValidationFailure);
222 return true;
223 }
224 }
225 else if (hasZeroLengthPassword)
226 {
227 failResponse = BadRequest<User>(ErrorCode.ModelValidationFailure);
228 return true;
229 }
230 }
231
232 if (createRequest.Group != null && createRequest.PermissionSet != null)
233 {
234 failResponse = BadRequest<User>(ErrorCode.UserGroupAndPermissionSet);
235 return true;
236 }
237
238 createRequest.Name = createRequest.Name?.Trim();
239 if (createRequest.Name?.Length == 0)
240 createRequest.Name = null;
241
242 if (!(createRequest.Name == null ^ createRequest.SystemIdentifier == null))
243 {
244 failResponse = BadRequest<User>(ErrorCode.UserMismatchNameSid);
245 return true;
246 }
247
249 return failResponse != null;
250 }
251
255
258 {
260 return Forbid<User>();
261
262 User? user;
263 if (includeJoins)
264 {
265 var queryable = Queryable(true, true);
266
267 user = await queryable.FirstOrDefaultAsync(
268 dbModel => dbModel.Id == id,
270 }
271 else
272 user = await usersDataLoader.LoadAsync(id, cancellationToken);
273
274 if (user == default)
275 return NotFound<User>();
276
278 return Forbid<User>();
279
280 return new AuthorityResponse<User>(user);
281 }
282
285 => Queryable(includeJoins, false);
286
288 public async ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken)
289 => new AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>(
291
296 CancellationToken cancellationToken)
297 {
299
301 return failResponse;
302
304 .Users
305 .AsQueryable()
306 .CountAsync(cancellationToken);
307 if (totalUsers >= generalConfigurationOptions.Value.UserLimit)
308 return Conflict<User>(ErrorCode.UserLimitReached);
309
311 if (dbUser == null)
312 return Gone<User>();
313
314 if (createRequest.SystemIdentifier != null)
315 try
316 {
318 if (sysIdentity == null)
319 return Gone<User>();
320 dbUser.Name = sysIdentity.Username;
322 }
324 {
325 Logger.LogTrace(ex, "System identities not implemented!");
326 return new AuthorityResponse<User>(
327 new ErrorMessageResponse(ErrorCode.RequiresPosixSystemIdentity),
328 HttpFailureResponse.NotImplemented);
329 }
330 else
331 {
332 var hasZeroLengthPassword = createRequest.Password?.Length == 0;
333 var hasOAuthConnections = (createRequest.OAuthConnections?.Count > 0) == true;
334
335 // special case allow PasswordHash to be null by setting Password to "" if OAuthConnections are set
337 {
338 var result = TrySetPassword(dbUser, createRequest.Password!, true);
339 if (result != null)
340 return result;
341 }
342 }
343
345
347
349
350 Logger.LogInformation("Created new user {name} ({id})", dbUser.Name, dbUser.Id);
351
353
355 }
356
358#pragma warning disable CA1502
359#pragma warning disable CA1506 // TODO: Decomplexify
361#pragma warning restore CA1502
362#pragma warning restore CA1506
363 {
364 ArgumentNullException.ThrowIfNull(model);
365
366 if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true)
367 return BadRequest<User>(ErrorCode.ModelValidationFailure);
368
369 if (model.Group != null && model.PermissionSet != null)
370 return BadRequest<User>(ErrorCode.UserGroupAndPermissionSet);
371
376
380 .Users
381 .AsQueryable()
382 .Where(x => x.Id == model.Id)
383 .Include(x => x.CreatedBy)
384 .Include(x => x.OAuthConnections)
385 .Include(x => x.Group!)
386 .ThenInclude(x => x.PermissionSet)
387 .Include(x => x.PermissionSet)
388 .FirstOrDefaultAsync(cancellationToken);
389
390 if (originalUser == default)
391 return NotFound<User>();
392
394 return Forbid<User>();
395
396 // Ensure they are only trying to edit things they have perms for (system identity change will trigger a bad request)
397 if ((!canEditAllUsers
398 && (model.Id != originalUser.Id
399 || model.Enabled.HasValue
400 || model.Group != null
401 || model.PermissionSet != null
402 || model.Name != null))
403 || (!passwordEdit && model.Password != null)
404 || (!oAuthEdit && model.OAuthConnections != null))
405 return Forbid<User>();
406
408 var invalidateSessions = false;
409 if (originalUserHasSid && originalUser.PasswordHash != null)
410 {
411 // cleanup from https://github.com/tgstation/tgstation-server/issues/1528
412 Logger.LogDebug("System user ID {userId}'s PasswordHash is polluted, updating database.", originalUser.Id);
414
415 invalidateSessions = true;
416 }
417
418 if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
419 return BadRequest<User>(ErrorCode.UserSidChange);
420
421 if (model.Password != null)
422 {
424 return BadRequest<User>(ErrorCode.UserMismatchPasswordSid);
425
426 var result = TrySetPassword(originalUser, model.Password, false);
427 if (result != null)
428 return result;
429
430 invalidateSessions = true;
431 }
432
433 if (model.Name != null && User.CanonicalizeName(model.Name) != originalUser.CanonicalName)
434 return BadRequest<User>(ErrorCode.UserNameChange);
435
436 if (model.OAuthConnections != null
437 && (model.OAuthConnections.Count != originalUser.OAuthConnections!.Count
438 || !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId))))
439 {
441 return BadRequest<User>(ErrorCode.AdminUserCannotOAuth);
442
443 if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
444 return BadRequest<User>(ErrorCode.CannotRemoveLastAuthenticationOption);
445
446 originalUser.OAuthConnections.Clear();
447 foreach (var updatedConnection in model.OAuthConnections)
448 originalUser.OAuthConnections.Add(new Models.OAuthConnection
449 {
450 Provider = updatedConnection.Provider,
451 ExternalUserId = updatedConnection.ExternalUserId,
452 });
453 }
454
455 if (model.Group != null)
456 {
458 .Groups
459 .AsQueryable()
460 .Where(x => x.Id == model.Group.Id)
461 .Include(x => x.PermissionSet)
462 .FirstOrDefaultAsync(cancellationToken);
463
464 if (originalUser.Group == default)
465 return Gone<User>();
466
467 DatabaseContext.Groups.Attach(originalUser.Group);
468 if (originalUser.PermissionSet != null)
469 {
470 Logger.LogInformation("Deleting permission set {permissionSetId}...", originalUser.PermissionSet.Id);
471 DatabaseContext.PermissionSets.Remove(originalUser.PermissionSet);
473 }
474 }
475 else if (model.PermissionSet != null)
476 {
477 if (originalUser.PermissionSet == null)
478 {
479 Logger.LogTrace("Creating new permission set...");
480 originalUser.PermissionSet = new Models.PermissionSet();
481 }
482
485
486 originalUser.Group = null;
488 }
489
490 var fail = CheckValidName(model, false);
491 if (fail != null)
492 return fail;
493
495
496 if (model.Enabled.HasValue)
497 {
498 invalidateSessions = originalUser.Require(x => x.Enabled) && !model.Enabled.Value;
499 originalUser.Enabled = model.Enabled.Value;
500 }
501
504
506
507 Logger.LogInformation("Updated user {userName} ({userId})", originalUser.Name, originalUser.Id);
508
511
513
514 // return id only if not a self update and cannot read users
517 return canReadBack
520 }
521
529 GraphQL.Subscriptions.UserSubscriptions.UserUpdatedTopics(
530 user.Require(x => x.Id))
531 .Select(topic => topicEventSender.SendAsync(
532 topic,
534 CancellationToken.None))); // DCT: Operation should always run
535
543 {
546 .Users
547 .AsQueryable();
548
549 if (!allowSystemUser)
551 .Where(user => user.CanonicalName != tgsUserCanonicalName);
552
553 if (includeJoins)
555 .Include(x => x.CreatedBy)
556 .Include(x => x.OAuthConnections)
557 .Include(x => x.Group!)
558 .ThenInclude(x => x.PermissionSet)
559 .Include(x => x.PermissionSet);
560
561 return queryable;
562 }
563
570 async ValueTask<User> CreateNewUserFromModel(Api.Models.Internal.UserApiBase model, CancellationToken cancellationToken)
571 {
572 Models.PermissionSet? permissionSet = null;
573 UserGroup? group = null;
574 if (model.Group != null)
576 .Groups
577 .AsQueryable()
578 .Where(x => x.Id == model.Group.Id)
579 .Include(x => x.PermissionSet)
580 .FirstOrDefaultAsync(cancellationToken);
581 else
582 permissionSet = new Models.PermissionSet
583 {
585 InstanceManagerRights = model.PermissionSet?.InstanceManagerRights ?? InstanceManagerRights.None,
586 };
587
588 return new User
589 {
590 CreatedAt = DateTimeOffset.UtcNow,
591 CreatedBy = AuthenticationContext.User,
592 Enabled = model.Enabled ?? false,
593 PermissionSet = permissionSet,
594 Group = group,
595 Name = model.Name,
596 SystemIdentifier = model.SystemIdentifier,
598 .OAuthConnections
599 ?.Select(x => new Models.OAuthConnection
600 {
601 Provider = x.Provider,
602 ExternalUserId = x.ExternalUserId,
603 })
604 .ToList()
605 ?? new List<Models.OAuthConnection>(),
606 };
607 }
608
617 {
618 newPassword ??= String.Empty;
619 if (newPassword.Length < generalConfigurationOptions.Value.MinimumPasswordLength)
620 return new AuthorityResponse<User>(
621 new ErrorMessageResponse(ErrorCode.UserPasswordLength)
622 {
623 AdditionalData = $"Required password length: {generalConfigurationOptions.Value.MinimumPasswordLength}",
624 },
625 HttpFailureResponse.BadRequest);
627 return null;
628 }
629 }
630}
Represents initial credentials used by the server.
static readonly string AdminUserName
The name of the default admin user.
virtual ? long Id
The ID of the entity.
Definition EntityId.cs:13
Represents a set of server permissions.
AdministrationRights? AdministrationRights
The Rights.AdministrationRights for the user.
Represents an error message returned by the server.
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
ILogger< AuthorityBase > Logger
Gets the ILogger for the AuthorityBase.
readonly ISessionInvalidationTracker sessionInvalidationTracker
The ISessionInvalidationTracker for the UserAuthority.
readonly ITopicEventSender topicEventSender
The ITopicEventSender for the UserAuthority.
AuthorityResponse< User >? TrySetPassword(User dbUser, string newPassword, bool newUser)
Attempt to change the password of a given dbUser .
readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee
The IPermissionsUpdateNotifyee for the UserAuthority.
async ValueTask< AuthorityResponse< User > > GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken)
Gets the User with a given id .A ValueTask<TResult> resulting in a User AuthorityResponse<TResult>.
static Task< Dictionary< long, User > > GetUsers(IReadOnlyList< long > ids, IDatabaseContext databaseContext, CancellationToken cancellationToken)
Implements the usersDataLoader.
async ValueTask< AuthorityResponse< GraphQL.Types.OAuth.OAuthConnection[]> > OAuthConnections(long userId, CancellationToken cancellationToken)
Gets the GraphQL.Types.OAuth.OAuthConnections for the User with a given userId .A ValueTask<TResult> ...
static ? AuthorityResponse< User > CheckValidName(UserUpdateRequest model, bool newUser)
Check if a given model has a valid UserName.Name specified.
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the UserAuthority.
readonly IOptionsSnapshot< GeneralConfiguration > generalConfigurationOptions
The IOptionsSnapshot<TOptions> of GeneralConfiguration for the UserAuthority.
IQueryable< User > Queryable(bool includeJoins)
Gets all registered Users.A IQueryable<T> of Users.
IQueryable< User > Queryable(bool includeJoins, bool allowSystemUser)
Gets all registered Users.
async ValueTask< User > CreateNewUserFromModel(Api.Models.Internal.UserApiBase model, CancellationToken cancellationToken)
Creates a new User from a given model .
UserAuthority(IAuthenticationContext authenticationContext, IDatabaseContext databaseContext, ILogger< UserAuthority > logger, IUsersDataLoader usersDataLoader, IOAuthConnectionsDataLoader oAuthConnectionsDataLoader, ISystemIdentityFactory systemIdentityFactory, IPermissionsUpdateNotifyee permissionsUpdateNotifyee, ICryptographySuite cryptographySuite, ISessionInvalidationTracker sessionInvalidationTracker, ITopicEventSender topicEventSender, IOptionsSnapshot< GeneralConfiguration > generalConfigurationOptions)
Initializes a new instance of the UserAuthority class.
ValueTask< AuthorityResponse< User > > Read(CancellationToken cancellationToken)
Gets the currently authenticated user.A ValueTask<TResult> resulting in a User AuthorityResponse<TRes...
static bool BadCreateRequestChecks(UserCreateRequest createRequest, bool? needZeroLengthPasswordWithOAuthConnections, [NotNullWhen(true)] out AuthorityResponse< User >? failResponse)
Checks if a createRequest should return a bad request AuthorityResponse<TResult>.
static async ValueTask< ILookup< long, GraphQL.Types.OAuth.OAuthConnection > > GetOAuthConnections(IReadOnlyList< long > userIds, IDatabaseContext databaseContext, CancellationToken cancellationToken)
Implements the usersDataLoader.
async ValueTask< AuthorityResponse< User > > Create(UserCreateRequest createRequest, bool? needZeroLengthPasswordWithOAuthConnections, CancellationToken cancellationToken)
Creates a User.A ValueTask<TResult> resulting in am AuthorityResponse<TResult> for the created User.
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the UserAuthority.
ValueTask SendUserUpdatedTopics(User user)
Send topics through the topicEventSender indicating a given user was created or updated.
readonly IUsersDataLoader usersDataLoader
The IUsersDataLoader for the UserAuthority.
async ValueTask< AuthorityResponse< User > > Update(UserUpdateRequest model, CancellationToken cancellationToken)
Updates a User.A ValueTask<TResult> resulting in am AuthorityResponse<TResult> for the created User.
readonly IOAuthConnectionsDataLoader oAuthConnectionsDataLoader
The IOAuthConnectionsDataLoader for the UserAuthority.
Backend abstract implementation of IDatabaseContext.
DbSet< PermissionSet > PermissionSets
The PermissionSets in the DatabaseContext.
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext.A Task representing the running operation.
DbSet< User > Users
The Users in the DatabaseContext.
DbSet< UserGroup > Groups
The UserGroups in the DatabaseContext.
Represents a group of Users.
Definition UserGroup.cs:16
const string TgsSystemUserName
Username used when creating jobs automatically.
Definition User.cs:21
static string CanonicalizeName(string name)
Change a UserName.Name into a CanonicalName.
string? CanonicalName
The uppercase invariant of UserName.Name.
Definition User.cs:58
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType .The value of rightsType . Note that if InstancePermissionSet is ...
IDatabaseCollection< User > Users
The Users in the IDatabaseContext.
IDatabaseCollection< OAuthConnection > OAuthConnections
The DbSet<TEntity> for OAuthConnections.
Represents a host-side model that may be transformed into a TApiModel .
For creating and accessing authentication contexts.
Contains various cryptographic functions.
void SetUserPassword(User user, string newPassword, bool newUser)
Sets a User.PasswordHash for a given user .
Receives notifications about permissions updates.
ValueTask UserDisabled(User user, CancellationToken cancellationToken)
Called when a given User is successfully disabled.
void UserModifiedInvalidateSessions(User user)
Invalidate all sessions for a given user .
Task< ISystemIdentity?> CreateSystemIdentity(User user, CancellationToken cancellationToken)
Create a ISystemIdentity for a given user .
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition ErrorCode.cs:12
@ List
User may list files if the Models.Instance allows it.
RightsType
The type of rights a model uses.
Definition RightsType.cs:7
InstanceManagerRights
Rights for managing Models.Instances.
AdministrationRights
Administration rights for the server.
@ Api
The ApiHeaders.ApiVersionHeader header is missing or invalid.
HttpFailureResponse
Indicates the type of HTTP status code an failing AuthorityResponse should generate.
HttpSuccessResponse
Indicates the type of HTTP status code a successful AuthorityResponse<TResult> should generate.
@ Enabled
The OAuth Gateway is enabled.