/// <inheritdoc /> public void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry) { if (user == null) { throw new ArgumentNullException(nameof(user)); } if (systemIdentity == null) { throw new ArgumentNullException(nameof(systemIdentity)); } lock (cachedIdentities) { var uid = systemIdentity.Uid; logger.LogDebug("Caching system identity {0} of user {1}", uid, user.Id); if (cachedIdentities.TryGetValue(user.Id.Value, out var identCache)) { logger.LogTrace("Expiring previously cached identity..."); identCache.Dispose(); // also clears it out } identCache = new IdentityCacheObject(systemIdentity.Clone(), asyncDelayer, () => { logger.LogDebug("Expiring system identity cache for user {1}", uid, user.Id); lock (cachedIdentities) cachedIdentities.Remove(user.Id.Value); }, expiry); cachedIdentities.Add(user.Id.Value, identCache); } }
/// <summary> /// Construct an <see cref="IdentityCache"/> /// </summary> /// <param name="systemIdentity">The value of <see cref="SystemIdentity"/></param> /// <param name="onExpiry">The <see cref="Action"/> to take on expiry</param> /// <param name="expiry">The <see cref="DateTimeOffset"/></param> public IdentityCacheObject(ISystemIdentity systemIdentity, Action onExpiry, DateTimeOffset expiry) { SystemIdentity = systemIdentity ?? throw new ArgumentNullException(nameof(systemIdentity)); if (onExpiry == null) { throw new ArgumentNullException(nameof(onExpiry)); } var now = DateTimeOffset.Now; if (expiry < now) { throw new ArgumentOutOfRangeException(nameof(expiry), expiry, "expiry must be greater than DateTimeOffset.Now!"); } cancellationTokenSource = new CancellationTokenSource(); async Task DisposeOnExipiry(CancellationToken cancellationToken) { using (SystemIdentity) try { await Task.Delay(expiry - now, cancellationToken).ConfigureAwait(false); } finally { onExpiry(); } } task = DisposeOnExipiry(cancellationTokenSource.Token); }
/// <summary> /// Construct an <see cref="AuthenticationContext"/> /// </summary> /// <param name="systemIdentity">The value of <see cref="SystemIdentity"/></param> /// <param name="user">The value of <see cref="User"/></param> /// <param name="instanceUser">The value of <see cref="InstanceUser"/></param> public AuthenticationContext(ISystemIdentity systemIdentity, User user, InstanceUser instanceUser) { this.user = user ?? throw new ArgumentNullException(nameof(user)); if (systemIdentity == null && User.SystemIdentifier != null) { throw new ArgumentNullException(nameof(systemIdentity)); } InstanceUser = instanceUser; SystemIdentity = systemIdentity; }
/// <summary> /// If a <see cref="ForbidResult"/> should be returned from actions due to conflicts with one or both of the <see cref="Api.Models.Instance.ConfigurationType"/> or the <see cref="IAuthenticationContext.SystemIdentity"/> or a given <paramref name="path"/> tries to access parent directories /// </summary> /// <param name="path">The path to validate if any</param> /// <param name="systemIdentityToUse">The <see cref="ISystemIdentity"/> to use when calling into <see cref="Components.StaticFiles.IConfiguration"/></param> /// <returns><see langword="true"/> if a <see cref="ForbidResult"/> should be returned, <see langword="false"/> otherwise</returns> bool ForbidDueToModeConflicts(string path, out ISystemIdentity systemIdentityToUse) { if (Instance.ConfigurationType == ConfigurationType.Disallowed || (Instance.ConfigurationType == ConfigurationType.SystemIdentityWrite && AuthenticationContext.SystemIdentity == null) || (path != null && ioManager.PathContainsParentAccess(path))) { systemIdentityToUse = null; return(true); } systemIdentityToUse = Instance.ConfigurationType == ConfigurationType.SystemIdentityWrite ? AuthenticationContext.SystemIdentity : null; return(false); }
/// <summary> /// Construct an <see cref="AuthenticationContext"/> /// </summary> /// <param name="systemIdentity">The value of <see cref="SystemIdentity"/></param> /// <param name="user">The value of <see cref="User"/></param> /// <param name="instanceUser">The value of <see cref="InstancePermissionSet"/></param> public AuthenticationContext(ISystemIdentity systemIdentity, User user, InstancePermissionSet instanceUser) { User = user ?? throw new ArgumentNullException(nameof(user)); if (systemIdentity == null && User.SystemIdentifier != null) { throw new ArgumentNullException(nameof(systemIdentity)); } PermissionSet = user.PermissionSet ?? user.Group.PermissionSet ?? throw new ArgumentException("No PermissionSet provider", nameof(user)); InstancePermissionSet = instanceUser; SystemIdentity = systemIdentity; }
/// <inheritdoc /> public async Task <bool> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken).ConfigureAwait(false); var path = ValidateConfigRelativePath(configurationRelativePath); bool?result = null; void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken); if (systemIdentity == null) { await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); } else { await systemIdentity.RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(false); } return(result.Value); }
/// <inheritdoc /> public async Task <bool> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken).ConfigureAwait(false); var path = ValidateConfigRelativePath(configurationRelativePath); var result = false; using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path); if (systemIdentity != null) { await systemIdentity.RunImpersonated(CheckDeleteImpl, cancellationToken).ConfigureAwait(false); } else { CheckDeleteImpl(); } } return(result); }
/// <inheritdoc /> public void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry) { if (user == null) { throw new ArgumentNullException(nameof(user)); } if (systemIdentity == null) { throw new ArgumentNullException(nameof(systemIdentity)); } lock (cachedIdentities) { if (cachedIdentities.TryGetValue(user.Id, out var identCache)) { identCache.Dispose(); //also clears it out } identCache = new IdentityCacheObject(systemIdentity.Clone(), () => { lock (cachedIdentities) cachedIdentities.Remove(user.Id); }, expiry); cachedIdentities.Add(user.Id, identCache); } }
/// <inheritdoc /> public async Task <ConfigurationFile> Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken).ConfigureAwait(false); var path = ValidateConfigRelativePath(configurationRelativePath); ConfigurationFile result = null; void WriteImpl() { lock (this) try { var fileHash = previousHash; var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken); if (!success) { return; } result = new ConfigurationFile { Content = data, IsDirectory = false, LastReadHash = fileHash, AccessDenied = false, Path = configurationRelativePath }; } catch (UnauthorizedAccessException) { //this happens on windows, dunno about linux bool isDirectory; try { isDirectory = synchronousIOManager.IsDirectory(path); } catch { isDirectory = false; } result = new ConfigurationFile { Path = configurationRelativePath }; if (!isDirectory) { result.AccessDenied = true; } else { result.IsDirectory = true; } } } using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) if (systemIdentity == null) { await Task.Factory.StartNew(WriteImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); } else { await systemIdentity.RunImpersonated(WriteImpl, cancellationToken).ConfigureAwait(false); } return(result); }
/// <inheritdoc /> public async Task <ConfigurationFile> Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken).ConfigureAwait(false); var path = ValidateConfigRelativePath(configurationRelativePath); ConfigurationFile result = null; void ReadImpl() { lock (this) try { var content = synchronousIOManager.ReadFile(path); string sha1String; #pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1. using (var sha1 = new SHA1Managed()) #pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1. sha1String = String.Join("", sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))); result = new ConfigurationFile { Content = content, IsDirectory = false, LastReadHash = sha1String, AccessDenied = false, Path = configurationRelativePath }; } catch (UnauthorizedAccessException) { //this happens on windows, dunno about linux bool isDirectory; try { isDirectory = synchronousIOManager.IsDirectory(path); } catch { isDirectory = false; } result = new ConfigurationFile { Path = configurationRelativePath }; if (!isDirectory) { result.AccessDenied = true; } else { result.IsDirectory = true; } } } using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) if (systemIdentity == null) { await Task.Factory.StartNew(ReadImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); } else { await systemIdentity.RunImpersonated(ReadImpl, cancellationToken).ConfigureAwait(false); } return(result); }
/// <inheritdoc /> public async Task <IReadOnlyList <ConfigurationFile> > ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken).ConfigureAwait(false); var path = ValidateConfigRelativePath(configurationRelativePath); if (configurationRelativePath == null) { configurationRelativePath = "/"; } List <ConfigurationFile> result = new List <ConfigurationFile>(); void ListImpl() { var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken); try { result.AddRange(enumerator.Select(x => new ConfigurationFile { IsDirectory = true, Path = ioManager.ConcatPath(configurationRelativePath, x), })); } catch (IOException e) { logger.LogDebug("IOException while writing {0}: {1}", path, e); result = null; return; } enumerator = synchronousIOManager.GetFiles(path, cancellationToken); result.AddRange(enumerator.Select(x => new ConfigurationFile { IsDirectory = false, Path = ioManager.ConcatPath(configurationRelativePath, x), })); } using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) if (systemIdentity == null) { ListImpl(); } else { await systemIdentity.RunImpersonated(ListImpl, cancellationToken).ConfigureAwait(false); } return(result); }
#pragma warning disable CA1506 // TODO: Decomplexify public async Task <IActionResult> CreateToken(CancellationToken cancellationToken) { if (ApiHeaders == null) { Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS4 bearer token\"")); return(HeadersIssue(false)); } if (ApiHeaders.IsTokenAuthentication) { return(BadRequest(new ErrorMessage(ErrorCode.TokenWithToken))); } var oAuthLogin = ApiHeaders.OAuthProvider.HasValue; ISystemIdentity systemIdentity = null; if (!oAuthLogin) { try { // trust the system over the database because a user's name can change while still having the same SID systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false); } catch (NotImplementedException ex) { Logger.LogTrace(ex, "System identities not implemented!"); } } using (systemIdentity) { // Get the user from the database IQueryable <Models.User> query = DatabaseContext.Users.AsQueryable(); if (oAuthLogin) { string externalUserId; try { var validator = oAuthProviders .GetValidator(ApiHeaders.OAuthProvider.Value); if (validator == null) { return(BadRequest(new ErrorMessage(ErrorCode.OAuthProviderDisabled))); } externalUserId = await validator .ValidateResponseCode(ApiHeaders.Token, cancellationToken) .ConfigureAwait(false); } catch (RateLimitExceededException ex) { return(RateLimit(ex)); } if (externalUserId == null) { return(Unauthorized()); } query = query.Where( x => x.OAuthConnections.Any( y => y.Provider == ApiHeaders.OAuthProvider.Value && y.ExternalUserId == externalUserId)); } else { string canonicalName = Models.User.CanonicalizeName(ApiHeaders.Username); if (systemIdentity == null) { query = query.Where(x => x.CanonicalName == canonicalName); } else { query = query.Where(x => x.CanonicalName == canonicalName || x.SystemIdentifier == systemIdentity.Uid); } } var users = await query.Select(x => new Models.User { Id = x.Id, PasswordHash = x.PasswordHash, Enabled = x.Enabled, Name = x.Name }).ToListAsync(cancellationToken).ConfigureAwait(false); // Pick the DB user first var user = users .OrderByDescending(dbUser => dbUser.PasswordHash != null) .FirstOrDefault(); // No user? You're not allowed if (user == null) { return(Unauthorized()); } // A system user may have had their name AND password changed to one in our DB... // Or a DB user was created that had the same user/pass as a system user // Dumb admins... // FALLBACK TO THE DB USER HERE, DO NOT REVEAL A SYSTEM LOGIN!!! // This of course, allows system users to discover TGS users in this (HIGHLY IMPROBABLE) case but that is not our fault var originalHash = user.PasswordHash; var isDbUser = originalHash != null; bool usingSystemIdentity = systemIdentity != null && !isDbUser; if (!oAuthLogin) { if (!usingSystemIdentity) { // DB User password check and update if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password)) { return(Unauthorized()); } if (user.PasswordHash != originalHash) { Logger.LogDebug("User ID {0}'s password hash needs a refresh, updating database.", user.Id); var updatedUser = new Models.User { Id = user.Id }; DatabaseContext.Users.Attach(updatedUser); updatedUser.PasswordHash = user.PasswordHash; await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); } } else if (systemIdentity.Username != user.Name) { // System identity username change update Logger.LogDebug("User ID {0}'s system identity needs a refresh, updating database.", user.Id); DatabaseContext.Users.Attach(user); user.Name = systemIdentity.Username; user.CanonicalName = Models.User.CanonicalizeName(user.Name); await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); } } // Now that the bookeeping is done, tell them to f**k off if necessary if (!user.Enabled.Value) { Logger.LogTrace("Not logging in disabled user {0}.", user.Id); return(Forbid()); } var token = await tokenFactory.CreateToken(user, oAuthLogin, cancellationToken).ConfigureAwait(false); if (usingSystemIdentity) { // expire the identity slightly after the auth token in case of lag var identExpiry = token.ExpiresAt; identExpiry += tokenFactory.ValidationParameters.ClockSkew; identExpiry += TimeSpan.FromSeconds(15); identityCache.CacheSystemIdentity(user, systemIdentity, identExpiry); } Logger.LogDebug("Successfully logged in user {0}!", user.Id); return(Json(token)); } }