/*
* 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);
}
}
}
}