aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-11-05 21:06:47 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-11-05 21:06:47 -0500
commitc582d82d34c7cad9e4c4f874adb91246704826c5 (patch)
tree16389667f6eb7d014957b94b1cc8a4377fa5b2d4
parente49c82df7599b8f33000699709aefffff332c146 (diff)
bug fixes, and api updates
-rw-r--r--lib/Net.Http/src/Core/HttpCookie.cs20
-rw-r--r--lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs2
-rw-r--r--lib/Net.Http/src/Helpers/HelperTypes.cs4
-rw-r--r--lib/Plugins.Essentials/src/Accounts/AccountUtils.cs121
-rw-r--r--lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs51
-rw-r--r--lib/Plugins.Essentials/src/Extensions/ICookieController.cs54
-rw-r--r--lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs124
-rw-r--r--lib/Plugins.Essentials/src/Users/IUser.cs23
-rw-r--r--lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs61
-rw-r--r--lib/Plugins.Essentials/src/Users/IUserManager.cs79
-rw-r--r--lib/Plugins.Essentials/src/Users/PassValidateFlags.cs45
-rw-r--r--lib/Utils/src/Memory/PrivateString.cs182
-rw-r--r--lib/Utils/src/Memory/PrivateStringManager.cs116
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;
}
}
}