@page "/admin/users" @page "/admin/users/{DatabaseStr}" @page "/admin/users/{DatabaseStr}/{DatabaseInstanceStr}" @using Rms.Risk.Mango.Pivot.Core.MongoDb @using Rms.Risk.Mango.Services.Context @attribute [Authorize] @inject NavigationManager NavigationManager @inject IUserSession UserSession @inject IJSRuntime JsRuntime @inject IMongoDbServiceFactory MongoDbServiceFactory; @* * dbMango * * Copyright 2025 Deutsche Bank AG * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *@

Users Management

@if (Error != null) { }
@context.UserName
@if (context.IsBuiltin) {
BuiltIn
} else if ( !string.IsNullOrWhiteSpace( context.Db )) {
@context.Db
}
@code { [CascadingParameter] public IModalService Modal { get; set; } = null!; [Parameter] public string? DatabaseStr { get; set; } [Parameter] public string? DatabaseInstanceStr { get; set; } private string Database { get => UserSession.Database; set { if (UserSession.Database == value) return; UserSession.Database = value; SyncUrl(); } } private string DatabaseInstance { get => UserSession.DatabaseInstance; set { if (UserSession.DatabaseInstance == value) return; UserSession.DatabaseInstance = value; SyncUrl(); } } private Exception? Error { get; set; } private bool IsReady { get; set; } private UserInfoModel SelectedUser { get; set { if ( field == value ) return; field = value; // Populate AllRoles with non-built-in roles from the same database and built-in roles paired with it. AllRoles = RolesList.Where(x => !x.IsBuiltin && x.Db == field.Db) .Select(x => new RoleInDbModel { Role = x.RoleName, Db = x.Db } ) .Concat( RolesList .Where(x => x.IsBuiltin) .Select(x => new RoleInDbModel { Role = x.RoleName, Db = "" }) ) .DistinctBy(x => $"{x.Db}, {x.Role}") .OrderBy(x => $"{x.Db}, {x.Role}") .ToList() ; EditableSelectedUser = field.Clone(); } } = new(); private UserInfoModel EditableSelectedUser { get; set; } = new(); private List RolesList { get; set; } = []; private List AllRoles { get; set; } = []; private List AllDatabases { get; set; } = []; private List UserList { get; set; } = []; private bool IsNew => UserList.All(x => x.UserName != EditableSelectedUser.UserName); private bool CanUpdate => !EditableSelectedUser.IsBuiltin; private bool CanDelete => !EditableSelectedUser.IsBuiltin && !IsNew; private bool IsSelectable(UserInfoModel arg) => true; protected override void OnAfterRender(bool firstRender) { if (!firstRender) return; if (string.IsNullOrWhiteSpace(DatabaseStr)) DatabaseStr = Database; else Database = DatabaseStr; if (string.IsNullOrWhiteSpace(DatabaseInstanceStr)) DatabaseInstanceStr = DatabaseInstance; else DatabaseInstance = DatabaseInstanceStr; SyncUrl(); StateHasChanged(); Task.Run(LoadUsers); } private Task CanExecuteCommand() => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); private async Task Run(Func body) { var ticket = await CanExecuteCommand(); if (string.IsNullOrWhiteSpace(ticket)) return; var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); try { IsReady = false; await InvokeAsync(StateHasChanged); await body(ticket, cts.Token); } catch (Exception e) { await Display(e); } finally { IsReady = true; await InvokeAsync(StateHasChanged); } } private Task LoadUsers() => Run(async (_, token) => { try { AllDatabases = (await UserSession.MongoDbAdminForAdminDatabase.ListDatabases(token)) .Select(x => x.Name) .ToList(); } catch (Exception) { AllDatabases = [UserSession.DatabaseInstance]; } var usersInfoTasks = AllDatabases.Select( async instanceName => { var db = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, instanceName); var users = await db.GetUsersInfo(token: token); foreach( var u in users.Users ) { u.Db = instanceName; } return users; // Add this line to return the users from the async function }) .ToList(); var adminRolesTask = UserSession.MongoDbAdminForAdminDatabase.GetRolesInfo(showBuiltInRoles: true, token: token); var customRolesTasks = AllDatabases.Select(x => LoadRolesForDatabaseInstance(x, token)).ToList(); await Task.WhenAll(customRolesTasks.Cast().Concat(usersInfoTasks).Concat([adminRolesTask])); var adminRoles = await adminRolesTask; var all = new RolesInfoModel(); all.Roles.AddRange(adminRoles.Roles.Where(x => x.IsBuiltin)); foreach (var customTask in customRolesTasks) { var custom = await customTask; all.Roles.AddRange(custom.Roles); } RolesList = all.Roles .OrderBy(x => $"{x.Db}, {x.RoleName}") .ToList() ; UsersModel usersInfo = new(); foreach (var customTask in usersInfoTasks) { var custom = await customTask; usersInfo.Users.AddRange(custom.Users); } // Skip the admin user, as it is not editable var adminUser = usersInfo.Users.FirstOrDefault(x => x.UserName == "admin"); if ( adminUser != null ) adminUser.IsBuiltin = true; await Display(usersInfo); }); private Task LoadRolesForDatabaseInstance(string instanceName, CancellationToken token) { try { var db = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, instanceName); return db.GetRolesInfo(showBuiltInRoles: false, token: token); } catch (Exception) { return Task.FromResult(new RolesInfoModel()); } } private Task Display(UsersModel model) { Error = null; var oldUserName = SelectedUser.UserName; UserList = model.Users .OrderBy(x => x.UserName) .ToList() ; SelectedUser = !string.IsNullOrWhiteSpace(oldUserName) ? model.Users.FirstOrDefault(x => x.UserName == oldUserName) ?? model.Users.FirstOrDefault() ?? new() : model.Users.FirstOrDefault() ?? new UserInfoModel() ; return InvokeAsync(StateHasChanged); } private Task Display(Exception e) { Error = e; UserList.Clear(); AddUser(); return InvokeAsync(StateHasChanged); } private void SyncUrl() { var url = NavigationManager.BaseUri + $"admin/users/{Database}"; if (!string.IsNullOrWhiteSpace(DatabaseInstance)) url += $"/{DatabaseInstance}"; JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); } private static string GetUserClass(UserInfoModel r) => r.IsBuiltin ? "builtin" : ""; private static int _count; private Task AddUser() { SelectedUser = new () { UserName = $"newUser{++_count}", Db = UserSession.DatabaseInstance, IsBuiltin = false }; return InvokeAsync(StateHasChanged); } private async Task DeleteUser() { if ( IsNew || string.IsNullOrWhiteSpace(SelectedUser.UserName) || SelectedUser.IsBuiltin ) { await ModalDialogUtils.ShowInfoDialog( Modal, "Oops!", "Cannot delete the selected user."); return; } var rc = await ModalDialogUtils.ShowConfirmationDialog( Modal, "Confirm Deletion", $"Are you sure you want to delete the user '{SelectedUser.UserName}' from '{SelectedUser.Db}'?" ); if (!rc.Confirmed) return; // Logic for deleting the selected user goes here await Run(async (_, token) => { // Attention: use the admin service corresponding to the database you are going to delete user in! var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, SelectedUser.Db); await service.DropUser(SelectedUser.UserName, token); await LoadUsers(); }); } private async Task UpdateUser() { var user = EditableSelectedUser; var origUser = UserList.FirstOrDefault(x => x.UserName == user.UserName && x.Db == user.Db); if (user.IsBuiltin || string.IsNullOrWhiteSpace(user.UserName)) { await ModalDialogUtils.ShowInfoDialog(Modal, "Oops!", $"Can't modify built-in user '{user.UserName}'."); return; } var diff = CompareRoles(origUser ?? new UserInfoModel(), user); var diffDict = diff .ToDictionary(x => $"{x.Db} {x.Role}{(x.IsBuiltin ? " [Builtin]" : "")}", x => x.IsAdded ? "Added" : "Removed"); if (!string.IsNullOrWhiteSpace(user.Password)) { diffDict["Password"] = "Updated"; } if (diffDict.Count == 0) { await ModalDialogUtils.ShowInfoDialog(Modal, "No Changes", "No changes detected in the user roles and new password is not supplied."); return; } var rc = await ModalDialogUtils.ShowConfirmationDialog( Modal, "Warning", $"Are you sure you want to {(origUser == null ? "add" : "update")} the user '{user.UserName}' within database '{user.Db}'?", diffDict ); if (!rc.Confirmed) return; try { var cts = new CancellationTokenSource(DatabaseConfigurationService.DefaultTimeout); if ( string.IsNullOrWhiteSpace(user.Db) ) user.Db = UserSession.DatabaseInstance; if (origUser == null) await CreateUser(user, cts.Token); else if (!string.IsNullOrWhiteSpace(user.Password)) await UpdatePassword(user, cts.Token); await GrantAndRevokeRoles(user, diff, cts.Token); } catch (Exception ex) { await Display(ex); } } private async Task CreateUser(UserInfoModel user, CancellationToken token) { var command = new BsonDocument { { "createUser", user.UserName }, { "pwd", user.Password }, { "roles", new BsonArray() }, { "writeConcern",new BsonDocument { { "w", "majority" } } } }; var redactedCommand = command.ToJson().Replace(user.Password ?? "******", "******"); var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, user.Db); await service.RunCommand(command, redactedCommand, token); } private async Task UpdatePassword(UserInfoModel user, CancellationToken token) { if (string.IsNullOrWhiteSpace(user.Password)) throw new("Password is not specified"); var command = new BsonDocument { { "updateUser", user.UserName }, // Changed from createUser to updateUser { "pwd", user.Password }, { "writeConcern",new BsonDocument { { "w", "majority" } } } }; var redactedCommand = command.ToJson().Replace(user.Password, "******"); var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, user.Db); await service.RunCommand(command, redactedCommand, token); } private async Task GrantAndRevokeRoles(UserInfoModel user, List diff, CancellationToken token) { var toAdd = diff .Where(x => x.IsAdded) .GroupBy(x => x.Db) .Select(x => new { Db = x.Key, Roles = x.Select(y => new { y.Role, y.IsBuiltin }).ToList() }) .ToList(); var toRemove = diff .Where(x => !x.IsAdded) .GroupBy(x => x.Db) .Select(x => new { Db = x.Key, Roles = x.Select(y => new { y.Role, y.IsBuiltin }).ToList() }) // Update this to include IsBuiltin .ToList(); foreach (var action in toAdd) { var command = new BsonDocument { { "grantRolesToUser", user.UserName }, { "roles", new BsonArray(action.Roles .Select(role => role.IsBuiltin ? BsonValue.Create(role.Role) : new BsonDocument { { "role", role.Role }, { "db", action.Db } } ) ) }, { "writeConcern",new BsonDocument { { "w", "majority" } } } }; // Attention: use the admin service corresponding to the database you are going to create user in! var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, user.Db ); await service.RunCommand(command, token); } foreach (var action in toRemove) { var command = new BsonDocument { { "revokeRolesFromUser", user.UserName }, { "roles", new BsonArray(action.Roles .Select(role => role.IsBuiltin ? BsonValue.Create(role.Role) : new BsonDocument { { "role", role.Role }, { "db", action.Db } } ) ) }, { "writeConcern",new BsonDocument { { "w", "majority" } } } }; // Attention: use the admin service corresponding to the database you are going to create user in! var service = MongoDbServiceFactory.CreateAdmin(UserSession.Database, UserSession, user.Db); await service.RunCommand(command, token); } await LoadUsers(); } private async Task CopyUser() { try { var json = JsonUtils.ToJson(SelectedUser, new() { WriteIndented = true }); await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", json); await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", "User copied to clipboard."); } catch (Exception ex) { await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); } } private class RoleDiff { public string Role { get; set; } = string.Empty; public string Db { get; set; } = string.Empty; public bool IsBuiltin { get; set; } public bool IsAdded { get; set; } } private List CompareRoles(UserInfoModel user, UserInfoModel clone) { var builtinRoles = new HashSet(RolesList.Where(x => x.IsBuiltin).Select(x => x.RoleName)); // Identify roles present in the original user but missing in the clone var diffs = user.Roles .Where(role => !clone.Roles.Any(r => r.Role == role.Role && r.Db == role.Db)) .Select(role => new RoleDiff { Role = role.Role, Db = role.Db, IsBuiltin = builtinRoles.Contains(role.Role), IsAdded = false } ) .ToList(); // Identify roles present in the clone but missing in the original user diffs.AddRange(clone.Roles .Where(role => !user.Roles.Any(r => r.Role == role.Role && r.Db == role.Db)) .Select(role => new RoleDiff { Role = role.Role, Db = role.Db, IsBuiltin = builtinRoles.Contains(role.Role), IsAdded = true } ) ); return diffs; } private async Task PasteUser() { try { var json = await JsRuntime.InvokeAsync("DashboardUtils.PasteFromClipboard"); var clone = JsonUtils.FromJson(json); if (clone == null) return; // Search for the existing user by name,db var existingUser = UserList.FirstOrDefault(x => x.UserName == clone.UserName && x.Db == clone.Db); if ( existingUser != null ) { if ( existingUser.IsBuiltin ) { // If the user is built-in, show a warning and do not overwrite await ModalDialogUtils.ShowInfoDialog(Modal, "Oops!", $"User '{clone.UserName}' is a built-in user and cannot be changed."); return; } // If the user already exists, show a warning and do not overwrite var rc = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Warning", $"User '{clone.UserName}' already exists. Please choose a different name."); if (!rc.Confirmed) return; } SelectedUser = clone; await InvokeAsync(StateHasChanged); await ModalDialogUtils.ShowInfoDialog(Modal, "Copied", $"User info for {SelectedUser.UserName} was successfully parsed. You still need to save it!"); } catch (Exception ex) { await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", ex); } } }