From bb86cb52f6d82d1366893c29b83bb5a55bde95c4 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:11:36 +1000 Subject: [PATCH] Added a command line tool for user management (#420) The CLI tool can be started from the root of the /App directory in the container with the command: `./gaseous-cli` Running without arguments presents the following help screen: ``` Gaseous CLI - A tool for managing the Gaseous Server Usage: gaseous-cli [command] [options] Commands: user [command] [options] - Manage users role [command] [options] - Manage roles help - Display this help message ``` --- Gaseous.sln | 6 + build/Dockerfile | 8 + build/Dockerfile-EmbeddedDB | 8 + gaseous-cli/Program.cs | 354 ++++++++++++++++++ gaseous-cli/gaseous-cli.csproj | 28 ++ .../Classes/Auth/Models/UserViewModel.cs | 51 +-- gaseous-server/Classes/Config.cs | 6 +- gaseous-server/Program.cs | 66 ---- 8 files changed, 436 insertions(+), 91 deletions(-) create mode 100644 gaseous-cli/Program.cs create mode 100644 gaseous-cli/gaseous-cli.csproj diff --git a/Gaseous.sln b/Gaseous.sln index 9eca1ea..4b55ce3 100644 --- a/Gaseous.sln +++ b/Gaseous.sln @@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "screenshots", "screenshots" screenshots\Game.png = screenshots\Game.png EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaseous-cli", "gaseous-cli\gaseous-cli.csproj", "{419CC4E4-8932-4E4A-B027-5521AA0CBA85}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,10 @@ Global {A01D2EFF-C82E-473B-84D7-7C25E736F5D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {A01D2EFF-C82E-473B-84D7-7C25E736F5D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {A01D2EFF-C82E-473B-84D7-7C25E736F5D2}.Release|Any CPU.Build.0 = Release|Any CPU + {419CC4E4-8932-4E4A-B027-5521AA0CBA85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {419CC4E4-8932-4E4A-B027-5521AA0CBA85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {419CC4E4-8932-4E4A-B027-5521AA0CBA85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {419CC4E4-8932-4E4A-B027-5521AA0CBA85}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build/Dockerfile b/build/Dockerfile index e806e36..800f17d 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -9,11 +9,19 @@ RUN echo "Build: $BUILDPLATFORM" # Copy everything COPY .. ./ + +# Build Gaseous Web Server # Restore as distinct layers RUN dotnet restore "gaseous-server/gaseous-server.csproj" -a $TARGETARCH # Build and publish a release RUN dotnet publish "gaseous-server/gaseous-server.csproj" --use-current-runtime --self-contained true -c Release -o out -a $TARGETARCH +# Build Gaseous CLI +# Restore as distinct layers +RUN dotnet restore "gaseous-cli/gaseous-cli.csproj" -a $TARGETARCH +# Build and publish a release +RUN dotnet publish "gaseous-cli/gaseous-cli.csproj" --use-current-runtime --self-contained true -c Release -o out -a $TARGETARCH + # update apt-get RUN apt-get update diff --git a/build/Dockerfile-EmbeddedDB b/build/Dockerfile-EmbeddedDB index 9be70af..23a3c67 100644 --- a/build/Dockerfile-EmbeddedDB +++ b/build/Dockerfile-EmbeddedDB @@ -9,11 +9,19 @@ RUN echo "Build: $BUILDPLATFORM" # Copy everything COPY .. ./ + +# Build Gaseous Web Server # Restore as distinct layers RUN dotnet restore "gaseous-server/gaseous-server.csproj" -a $TARGETARCH # Build and publish a release RUN dotnet publish "gaseous-server/gaseous-server.csproj" --use-current-runtime --self-contained true -c Release -o out -a $TARGETARCH +# Build Gaseous CLI +# Restore as distinct layers +RUN dotnet restore "gaseous-cli/gaseous-cli.csproj" -a $TARGETARCH +# Build and publish a release +RUN dotnet publish "gaseous-cli/gaseous-cli.csproj" --use-current-runtime --self-contained true -c Release -o out -a $TARGETARCH + # update apt-get RUN apt-get update diff --git a/gaseous-cli/Program.cs b/gaseous-cli/Program.cs new file mode 100644 index 0000000..672b4ac --- /dev/null +++ b/gaseous-cli/Program.cs @@ -0,0 +1,354 @@ +using System; +using Authentication; +using gaseous_server.Classes; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +/* ------------------------------------------------- */ +/* This tool is a CLI tool that is used to manage */ +/* the Gaseous Server. */ +/* Functions such as user management, and backups */ +/* are available. */ +/* ------------------------------------------------- */ + +// load app settings +Config.InitSettings(); + +// set up database connection +Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + +// set up identity +IServiceCollection services = new ServiceCollection(); +services.AddLogging(); + +services.AddIdentity(options => + { + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = true; + options.Password.RequiredLength = 10; + options.User.AllowedUserNameCharacters = null; + options.User.RequireUniqueEmail = true; + options.SignIn.RequireConfirmedPhoneNumber = false; + options.SignIn.RequireConfirmedEmail = false; + options.SignIn.RequireConfirmedAccount = false; + }) + .AddUserStore() + .AddRoleStore() + ; +services.AddScoped(); +services.AddScoped(); + +services.AddTransient, UserStore>(); +services.AddTransient, RoleStore>(); +var userManager = services.BuildServiceProvider().GetService>(); + +// load the command line arguments +string[] cmdArgs = Environment.GetCommandLineArgs(); + +// check if the user has entered any arguments +if (cmdArgs.Length == 1) +{ + // no arguments were entered + Console.WriteLine("Gaseous CLI - A tool for managing the Gaseous Server"); + Console.WriteLine("Usage: gaseous-cli [command] [options]"); + Console.WriteLine("Commands:"); + Console.WriteLine(" user [command] [options] - Manage users"); + Console.WriteLine(" role [command] [options] - Manage roles"); + // Console.WriteLine(" backup [command] [options] - Manage backups"); + // Console.WriteLine(" restore [command] [options] - Restore backups"); + Console.WriteLine(" help - Display this help message"); + return; +} + +// check if the user has entered the help command +if (cmdArgs[1] == "help") +{ + // display the help message + Console.WriteLine("Gaseous CLI - A tool for managing the Gaseous Server"); + Console.WriteLine("Usage: gaseous-cli [command] [options]"); + Console.WriteLine("Commands:"); + Console.WriteLine(" user [command] [options] - Manage users"); + Console.WriteLine(" role [command] [options] - Manage roles"); + // Console.WriteLine(" backup [command] [options] - Manage backups"); + // Console.WriteLine(" restore [command] [options] - Restore backups"); + Console.WriteLine(" help - Display this help message"); + return; +} + +// check if the user has entered the user command +if (cmdArgs[1] == "user") +{ + // check if the user has entered any arguments + if (cmdArgs.Length == 2) + { + // no arguments were entered + Console.WriteLine("User Management"); + Console.WriteLine("Usage: gaseous-cli user [command] [options]"); + Console.WriteLine("Commands:"); + Console.WriteLine(" add [username] [password] - Add a new user"); + Console.WriteLine(" delete [username] - Delete a user"); + Console.WriteLine(" resetpassword [username] [password] - Reset a user's password"); + Console.WriteLine(" list - List all users"); + return; + } + + // check if the user has entered the add command + if (cmdArgs[2] == "add") + { + // check if the user has entered the username and password + if (cmdArgs.Length < 5) + { + // the username and password were not entered + Console.WriteLine("Error: Please enter a username and password"); + return; + } + + // add a new user + UserTable userTable = new UserTable(db); + if (userTable.GetUserByEmail(cmdArgs[3]) != null) + { + Console.WriteLine("Error: User already exists"); + return; + } + + // create the user object + ApplicationUser user = new ApplicationUser + { + Id = Guid.NewGuid().ToString(), + Email = cmdArgs[3], + NormalizedEmail = cmdArgs[3].ToUpper(), + EmailConfirmed = true, + UserName = cmdArgs[3], + NormalizedUserName = cmdArgs[3].ToUpper() + }; + + // create the password + PasswordHasher passwordHasher = new PasswordHasher(); + user.PasswordHash = passwordHasher.HashPassword(user, cmdArgs[4]); + + await userManager.CreateAsync(user); + await userManager.AddToRoleAsync(user, "Player"); + + Console.WriteLine("User created successfully with default role: Player"); + + return; + } + + // check if the user has entered the delete command + if (cmdArgs[2] == "delete") + { + // check if the user has entered the username + if (cmdArgs.Length < 4) + { + // the username was not entered + Console.WriteLine("Error: Please enter a username"); + return; + } + + // delete the user + UserTable userTable = new UserTable(db); + ApplicationUser user = userTable.GetUserByEmail(cmdArgs[3]); + if (user == null) + { + Console.WriteLine("Error: User not found"); + return; + } + + await userManager.DeleteAsync(user); + + Console.WriteLine("User deleted successfully"); + + return; + } + + // check if the user has entered the resetpassword command + if (cmdArgs[2] == "resetpassword") + { + // check if the user has entered the username and password + if (cmdArgs.Length < 5) + { + // the username and password were not entered + Console.WriteLine("Error: Please enter a username and password"); + return; + } + + // reset the user's password + UserTable userTable = new UserTable(db); + ApplicationUser user = userTable.GetUserByEmail(cmdArgs[3]); + if (user == null) + { + Console.WriteLine("Error: User not found"); + return; + } + + // create the password + PasswordHasher passwordHasher = new PasswordHasher(); + user.PasswordHash = passwordHasher.HashPassword(user, cmdArgs[4]); + + await userManager.UpdateAsync(user); + + Console.WriteLine("Password reset successfully"); + + return; + } + + // check if the user has entered the list command + if (cmdArgs[2] == "list") + { + // list all users + UserTable userTable = new UserTable(db); + var userList = userTable.GetUsers(); + foreach (var user in userList) + { + var roles = await userManager.GetRolesAsync(user); + Console.WriteLine(user.Email + " - " + string.Join(", ", roles)); + } + return; + } +} + +// check if the user has entered the role command +if (cmdArgs[1] == "role") +{ + // check if the user has entered any arguments + if (cmdArgs.Length == 2) + { + // no arguments were entered + Console.WriteLine("Role Management"); + Console.WriteLine("Usage: gaseous-cli role [command] [options]"); + Console.WriteLine("Commands:"); + Console.WriteLine(" set [username] [role] - Set the role of a user"); + Console.WriteLine(" list - List all roles"); + return; + } + + // check if the user has entered the role command + if (cmdArgs[2] == "set") + { + // check if the user has entered the username and role + if (cmdArgs.Length < 5) + { + // the username and role were not entered + Console.WriteLine("Error: Please enter a username and role"); + return; + } + + // set the role of the user + UserTable userTable = new UserTable(db); + ApplicationUser user = userTable.GetUserByEmail(cmdArgs[3]); + if (user == null) + { + Console.WriteLine("Error: User not found"); + return; + } + + // remove all existing roles from user + var roles = await userManager.GetRolesAsync(user); + await userManager.RemoveFromRolesAsync(user, roles.ToArray()); + + // add the new role to the user + await userManager.AddToRoleAsync(user, cmdArgs[4]); + + Console.WriteLine("Role set successfully"); + + return; + } + + // check if the user has entered the list command + if (cmdArgs[2] == "list") + { + // list all roles + string[] roles = { "Player", "Gamer", "Admin" }; + foreach (var role in roles) + { + Console.WriteLine(role); + } + return; + } +} + +// // check if the user has entered the backup command +// if (cmdArgs[1] == "backup") +// { +// // check if the user has entered any arguments +// if (cmdArgs.Length == 2) +// { +// // no arguments were entered +// Console.WriteLine("Backup Management"); +// Console.WriteLine("Usage: gaseous-cli backup [command] [options]"); +// Console.WriteLine("Commands:"); +// Console.WriteLine(" create - Create a backup"); +// Console.WriteLine(" list - List all backups"); +// Console.WriteLine(" remove [backup_id] - Remove a backup"); +// return; +// } + +// // check if the user has entered the create command +// if (cmdArgs[2] == "create") +// { +// // create a backup +// Backup.CreateBackup(); +// return; +// } + +// // check if the user has entered the list command +// if (cmdArgs[2] == "list") +// { +// // list all backups +// Backup.ListBackups(); +// return; +// } + +// // check if the user has entered the remove command +// if (cmdArgs[2] == "remove") +// { +// // check if the user has entered the backup id +// if (cmdArgs.Length < 4) +// { +// // the backup id was not entered +// Console.WriteLine("Error: Please enter a backup id"); +// return; +// } + +// // remove the backup +// Backup.RemoveBackup(cmdArgs[3]); +// return; +// } +// } + +// // check if the user has entered the restore command +// if (cmdArgs[1] == "restore") +// { +// // check if the user has entered any arguments +// if (cmdArgs.Length == 2) +// { +// // no arguments were entered +// Console.WriteLine("Restore Management"); +// Console.WriteLine("Usage: gaseous-cli restore [command] [options]"); +// Console.WriteLine("Commands:"); +// Console.WriteLine(" restore [backup_id] - Restore a backup"); +// return; +// } + +// // check if the user has entered the restore command +// if (cmdArgs[2] == "restore") +// { +// // check if the user has entered the backup id +// if (cmdArgs.Length < 4) +// { +// // the backup id was not entered +// Console.WriteLine("Error: Please enter a backup id"); +// return; +// } + +// // restore the backup +// Restore.RestoreBackup(cmdArgs[3]); +// return; +// } +// } + +// the user entered an invalid command +Console.WriteLine("Error: Invalid command"); \ No newline at end of file diff --git a/gaseous-cli/gaseous-cli.csproj b/gaseous-cli/gaseous-cli.csproj new file mode 100644 index 0000000..13fad49 --- /dev/null +++ b/gaseous-cli/gaseous-cli.csproj @@ -0,0 +1,28 @@ + + + + Exe + net8.0 + gaseous_cli + enable + enable + + + + 4 + bin\Debug\net8.0\gaseous-cli.xml + + + 4 + bin\Release\net8.0\gaseous-cli.xml + + + + + + + + + + + diff --git a/gaseous-server/Classes/Auth/Models/UserViewModel.cs b/gaseous-server/Classes/Auth/Models/UserViewModel.cs index 0510011..f3469df 100644 --- a/gaseous-server/Classes/Auth/Models/UserViewModel.cs +++ b/gaseous-server/Classes/Auth/Models/UserViewModel.cs @@ -14,33 +14,40 @@ namespace Authentication get { string _highestRole = ""; - foreach (string role in Roles) + if (Roles != null) { - switch (role) + foreach (string role in Roles) { - case "Admin": - // there is no higher - _highestRole = role; - break; - case "Gamer": - // only one high is Admin, so check for that - if (_highestRole != "Admin") - { + switch (role) + { + case "Admin": + // there is no higher _highestRole = role; - } - break; - case "Player": - // make sure _highestRole isn't already set to Gamer or Admin - if (_highestRole != "Admin" && _highestRole != "Gamer") - { - _highestRole = role; - } - break; - default: - _highestRole = "Player"; - break; + break; + case "Gamer": + // only one high is Admin, so check for that + if (_highestRole != "Admin") + { + _highestRole = role; + } + break; + case "Player": + // make sure _highestRole isn't already set to Gamer or Admin + if (_highestRole != "Admin" && _highestRole != "Gamer") + { + _highestRole = role; + } + break; + default: + _highestRole = "Player"; + break; + } } } + else + { + _highestRole = "Player"; + } return _highestRole; } diff --git a/gaseous-server/Classes/Config.cs b/gaseous-server/Classes/Config.cs index cf36e76..f7b88e9 100644 --- a/gaseous-server/Classes/Config.cs +++ b/gaseous-server/Classes/Config.cs @@ -154,8 +154,8 @@ namespace gaseous_server.Classes } } - Console.WriteLine("Using configuration:"); - Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(_config, Formatting.Indented)); + // Console.WriteLine("Using configuration:"); + // Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(_config, Formatting.Indented)); } public static void UpdateConfig() @@ -202,7 +202,7 @@ namespace gaseous_server.Classes AppSettings.Remove(SettingName); } - Logging.Log(Logging.LogType.Information, "Load Settings", "Loading setting " + SettingName + " from database"); + // Logging.Log(Logging.LogType.Information, "Load Settings", "Loading setting " + SettingName + " from database"); try { diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index c44a51d..ff86a81 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -323,72 +323,6 @@ app.Use(async (context, next) => await next(); }); -// emergency password recovery if environment variable is set -// process: -// - set the environment variable "recoveraccount" to the email address of the account to be recovered -// - when the server starts the password will be reset to a random string and saved in the library -// directory with the name RecoverAccount.txt -// - user should copy this password and remove the "recoveraccount" environment variable and the -// RecoverAccount.txt file -// - the server will not start while the RecoverAccount.txt file exists -string PasswordRecoveryFile = Path.Combine(Config.LibraryConfiguration.LibraryRootDirectory, "RecoverAccount.txt"); -if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("recoveraccount"))) -{ - if (File.Exists(PasswordRecoveryFile)) - { - // password has already been set - do nothing and just exit - Logging.Log(Logging.LogType.Critical, "Server Startup", "Unable to start while recoveraccount environment varibale is set and RecoverAccount.txt file exists.", null, true); - Environment.Exit(0); - } - else - { - // generate and save the password to disk - int length = 10; - string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+"; - var random = new Random(); - string password = new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); - - File.WriteAllText(PasswordRecoveryFile, password); - - // reset the password - using (var scope = app.Services.CreateScope()) - { - var userManager = scope.ServiceProvider.GetRequiredService(); - if (await userManager.FindByNameAsync(Environment.GetEnvironmentVariable("recoveraccount"), CancellationToken.None) != null) - { - ApplicationUser User = await userManager.FindByEmailAsync(Environment.GetEnvironmentVariable("recoveraccount"), CancellationToken.None); - - //set user password - PasswordHasher ph = new PasswordHasher(); - User.PasswordHash = ph.HashPassword(User, password); - - await userManager.SetPasswordHashAsync(User, User.PasswordHash, CancellationToken.None); - - Logging.Log(Logging.LogType.Information, "Server Startup", "Password reset complete, remove the recoveraccount environment variable and RecoverAccount.text file to allow server start.", null, true); - - Environment.Exit(0); - } - else - { - Logging.Log(Logging.LogType.Critical, "Server Startup", "Account to recover not found.", null, true); - - Environment.Exit(0); - } - } - - } -} -else -{ - // check if RecoverAccount.text file is present - if (File.Exists(PasswordRecoveryFile)) - { - // cannot start while password recovery file exists - Logging.Log(Logging.LogType.Critical, "Server Startup", "Unable to start while RecoverAccount.txt file exists. Remove the file and try again.", null, true); - Environment.Exit(0); - } -} - // setup library directories Config.LibraryConfiguration.InitLibrary();