diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/Net.Http/src/Core/HttpCookie.cs | 20 | ||||
-rw-r--r-- | lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs | 2 | ||||
-rw-r--r-- | lib/Net.Http/src/Helpers/HelperTypes.cs | 4 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Accounts/AccountUtils.cs | 121 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs | 51 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Extensions/ICookieController.cs | 54 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs | 124 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Users/IUser.cs | 23 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs | 61 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Users/IUserManager.cs | 79 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/Users/PassValidateFlags.cs | 45 | ||||
-rw-r--r-- | lib/Utils/src/Memory/PrivateString.cs | 182 | ||||
-rw-r--r-- | lib/Utils/src/Memory/PrivateStringManager.cs | 116 |
13 files changed, 570 insertions, 312 deletions
diff --git a/lib/Net.Http/src/Core/HttpCookie.cs b/lib/Net.Http/src/Core/HttpCookie.cs index e0e5406..e19aaec 100644 --- a/lib/Net.Http/src/Core/HttpCookie.cs +++ b/lib/Net.Http/src/Core/HttpCookie.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -42,15 +42,13 @@ namespace VNLib.Net.Http.Core public bool HttpOnly { get; init; } public bool IsSession { get; init; } - public HttpCookie(string name) - { - this.Name = name; - } + public HttpCookie(string name) => Name = name; public string Compile() { throw new NotImplementedException(); } + public void Compile(ref ForwardOnlyWriter<char> writer) { //set the name of the cookie @@ -84,7 +82,7 @@ namespace VNLib.Net.Http.Core case CookieSameSite.None: writer.Append("None"); break; - case CookieSameSite.SameSite: + case CookieSameSite.Strict: writer.Append("Strict"); break; case CookieSameSite.Lax: @@ -112,14 +110,8 @@ namespace VNLib.Net.Http.Core public override int GetHashCode() => Name.GetHashCode(); - public override bool Equals(object? obj) - { - return obj is HttpCookie other && Equals(other); - } + public override bool Equals(object? obj) => obj is HttpCookie other && Equals(other); - public bool Equals(HttpCookie? other) - { - return other != null && Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase); - } + public bool Equals(HttpCookie? other) => other != null && Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase); } }
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs b/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs index 5e7f019..10697f2 100644 --- a/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs +++ b/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs @@ -141,7 +141,7 @@ namespace VNLib.Net.Http.Core parseState.Location = new() { //Set a default scheme - Scheme = usingTls ? Uri.UriSchemeHttp : Uri.UriSchemeHttps, + Scheme = usingTls ? Uri.UriSchemeHttps : Uri.UriSchemeHttp, }; //Need to manually parse the query string diff --git a/lib/Net.Http/src/Helpers/HelperTypes.cs b/lib/Net.Http/src/Helpers/HelperTypes.cs index 8b03dbf..7e7e068 100644 --- a/lib/Net.Http/src/Helpers/HelperTypes.cs +++ b/lib/Net.Http/src/Helpers/HelperTypes.cs @@ -168,8 +168,8 @@ namespace VNLib.Net.Http /// </summary> None, /// <summary> - /// Cookie samesite property, Same-Site mode + /// Cookie samesite property, strict mode /// </summary> - SameSite + Strict } }
\ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs index a5fb074..396d496 100644 --- a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs +++ b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs @@ -48,7 +48,7 @@ namespace VNLib.Plugins.Essentials.Accounts { /// <summary> - /// The size in bytes of the random passwords generated when invoking the <see cref="SetRandomPasswordAsync(IPasswordHashingProvider, IUserManager, IUser, int)"/> + /// The size in bytes of the random passwords generated when invoking the /// </summary> public const int RANDOM_PASS_SIZE = 240; @@ -90,56 +90,37 @@ namespace VNLib.Plugins.Essentials.Accounts #region Password/User helper extensions /// <summary> - /// Generates and sets a random password for the specified user account + /// Validates a password associated with the specified user /// </summary> - /// <param name="manager">The configured <see cref="IUserManager"/> to process the password update on</param> - /// <param name="user">The user instance to update the password on</param> - /// <param name="passHashing">The <see cref="PasswordHashing"/> instance to hash the random password with</param> - /// <param name="size">Size (in bytes) of the generated random password</param> - /// <returns>A value indicating the results of the event (number of rows affected, should evaluate to true)</returns> - /// <exception cref="VnArgon2Exception"></exception> - /// <exception cref="ArgumentException"></exception> + /// <param name="manager"></param> + /// <param name="user">The user to validate the password against</param> + /// <param name="password">The password to test against the user</param> + /// <param name="flags">Validation flags</param> + /// <param name="cancellation">A token to cancel the validation</param> /// <exception cref="ArgumentNullException"></exception> - public static async Task<ERRNO> SetRandomPasswordAsync(this IPasswordHashingProvider passHashing, IUserManager manager, IUser user, int size = RANDOM_PASS_SIZE) + /// <returns>A value greater than 0 if successful, 0 or negative values if a failure occured</returns> + public static async Task<ERRNO> ValidatePasswordAsync(this IUserManager manager, IUser user, string password, PassValidateFlags flags, CancellationToken cancellation) { _ = manager ?? throw new ArgumentNullException(nameof(manager)); - _ = user ?? throw new ArgumentNullException(nameof(user)); - _ = passHashing ?? throw new ArgumentNullException(nameof(passHashing)); - if (user.IsReleased) - { - throw new ObjectDisposedException("The specifed user object has been released"); - } - //Alloc a buffer - using IMemoryHandle<byte> buffer = MemoryUtil.SafeAlloc<byte>(size); - //Use the CGN to get a random set - RandomHash.GetRandomBytes(buffer.Span); - //Hash the new random password - using PrivateString passHash = passHashing.Hash(buffer.Span); - //Write the password to the user account - return await manager.UpdatePassAsync(user, passHash); + using PrivateString ps = new(password, false); + return await manager.ValidatePasswordAsync(user, ps, flags, cancellation).ConfigureAwait(false); } - + /// <summary> - /// Creates a new user with a random user id and the specified email address and password. - /// If privileges are left null, the minimum privileges will be set. + /// Updates a password associated with the specified user. If the update fails, the transaction + /// is rolled back. /// </summary> /// <param name="manager"></param> - /// <param name="emailAddress">The user's email address or secondary id</param> - /// <param name="password">The user's password</param> - /// <param name="privileges">Optional user privilage level</param> - /// <param name="cancellation">A token to cancel the operation</param> - /// <returns>A task that resolves the new user</returns> + /// <param name="user">The user account to update the password of</param> + /// <param name="password">The new password to set</param> /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="UserExistsException"></exception> - /// <exception cref="UserCreationFailedException"></exception> - public static Task<IUser> CreateUserAsync(this IUserManager manager, string emailAddress, PrivateString password, ulong? privileges, CancellationToken cancellation = default) + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The result of the operation, the result should be 1 (aka true)</returns> + public static async Task<ERRNO> UpdatePasswordAsync(this IUserManager manager, IUser user, string password, CancellationToken cancellation = default) { _ = manager ?? throw new ArgumentNullException(nameof(manager)); - //Create a random user id - string randomId = GetRandomUserId(); - //Set the default/minimum privileges - privileges ??= MINIMUM_LEVEL; - return manager.CreateUserAsync(randomId, emailAddress, privileges.Value, password, cancellation); + using PrivateString ps = new(password, false); + return await manager.UpdatePasswordAsync(user, ps, cancellation).ConfigureAwait(false); } /// <summary> @@ -169,14 +150,6 @@ namespace VNLib.Plugins.Essentials.Accounts public static void SetAccountOrigin(this IUser ud, string origin) => ud[ACC_ORIGIN_ENTRY] = origin; /// <summary> - /// Gets a random user-id generated from crypograhic random number - /// then hashed (SHA1) and returns a hexadecimal string - /// </summary> - /// <returns>The random string user-id</returns> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetRandomUserId() => RandomHash.GetRandomHash(HashAlg.SHA1, 64, HashEncodingMode.Hexadecimal); - - /// <summary> /// Generates a cryptographically secure random password, then hashes it /// and returns the hash of the new password /// </summary> @@ -207,58 +180,6 @@ namespace VNLib.Plugins.Essentials.Accounts } /// <summary> - /// Asynchronously verifies the desired user's password. If the user is not found or the password is not found - /// returns false. Returns true if the user exist's has a valid password hash and matches the supplied password value. - /// </summary> - /// <param name="manager"></param> - /// <param name="userId">The id of the user to check the password against</param> - /// <param name="rawPassword">The raw password of the user to compare hashes against</param> - /// <param name="hashing">The password hashing tools</param> - /// <param name="cancellation">A token to cancel the operation</param> - /// <returns>A task that completes with the value of the password hashing match.</returns> - /// <exception cref="ArgumentNullException"></exception> - public static async Task<bool> VerifyPasswordAsync(this IUserManager manager, string userId, PrivateString rawPassword, IPasswordHashingProvider hashing, CancellationToken cancellation) - { - _ = manager ?? throw new ArgumentNullException(nameof(manager)); - _ = userId ?? throw new ArgumentNullException(nameof(userId)); - _ = rawPassword ?? throw new ArgumentNullException(nameof(rawPassword)); - _ = hashing ?? throw new ArgumentNullException(nameof(hashing)); - - //Get the user, may be null if the user does not exist - using IUser? user = await manager.GetUserAndPassFromIDAsync(userId, cancellation); - - if(user == null) - { - return false; - } - - if(user.PassHash == null) - { - return false; - } - - return hashing.Verify(user.PassHash.ToReadOnlySpan(), rawPassword.ToReadOnlySpan()); - } - - /// <summary> - /// Verifies the user's raw password against the hashed password using the specified - /// <see cref="PasswordHashing"/> instance - /// </summary> - /// <param name="user"></param> - /// <param name="rawPassword"></param> - /// <param name="hashing">The <see cref="IPasswordHashingProvider"/> provider instance</param> - /// <returns>True if the password </returns> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool VerifyPassword(this IUser user, PrivateString rawPassword, IPasswordHashingProvider hashing) - { - _ = user ?? throw new ArgumentNullException(nameof(user)); - _ = rawPassword ?? throw new ArgumentNullException(nameof(rawPassword)); - _ = hashing ?? throw new ArgumentNullException(nameof(hashing)); - - return user.PassHash != null && hashing.Verify(user.PassHash.ToReadOnlySpan(), rawPassword.ToReadOnlySpan()); - } - - /// <summary> /// Verifies a password against its previously encoded hash. /// </summary> /// <param name="provider"></param> diff --git a/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs b/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs new file mode 100644 index 0000000..e346af1 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: UserCreationRequest.cs +* +* UserCreationRequest.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using VNLib.Utils.Memory; +using VNLib.Plugins.Essentials.Users; + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// <summary> + /// A concrete implementation of <see cref="IUserCreationRequest"/> + /// that can be used to create a new user. + /// </summary> + public class UserCreationRequest : IUserCreationRequest + { + ///<inheritdoc/> + public PrivateString? Password { get; init; } + + ///<inheritdoc/> + public ulong Privileges { get; init; } = AccountUtil.MINIMUM_LEVEL; + + ///<inheritdoc/> + public string EmailAddress { get; init; } = string.Empty; + + ///<inheritdoc/> + public bool UseRawPassword { get; init; } + + ///<inheritdoc/> + public UserStatus InitialStatus { get; init; } = UserStatus.Unverified; + } +}
\ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Extensions/ICookieController.cs b/lib/Plugins.Essentials/src/Extensions/ICookieController.cs new file mode 100644 index 0000000..b88e648 --- /dev/null +++ b/lib/Plugins.Essentials/src/Extensions/ICookieController.cs @@ -0,0 +1,54 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: ICookieController.cs +* +* ICookieController.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +namespace VNLib.Plugins.Essentials.Extensions +{ + /// <summary> + /// Manges a single cookie for connections + /// </summary> + public interface ICookieController + { + /// <summary> + /// Sets the cookie value for the given entity + /// </summary> + /// <param name="entity">The http connection to set the cookie value for</param> + /// <param name="value">The cookie value</param> + void SetCookie(HttpEntity entity, string value); + + /// <summary> + /// Gets the cookie value for the given entity + /// </summary> + /// <param name="entity">The entity to get the cookie for</param> + /// <returns>The cookie value if set, null otherwise</returns> + string? GetCookie(HttpEntity entity); + + /// <summary> + /// Expires an existing request cookie for the given entity, avoiding + /// setting the response cookie unless necessary + /// </summary> + /// <param name="entity">The http connection to expire the cookie on</param> + /// <param name="force">Forcibly set the response cookie regardless of it's existence</param> + void ExpireCookie(HttpEntity entity, bool force); + } +}
\ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs b/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs new file mode 100644 index 0000000..1893b6e --- /dev/null +++ b/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs @@ -0,0 +1,124 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: SingleCookieController.cs +* +* SingleCookieController.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Collections.Generic; + +using VNLib.Net.Http; + +namespace VNLib.Plugins.Essentials.Extensions +{ + /// <summary> + /// Implements a sinlge cookie controller + /// </summary> + public class SingleCookieController : ICookieController + { + private readonly string _cookieName; + private readonly TimeSpan _validFor; + + /// <summary> + /// Creates a new <see cref="SingleCookieController"/> instance + /// </summary> + /// <param name="cookieName">The name of the cookie to manage</param> + /// <param name="validFor">The max-age cookie value</param> + public SingleCookieController(string cookieName, TimeSpan validFor) + { + _cookieName = cookieName; + _validFor = validFor; + } + + /// <summary> + /// The domain of the cookie + /// </summary> + public string? Domain { get; init; } + + /// <summary> + /// The path of the cookie + /// </summary> + public string? Path { get; init; } + + /// <summary> + /// Whether the cookie is secure + /// </summary> + public bool Secure { get; init; } + + /// <summary> + /// Whether the cookie is HTTP only + /// </summary> + public bool HttpOnly { get; init; } + + /// <summary> + /// The SameSite policy of the cookie + /// </summary> + public CookieSameSite SameSite { get; init; } + + + /// <summary> + /// Optionally clears the cookie (does not force) + /// </summary> + /// <param name="entity">The entity to clear the cookie for</param> + public void ExpireCookie(HttpEntity entity) => ExpireCookie(entity, false); + + ///<inheritdoc/> + public void ExpireCookie(HttpEntity entity, bool force) + { + _ = entity ?? throw new ArgumentNullException(nameof(entity)); + SetCookieInternal(entity, string.Empty, force); + } + + ///<inheritdoc/> + public string? GetCookie(HttpEntity entity) + { + _ = entity ?? throw new ArgumentNullException(nameof(entity)); + return entity.Server.RequestCookies.GetValueOrDefault(_cookieName); + } + + ///<inheritdoc/> + public void SetCookie(HttpEntity entity, string value) + { + _ = entity ?? throw new ArgumentNullException(nameof(entity)); + SetCookieInternal(entity, value, true); + } + + private void SetCookieInternal(HttpEntity entity, string value, bool force) + { + //Only set cooke if already exists or force is true + if (entity.Server.RequestCookies.ContainsKey(value) || force) + { + //Build and set cookie + HttpCookie cookie = new(_cookieName, value) + { + Secure = Secure, + HttpOnly = HttpOnly, + ValidFor = _validFor, + SameSite = SameSite, + Path = Path, + Domain = Domain + }; + + entity.Server.SetCookie(in cookie); + } + } + } +}
\ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Users/IUser.cs b/lib/Plugins.Essentials/src/Users/IUser.cs index a36ba70..315c8c5 100644 --- a/lib/Plugins.Essentials/src/Users/IUser.cs +++ b/lib/Plugins.Essentials/src/Users/IUser.cs @@ -27,14 +27,19 @@ using System.Collections.Generic; using VNLib.Utils; using VNLib.Utils.Async; -using VNLib.Utils.Memory; namespace VNLib.Plugins.Essentials.Users { + /// <summary> /// Represents an abstract user account /// </summary> - public interface IUser : IAsyncExclusiveResource, IDisposable, IObjectStorage, IEnumerable<KeyValuePair<string, string>>, IIndexable<string, string> + public interface IUser : + IAsyncExclusiveResource, + IDisposable, + IObjectStorage, + IEnumerable<KeyValuePair<string, string>>, + IIndexable<string, string> { /// <summary> /// The user's privilege level @@ -52,9 +57,9 @@ namespace VNLib.Plugins.Essentials.Users DateTimeOffset Created { get; } /// <summary> - /// The user's password hash if retreived from the backing store, otherwise null + /// The user's email address /// </summary> - PrivateString? PassHash { get; } + string EmailAddress { get; set; } /// <summary> /// Status of account @@ -62,16 +67,6 @@ namespace VNLib.Plugins.Essentials.Users UserStatus Status { get; set; } /// <summary> - /// Is the account only usable from local network? - /// </summary> - bool LocalOnly { get; set; } - - /// <summary> - /// The user's email address - /// </summary> - string EmailAddress { get; set; } - - /// <summary> /// Marks the user for deletion on release /// </summary> void Delete(); diff --git a/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs b/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs new file mode 100644 index 0000000..a5b9a30 --- /dev/null +++ b/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs @@ -0,0 +1,61 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: IUserCreationRequest.cs +* +* IUserCreationRequest.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using VNLib.Utils.Memory; + +namespace VNLib.Plugins.Essentials.Users +{ + /// <summary> + /// A request to create a new user + /// </summary> + public interface IUserCreationRequest + { + /// <summary> + /// The value to store in the users password field. By default this + /// value will be hashed before being stored in the database, unless + /// <see cref="UseRawPassword"/> is set to true. + /// </summary> + PrivateString? Password { get; } + + /// <summary> + /// The user's initial privilege level + /// </summary> + ulong Privileges { get; } + + /// <summary> + /// The user's email address + /// </summary> + string EmailAddress { get; } + + /// <summary> + /// Should the password be stored as-is in the database? + /// </summary> + bool UseRawPassword { get; } + + /// <summary> + /// The user's initial status + /// </summary> + UserStatus InitialStatus { get; } + } +}
\ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Users/IUserManager.cs b/lib/Plugins.Essentials/src/Users/IUserManager.cs index b731033..400a5d0 100644 --- a/lib/Plugins.Essentials/src/Users/IUserManager.cs +++ b/lib/Plugins.Essentials/src/Users/IUserManager.cs @@ -28,15 +28,37 @@ using System.Threading.Tasks; using VNLib.Utils; using VNLib.Utils.Memory; +using VNLib.Plugins.Essentials.Accounts; namespace VNLib.Plugins.Essentials.Users { + /// <summary> /// A backing store that provides user accounts /// </summary> public interface IUserManager { /// <summary> + /// Gets the internal password hash provider if one is available + /// </summary> + /// <returns>The internal hash provider if available, null otherwise</returns> + IPasswordHashingProvider? GetHashProvider(); + + /// <summary> + /// Computes uinuqe user-id that is safe for use in the database. + /// </summary> + /// <param name="input">The value to convert to a safe user-id</param> + /// <returns>The safe-user id</returns> + string ComputeSafeUserId(string input); + + /// <summary> + /// Gets the number of entries in the current user table + /// </summary> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The number of users in the table, or -1 if the operation failed</returns> + Task<long> GetUserCountAsync(CancellationToken cancellation = default); + + /// <summary> /// Attempts to get a user object without their password from the database asynchronously /// </summary> /// <param name="userId">The id of the user</param> @@ -55,36 +77,42 @@ namespace VNLib.Plugins.Essentials.Users Task<IUser?> GetUserFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default); /// <summary> - /// Attempts to get a user object with their password from the database on the current thread + /// Creates a new user account in the store as per the request. The user-id field is optional, + /// and if set to null or empty, will be generated automatically by the store. /// </summary> - /// <param name="userid">The id of the user</param> + /// <param name="userId">An optional user id to force</param> /// <param name="cancellation">A token to cancel the operation</param> - /// <returns>The user's <see cref="IUser"/> object, null if the user was not found</returns> + /// <param name="creation">The account email address</param> + /// <returns>An object representing a user's account if successful, null otherwise</returns> + /// <exception cref="UserExistsException"></exception> /// <exception cref="ArgumentNullException"></exception> - Task<IUser?> GetUserAndPassFromIDAsync(string userid, CancellationToken cancellation = default); + /// <exception cref="UserCreationFailedException"></exception> + Task<IUser> CreateUserAsync(IUserCreationRequest creation, string? userId, CancellationToken cancellation = default); /// <summary> - /// Attempts to get a user object with their password from the database asynchronously + /// Validates a password associated with the specified user /// </summary> - /// <param name="emailAddress">The user's email address</param> - /// <param name="cancellationToken">A token to cancel the operation</param> - /// <returns>The user's <see cref="IUser"/> object, null if the user was not found</returns> - /// <exception cref="ArgumentNullException"></exception> - Task<IUser?> GetUserAndPassFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default); + /// <param name="user">The user to validate the password against</param> + /// <param name="password">The password to test against the user</param> + /// <param name="flags">Validation flags</param> + /// <param name="cancellation">A token to cancel the validation</param> + /// <returns>A value greater than 0 if successful, 0 or negative values if a failure occured</returns> + Task<ERRNO> ValidatePasswordAsync(IUser user, PrivateString password, PassValidateFlags flags, CancellationToken cancellation = default); /// <summary> - /// Creates a new user in the current user's table and if successful returns the new user object (without password) + /// An operation that will attempt to recover a user's password if possible. Not all user + /// managment systems allow recovering passwords for users. This method should return + /// null if the operation is not supported. + /// <para> + /// The returned value will likely not be the user's raw password but instead a hashed + /// or encrypted version of the password. + /// </para> /// </summary> - /// <param name="userid">The user id</param> - /// <param name="privileges">A number representing the privilage level of the account</param> - /// <param name="passHash">Value to store in the password field</param> - /// <param name="cancellation">A token to cancel the operation</param> - /// <param name="emailAddress">The account email address</param> - /// <returns>An object representing a user's account if successful, null otherwise</returns> - /// <exception cref="UserExistsException"></exception> - /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="UserCreationFailedException"></exception> - Task<IUser> CreateUserAsync(string userid, string emailAddress, ulong privileges, PrivateString passHash, CancellationToken cancellation = default); + /// <param name="user">The user to recover the password for</param> + /// <param name="cancellation">A token to cancel the opertion</param> + /// <returns>The password if found</returns> + /// <exception cref="NotSupportedException"></exception> + Task<PrivateString?> RecoverPasswordAsync(IUser user, CancellationToken cancellation = default); /// <summary> /// Updates a password associated with the specified user. If the update fails, the transaction @@ -94,13 +122,6 @@ namespace VNLib.Plugins.Essentials.Users /// <param name="newPass">The new password to set</param> /// <param name="cancellation">A token to cancel the operation</param> /// <returns>The result of the operation, the result should be 1 (aka true)</returns> - Task<ERRNO> UpdatePassAsync(IUser user, PrivateString newPass, CancellationToken cancellation = default); - - /// <summary> - /// Gets the number of entries in the current user table - /// </summary> - /// <param name="cancellation">A token to cancel the operation</param> - /// <returns>The number of users in the table, or -1 if the operation failed</returns> - Task<long> GetUserCountAsync(CancellationToken cancellation = default); + Task<ERRNO> UpdatePasswordAsync(IUser user, PrivateString newPass, CancellationToken cancellation = default); } }
\ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Users/PassValidateFlags.cs b/lib/Plugins.Essentials/src/Users/PassValidateFlags.cs new file mode 100644 index 0000000..f6ff43a --- /dev/null +++ b/lib/Plugins.Essentials/src/Users/PassValidateFlags.cs @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: PassValidateFlags.cs +* +* PassValidateFlags.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Essentials.Users +{ + /// <summary> + /// Flags for password validation + /// </summary> + [Flags] + public enum PassValidateFlags + { + /// <summary> + /// No flags/default + /// </summary> + None = 0, + + /// <summary> + /// Bypasses hashing of the password if possible + /// </summary> + BypassHashing = 1, + } +}
\ No newline at end of file diff --git a/lib/Utils/src/Memory/PrivateString.cs b/lib/Utils/src/Memory/PrivateString.cs index 20d658a..8300b97 100644 --- a/lib/Utils/src/Memory/PrivateString.cs +++ b/lib/Utils/src/Memory/PrivateString.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -27,15 +27,33 @@ using System.Diagnostics.CodeAnalysis; namespace VNLib.Utils.Memory { + /// <summary> /// Provides a wrapper class that will have unsafe access to the memory of /// the specified <see cref="string"/> provided during object creation. /// </summary> /// <remarks>The value of the memory the protected string points to is undefined when the instance is disposed</remarks> - public class PrivateString : PrivateStringManager, IEquatable<PrivateString>, IEquatable<string>, ICloneable + public class PrivateString : + PrivateStringManager, + IEquatable<PrivateString>, + IEquatable<string>, + ICloneable { - protected string StrRef => base[0]!; - private readonly bool OwnsReferrence; + /// <summary> + /// Gets the internal string referrence + /// </summary> + protected string StringRef => base[0]!; + + /// <summary> + /// Does the current instance "own" the memory the data parameter points to + /// </summary> + protected bool OwnsReferrence { get; } + + /// <summary> + /// The internal string's length + /// </summary> + /// <exception cref="ObjectDisposedException"></exception> + public int Length => StringRef.Length; /// <summary> /// Creates a new <see cref="PrivateString"/> over the specified string and the memory it points to. @@ -43,130 +61,60 @@ namespace VNLib.Utils.Memory /// <param name="data">The <see cref="string"/> instance pointing to the memory to protect</param> /// <param name="ownsReferrence">Does the current instance "own" the memory the data parameter points to</param> /// <remarks>You should no longer reference the input string directly</remarks> - public PrivateString(string data, bool ownsReferrence = true) : base(1) + /// <exception cref="ArgumentException"></exception> + public PrivateString(string data, bool ownsReferrence) : base(1) { //Create a private string manager to store referrence to string base[0] = data ?? throw new ArgumentNullException(nameof(data)); OwnsReferrence = ownsReferrence; } - //Create private string from a string - public static explicit operator PrivateString?(string? data) - { - //Allow passing null strings during implicit casting - return data == null ? null : new(data); - } - - public static PrivateString? ToPrivateString(string? value) - { - return value == null ? null : new PrivateString(value, true); - } - - //Cast to string - public static explicit operator string (PrivateString str) - { - //Check if disposed, or return the string - str.Check(); - return str.StrRef; - } - - public static implicit operator ReadOnlySpan<char>(PrivateString str) - { - return str.Disposed ? Span<char>.Empty : str.StrRef.AsSpan(); - } - /// <summary> /// Gets the value of the internal string as a <see cref="ReadOnlySpan{T}"/> /// </summary> /// <returns>The <see cref="ReadOnlySpan{T}"/> referrence to the internal string</returns> /// <exception cref="ObjectDisposedException"></exception> - public ReadOnlySpan<char> ToReadOnlySpan() - { - Check(); - return StrRef.AsSpan(); - } + public ReadOnlySpan<char> ToReadOnlySpan() => StringRef.AsSpan(); + + /// <summary> + /// Creates a new deep copy of the current instance that + /// is an independent <see cref="PrivateString"/> + /// </summary> + /// <returns>The new <see cref="PrivateString"/> instance</returns> + /// <exception cref="ObjectDisposedException"></exception> + public virtual PrivateString Clone() => new(ToString(), true); ///<inheritdoc/> - public bool Equals(string? other) - { - Check(); - return StrRef.Equals(other, StringComparison.Ordinal); - } + public bool Equals(string? other) => StringRef.Equals(other, StringComparison.Ordinal); + ///<inheritdoc/> - public bool Equals(PrivateString? other) - { - Check(); - return other != null && StrRef.Equals(other.StrRef, StringComparison.Ordinal); - } + public bool Equals(PrivateString? other) => other is not null && StringRef.Equals(other.StringRef, StringComparison.Ordinal); + ///<inheritdoc/> - public override bool Equals(object? obj) - { - Check(); - return obj is PrivateString otherRef && StrRef.Equals(otherRef); - } + public override bool Equals(object? obj) => obj is PrivateString otherRef && StringRef.Equals(otherRef); + ///<inheritdoc/> - public bool Equals(ReadOnlySpan<char> other) - { - Check(); - return StrRef.AsSpan().SequenceEqual(other); - } + public bool Equals(ReadOnlySpan<char> other) => StringRef.AsSpan().SequenceEqual(other); + /// <summary> /// Creates a deep copy of the internal string and returns that copy /// </summary> /// <returns>A deep copy of the internal string</returns> - public override string ToString() - { - Check(); - return new(StrRef.AsSpan()); - } - /// <summary> - /// String length - /// </summary> - /// <exception cref="ObjectDisposedException"></exception> - public int Length - { - get - { - Check(); - return StrRef.Length; - } - } - /// <summary> - /// Indicates whether the underlying string is null or an empty string ("") - /// </summary> - /// <param name="ps"></param> - /// <returns>True if the parameter is null, or an empty string (""). False otherwise</returns> - public static bool IsNullOrEmpty([NotNullWhen(false)] PrivateString? ps) => ps == null|| ps.Length == 0; + public override string ToString() => CopyStringAtIndex(0)!; /// <summary> /// The hashcode of the underlying string /// </summary> /// <returns></returns> - public override int GetHashCode() - { - Check(); - return StrRef.GetHashCode(StringComparison.Ordinal); - } + public override int GetHashCode() => Disposed ? 0 : string.GetHashCode(StringRef, StringComparison.Ordinal); /// <summary> - /// Creates a new deep copy of the current instance that is an independent <see cref="PrivateString"/> + /// Creates a new deep copy of the current instance that + /// is an independent <see cref="PrivateString"/> /// </summary> /// <returns>The new <see cref="PrivateString"/> instance</returns> /// <exception cref="ObjectDisposedException"></exception> - public override object Clone() - { - Check(); - //Copy all contents of string to another reference - string clone = new (StrRef.AsSpan()); - //return a new private string - return new PrivateString(clone, true); - } - - ///<inheritdoc/> - protected override void Free() - { - Erase(); - } + object ICloneable.Clone() => new PrivateString(ToString(), true); /// <summary> /// Erases the contents of the internal CLR string @@ -179,5 +127,43 @@ namespace VNLib.Utils.Memory base.Free(); } } + + ///<inheritdoc/> + protected override void Free() => Erase(); + + /// <summary> + /// Indicates whether the underlying string is null or an empty string ("") + /// </summary> + /// <param name="ps"></param> + /// <returns>True if the parameter is null, or an empty string (""). False otherwise</returns> + public static bool IsNullOrEmpty([NotNullWhen(false)] PrivateString? ps) => ps is null || ps.Length == 0; + + /// <summary> + /// A nullable cast to a <see cref="PrivateString"/> + /// </summary> + /// <param name="data"></param> + public static explicit operator PrivateString?(string? data) => ToPrivateString(data, true); + + /// <summary> + /// Creates a new <see cref="PrivateString"/> if the data is not null that owns the memory + /// the string points to, null otherwise. + /// </summary> + /// <param name="data">The string reference to wrap</param> + /// <param name="ownsString">A value that indicates if the string memory is owned by the instance</param> + /// <returns>The new private string wrapper, or null if the value is null</returns> + public static PrivateString? ToPrivateString(string? data, bool ownsString) => data == null ? null : new(data, ownsString); + + /// <summary> + /// Casts the <see cref="PrivateString"/> to a <see cref="string"/> + /// </summary> + /// <param name="str"></param> + public static explicit operator string?(PrivateString? str) => str?.StringRef; + + /// <summary> + /// Casts the <see cref="PrivateString"/> to a <see cref="ReadOnlySpan{T}"/> + /// </summary> + /// <param name="str"></param> + public static implicit operator ReadOnlySpan<char>(PrivateString? str) => (str is null || str.Disposed) ? Span<char>.Empty : str.StringRef.AsSpan(); + } } diff --git a/lib/Utils/src/Memory/PrivateStringManager.cs b/lib/Utils/src/Memory/PrivateStringManager.cs index 8f01e98..3d50463 100644 --- a/lib/Utils/src/Memory/PrivateStringManager.cs +++ b/lib/Utils/src/Memory/PrivateStringManager.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -24,24 +24,25 @@ using System; - namespace VNLib.Utils.Memory { /// <summary> /// When inherited by a class, provides a safe string storage that zeros a CLR string memory on disposal /// </summary> - public class PrivateStringManager : VnDisposeable, ICloneable + public class PrivateStringManager : VnDisposeable { + private readonly StringRef[] ProtectedElements; + /// <summary> - /// Strings to be cleared when exiting + /// Create a new instance with fixed array size /// </summary> - private readonly string?[] ProtectedElements; + /// <param name="elements">Number of elements to protect</param> + public PrivateStringManager(int elements) => ProtectedElements = new StringRef[elements]; + /// <summary> /// Gets or sets a string referrence into the protected elements store /// </summary> - /// <param name="index"></param> - /// <exception cref="ArgumentException"></exception> - /// <exception cref="ArgumentNullException"></exception> + /// <param name="index">The table index to store the string</param> /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <returns>Referrence to string associated with the index</returns> @@ -50,68 +51,75 @@ namespace VNLib.Utils.Memory get { Check(); - return ProtectedElements[index]; + return ProtectedElements[index].Value; } set { Check(); - //Check to see if the string has been interned - if (!string.IsNullOrEmpty(value) && string.IsInterned(value) != null) - { - throw new ArgumentException($"The specified string has been CLR interned and cannot be stored in {nameof(PrivateStringManager)}"); - } - //Clear the old value before setting the new one - if (!string.IsNullOrEmpty(ProtectedElements[index])) - { - MemoryUtil.UnsafeZeroMemory<char>(ProtectedElements[index]); - } - //set new value - ProtectedElements[index] = value; + SetValue(index, value); } } - /// <summary> - /// Create a new instance with fixed array size - /// </summary> - /// <param name="elements">Number of elements to protect</param> - public PrivateStringManager(int elements) + + private void SetValue(int index, string? value) { - //Allocate the string array - ProtectedElements = new string[elements]; + //Try to get the old reference and erase it + StringRef strRef = ProtectedElements[index]; + strRef.Erase(); + + //Set the new value and determine if it is interned + ProtectedElements[index] = value is null ? + new StringRef(null, false) + : new StringRef(value, string.IsInterned(value) != null); } - ///<inheritdoc/> - protected override void Free() + + /// <summary> + /// Gets a copy of the string at the specified index. The + /// value returned is safe from erasure and is an independent + /// string + /// </summary> + /// <param name="index">The index to get the copy of the string at</param> + /// <returns>The copied string instance</returns> + protected string? CopyStringAtIndex(int index) { - //Zero all strings specified - for (int i = 0; i < ProtectedElements.Length; i++) + Check(); + StringRef str = ProtectedElements[index]; + + if(str.Value is null) { - if (!string.IsNullOrEmpty(ProtectedElements[i])) - { - //Zero the string memory - MemoryUtil.UnsafeZeroMemory<char>(ProtectedElements[i]); - //Set to null - ProtectedElements[i] = null; - } + //Pass null + return null; + } + else if (str.IsInterned) + { + /* + * If string is interned, it is safe to return the + * string referrence as it will not be erased + */ + return str.Value; + } + else + { + //Copy to new clr string + return str.Value.AsSpan().ToString(); } } - /// <summary> - /// Creates a deep copy for a new independent <see cref="PrivateStringManager"/> - /// </summary> - /// <returns>A new independent <see cref="PrivateStringManager"/> instance</returns> - /// <remarks>Be careful duplicating large instances, and make sure clones are properly disposed if necessary</remarks> - /// <exception cref="ObjectDisposedException"></exception> - public virtual object Clone() + ///<inheritdoc/> + protected override void Free() => Array.ForEach(ProtectedElements, static p => p.Erase()); + + private readonly record struct StringRef(string? Value, bool IsInterned) { - Check(); - PrivateStringManager other = new (ProtectedElements.Length); - //Copy all strings to the other instance - for(int i = 0; i < ProtectedElements.Length; i++) + public readonly void Erase() { - //Copy all strings and store their copies in the new array - other.ProtectedElements[i] = this.ProtectedElements[i].AsSpan().ToString(); + /* + * Only erase if the string is not interned + * and is not null + */ + if (Value is not null && !IsInterned) + { + MemoryUtil.UnsafeZeroMemory<char>(Value); + } } - //return the new copy - return other; } } } |