/* * Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Accounts * File: AccountsEntryPoint.cs * * AccountsEntryPoint.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Essentials.Accounts 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.Accounts 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.Text.Json; using FluentValidation.Results; using VNLib.Utils; using VNLib.Utils.Memory; using VNLib.Utils.Logging; using VNLib.Plugins.Essentials.Users; using VNLib.Plugins.Essentials.Middleware; using VNLib.Plugins.Essentials.Accounts.MFA; using VNLib.Plugins.Essentials.Accounts.Endpoints; using VNLib.Plugins.Essentials.Accounts.SecurityProvider; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Users; using VNLib.Plugins.Extensions.Loading.Routing; using VNLib.Plugins.Essentials.Accounts.MFA.Otp; using VNLib.Plugins.Essentials.Accounts.MFA.Totp; using VNLib.Plugins.Essentials.Accounts.MFA.Fido; namespace VNLib.Plugins.Essentials.Accounts { public sealed class AccountsEntryPoint : PluginBase { public override string PluginName => "Essentials.Accounts"; private bool SetupMode => HostArgs.HasArgument("--account-setup"); /// protected override void OnLoad() { //Add optional endpoint routing if (this.HasConfigForType()) { this.Route(); this.Route(); } if (this.HasConfigForType()) { this.Route(); } if (this.HasConfigForType()) { this.Route(); } if (this.HasConfigForType()) { this.Route(); } if (this.HasConfigForType()) { this.Route(); } if (this.HasConfigForType()) { this.Route(); } if (this.HasConfigForType()) { this.Route(); } //Only export the account security service if the configuration element is defined if (this.HasConfigForType()) { //Inint the security provider and export it AccountSecProvider securityProvider = this.GetOrCreateSingleton(); this.ExportService(securityProvider); //Also add the middleware array this.ExportService([ securityProvider ]); Log.Information("Configuring the account security provider service"); } if (SetupMode) { Log.Warn("Setup mode is enabled, this is not recommended for production use"); } //Write loaded to log Log.Information("Plugin loaded"); } protected override void OnUnLoad() { //Write closing messsage and dispose the log Log.Information("Plugin unloaded"); } protected override async void ProcessHostCommand(string cmd) { //Only process commands if the plugin is in setup mode if (!SetupMode) { return; } try { //Create argument parser ArgumentList args = new(cmd.Split(' ')); IUserManager Users = this.GetOrCreateSingleton(); string? username = args.GetArgument("-u"); string? password = args.GetArgument("-p"); if (args.Count < 1) { Log.Warn("Not enough arguments, use the help command to view available commands"); return; } switch (args[0].ToLower(null)) { case "help": const string help = @" Command help for {name} Commands: create -u -p Create a new user reset-password -u -p -l Reset a user's password delete -u Delete a user disable-mfa -u Disable a user's MFA configuration enable-totp -u -s Enable TOTP MFA for a user set-privilege -u -l Set a user's privilege level add-pubkey -u Add a JWK public key to a user's profile help Display this help message "; Log.Information(help, PluginName); break; //Create new user case "create": { if (username == null || password == null) { Log.Warn("You are missing required argument values. Format 'create -u -p '"); break; } string? privilege = args.GetArgument("-l"); if(!ulong.TryParse(privilege, out ulong privLevel)) { privLevel = AccountUtil.MINIMUM_LEVEL; } //Create the user creation request UserCreationRequest creation = new() { Username = username, InitialStatus = UserStatus.Active, Privileges = privLevel, Password = PrivateString.ToPrivateString(password, false) }; //Create the user using IUser user = await Users.CreateUserAsync(creation, null); //Set local account user.SetAccountOrigin(AccountUtil.LOCAL_ACCOUNT_ORIGIN); await user.ReleaseAsync(); Log.Information("Successfully created user {id}", username); } break; case "reset-password": { if (username == null || password == null) { Log.Warn("You are missing required argument values. Format 'create -u -p '"); break; } //Get the user using IUser? user = await Users.GetUserFromUsernameAsync(username); if(user == null) { Log.Warn("The specified user does not exist"); break; } //Set the password await Users.UpdatePasswordAsync(user, password); Log.Information("Successfully reset password for {id}", username); } break; case "delete": { if(username == null) { Log.Warn("You are missing required argument values. Format 'delete -u '"); break; } //Get user using IUser? user = await Users.GetUserFromUsernameAsync(username); if (user == null) { Log.Warn("The specified user does not exist"); break; } //delete user user.Delete(); //Release user await user.ReleaseAsync(); Log.Information("Successfully deleted user {id}", username); } break; case "disable-mfa": { if (username == null) { Log.Warn("You are missing required argument values. Format 'disable-mfa -u '"); break; } //Get user using IUser? user = await Users.GetUserFromUsernameAsync(username); if (user == null) { Log.Warn("The specified user does not exist"); break; } //Disable all mfa methods user.TotpDisable(); //user.OtpDisable(); user.FidoDisable(); await user.ReleaseAsync(); Log.Information("Successfully disabled MFA for {id}", username); } break; case "enable-totp": { string? secret = args.GetArgument("-s"); if (username == null || secret == null) { Log.Warn("You are missing required argument values. Format 'enable-totp -u -s '"); break; } //Get user using IUser? user = await Users.GetUserFromUsernameAsync(username); if (user == null) { Log.Warn("The specified user does not exist"); break; } try { byte[] sec = VnEncoding.FromBase32String(secret) ?? throw new Exception(""); } catch { Log.Error("Your TOTP secret is not valid base32"); break; } //Update the totp secret and flush changes user.TotpSetSecret(secret); await user.ReleaseAsync(); Log.Information("Successfully set TOTP secret for {id}", username); } break; case "add-pubkey": { if (string.IsNullOrWhiteSpace(username)) { Log.Warn("You are missing required argument values. Format 'add-pubkey -u "); break; } Console.WriteLine("Enter public key JWK..."); //Wait for pubkey string? pubkeyJwk = Console.ReadLine(); if(string.IsNullOrWhiteSpace(pubkeyJwk)) { Log.Warn("No public key supplied."); break; } //Get user using IUser? user = await Users.GetUserFromUsernameAsync(username); if (user == null) { Log.Warn("The specified user does not exist"); break; } OtpAuthPublicKey? pubkey = JsonSerializer.Deserialize(pubkeyJwk); if (pubkey == null) { Log.Error("You public key is not a JSON object"); break; } //Validate ValidationResult res = PkiLoginEndpoint.UserJwkValidator.Validate(pubkey); if (!res.IsValid) { Log.Error("The public key JWK is not valid:\n{errors}", res.ToDictionary()); break; } //Add/update the public key and flush changes user.OtpAddPublicKey(pubkey); await user.ReleaseAsync(); Log.Information("Successfully set TOTP secret for {id}", username); } break; case "set-privilege": { if (username == null) { Log.Warn("You are missing required argument values. Format 'set-privilege -u -l '"); break; } string? privilege = args.GetArgument("-l"); if (!ulong.TryParse(privilege, out ulong privLevel)) { Log.Warn("You are missing required argument values. Format 'set-privilege -u -l '"); break; } //Get user using IUser? user = await Users.GetUserFromUsernameAsync(username); if (user == null) { Log.Warn("The specified user does not exist"); break; } user.Privileges = privLevel; await user.ReleaseAsync(); Log.Information("Successfully set privilege level for {id}", username); } break; default: Log.Warn("Uknown command, use the help command"); break; } } catch (UserExistsException) { Log.Error("User already exists"); } catch(UserCreationFailedException ucfe) { Log.Error(ucfe, "Failed to create the new user"); } catch (ArgumentOutOfRangeException) { Log.Error("You are missing required command arguments"); } catch(Exception ex) { Log.Error(ex); } } } }