diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/UserFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/UserFilter.cs index 6ac35ed2..4c0d259c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/UserFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/UserFilter.cs @@ -14,6 +14,9 @@ public class UserFilter : Pagination [JsonPropertyName("roles")] public IEnumerable? Roles { get; set; } + [JsonPropertyName("types")] + public IEnumerable? Types { get; set; } + [JsonPropertyName("sources")] public IEnumerable? Sources { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 910b10c9..0958b4f4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -18,9 +18,10 @@ public interface IBotSharpRepository : IHaveServiceProvider #endregion #region Role + bool RefreshRoles(IEnumerable roles) => throw new NotImplementedException(); IEnumerable GetRoles(RoleFilter filter) => throw new NotImplementedException(); - Role? GetRoleDetails(string roleId) => throw new NotImplementedException(); - bool UpdateRole(Role role, bool isUpdateRoleAgents = false) => throw new NotImplementedException(); + Role? GetRoleDetails(string roleId, bool includeAgent = false) => throw new NotImplementedException(); + bool UpdateRole(Role role, bool updateRoleAgents = false) => throw new NotImplementedException(); #endregion #region User @@ -41,8 +42,8 @@ public interface IBotSharpRepository : IHaveServiceProvider void UpdateUserIsDisable(string userId, bool isDisable) => throw new NotImplementedException(); void UpdateUsersIsDisable(List userIds, bool isDisable) => throw new NotImplementedException(); PagedItems GetUsers(UserFilter filter) => throw new NotImplementedException(); - User? GetUserDetails(string userId) => throw new NotImplementedException(); - bool UpdateUser(User user, bool isUpdateUserAgents = false) => throw new NotImplementedException(); + User? GetUserDetails(string userId, bool includeAgent = false) => throw new NotImplementedException(); + bool UpdateUser(User user, bool updateUserAgents = false) => throw new NotImplementedException(); #endregion #region Agent diff --git a/src/Infrastructure/BotSharp.Abstraction/Roles/IRoleService.cs b/src/Infrastructure/BotSharp.Abstraction/Roles/IRoleService.cs index 12e45dfd..325c2e9c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Roles/IRoleService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Roles/IRoleService.cs @@ -5,6 +5,7 @@ namespace BotSharp.Abstraction.Roles; public interface IRoleService { + Task RefreshRoles(); Task> GetRoleOptions(); Task> GetRoles(RoleFilter filter); Task GetRoleDetails(string roleId); diff --git a/src/Infrastructure/BotSharp.Abstraction/Roles/Models/Role.cs b/src/Infrastructure/BotSharp.Abstraction/Roles/Models/Role.cs index a11029f5..a0c11bc3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Roles/Models/Role.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Roles/Models/Role.cs @@ -15,8 +15,8 @@ public class Role public IEnumerable AgentActions { get; set; } = []; [JsonPropertyName("updated_time")] - public DateTime UpdatedTime { get; set; } = DateTime.UtcNow; + public DateTime UpdatedTime { get; set; } [JsonPropertyName("created_time")] - public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + public DateTime CreatedTime { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Roles/Models/RoleAgent.cs b/src/Infrastructure/BotSharp.Abstraction/Roles/Models/RoleAgent.cs index 430a89a1..8b84591c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Roles/Models/RoleAgent.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Roles/Models/RoleAgent.cs @@ -18,8 +18,8 @@ public class RoleAgent public Agent? Agent { get; set; } [JsonPropertyName("updated_time")] - public DateTime UpdatedTime { get; set; } = DateTime.UtcNow; + public DateTime UpdatedTime { get; set; } [JsonPropertyName("created_time")] - public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + public DateTime CreatedTime { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserAction.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserAction.cs index 4838e757..b565a260 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserAction.cs @@ -4,4 +4,6 @@ public static class UserAction { public const string Edit = "edit"; public const string Chat = "chat"; + public const string Train = "train"; + public const string Evaluate = "evaluate"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserRole.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserRole.cs index 0bde3b08..59f7feff 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserRole.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserRole.cs @@ -2,21 +2,23 @@ namespace BotSharp.Abstraction.Users.Enums; public class UserRole { + public const string Root = "root"; + /// /// Admin account /// public const string Admin = "admin"; - /// - /// Customer service representative (CSR) - /// - public const string CSR = "csr"; - /// /// Authorized user /// public const string User = "user"; + /// + /// Customer service representative (CSR) + /// + public const string CSR = "csr"; + /// /// Back office operations /// @@ -33,6 +35,4 @@ public class UserRole /// AI Assistant /// public const string Assistant = "assistant"; - - public const string Root = "root"; } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs index 77e40418..ea20307a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs @@ -9,6 +9,8 @@ public interface IUserService Task GetUser(string id); Task> GetUsers(UserFilter filter); Task GetUserDetails(string userId); + Task IsAuthorizedUser(string userId); + Task GetUserAuthorizations(string? agentId = null); Task UpdateUser(User user, bool isUpdateUserAgents = false); Task CreateUser(User user); Task ActiveUser(UserActivationModel model); diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserAuthorization.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserAuthorization.cs new file mode 100644 index 00000000..bf4ff025 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserAuthorization.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Users.Models; + +public class UserAuthorization +{ + public bool IsAdmin { get; set; } + public IEnumerable Permissions { get; set; } = []; + public IEnumerable AgentActions { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs index df10ac4b..7d4af3ac 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs @@ -25,8 +25,11 @@ public partial class AgentService var agentSettings = _services.GetRequiredService(); var user = _db.GetUserById(_user.Id); + var userService = _services.GetRequiredService(); + var auth = await userService.GetUserAuthorizations(); + _db.BulkInsertAgents(new List { agentRecord }); - if (!UserConstant.AdminRoles.Contains(user.Role)) + if (auth.IsAdmin || auth.Permissions.Contains(UserPermission.CreateAgent)) { _db.BulkInsertUserAgents(new List { @@ -34,7 +37,7 @@ public partial class AgentService { UserId = user.Id, AgentId = agentRecord.Id, - Actions = new List { UserAction.Edit, UserAction.Chat }, + Actions = new List { UserAction.Edit, UserAction.Train, UserAction.Evaluate, UserAction.Chat }, CreatedTime = DateTime.UtcNow, UpdatedTime = DateTime.UtcNow } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs index 8a05542e..e8cd7573 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs @@ -6,11 +6,10 @@ public partial class AgentService { public async Task DeleteAgent(string id) { - var user = _db.GetUserById(_user.Id); - var userAgents = await GetUserAgents(user?.Id); - var found = userAgents?.FirstOrDefault(x => x.AgentId == id); + var userService = _services.GetRequiredService(); + var auth = await userService.GetUserAuthorizations(id); - if (!UserConstant.AdminRoles.Contains(user?.Role) && (found?.Actions == null || !found.Actions.Contains(UserAction.Edit))) + if (auth.IsAdmin || auth.AgentActions.Contains(UserAction.Edit)) { return false; } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs index 61861aca..95268a17 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs @@ -17,8 +17,9 @@ public partial class AgentService return refreshResult; } - var user = _db.GetUserById(_user.Id); - if (!UserConstant.AdminRoles.Contains(user.Role)) + var userService = _services.GetRequiredService(); + var isValid = await userService.IsAuthorizedUser(_user.Id); + if (!isValid) { return "Unauthorized user."; } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs index 17aa4aa7..c6b2a743 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs @@ -12,12 +12,9 @@ public partial class AgentService if (agent == null || string.IsNullOrEmpty(agent.Id)) return; var userService = _services.GetRequiredService(); - var user = await userService.GetUser(_user.Id); + var auth = await userService.GetUserAuthorizations(agent.Id); - var userAgents = await GetUserAgents(user.Id); - var found = userAgents?.FirstOrDefault(x => x.AgentId == agent.Id); - - if (!UserConstant.AdminRoles.Contains(user?.Role) && (found?.Actions == null || found.Actions.Contains(UserAction.Edit))) + if (!auth.IsAdmin && !auth.AgentActions.Contains(UserAction.Edit)) { return; } diff --git a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs index 4bdfe0d7..69d5b029 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs @@ -9,6 +9,7 @@ using BotSharp.Abstraction.Users.Settings; using BotSharp.Abstraction.Interpreters.Settings; using BotSharp.Abstraction.Infrastructures; using BotSharp.Core.Processors; +using BotSharp.Core.Roles.Services; namespace BotSharp.Core; @@ -23,6 +24,7 @@ public static class BotSharpCoreExtensions services.AddSingleton(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index dadc3532..346eebd8 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -528,7 +528,7 @@ namespace BotSharp.Core.Repository var agentDir = GetAgentDataDir(agentId); if (string.IsNullOrEmpty(agentDir)) return false; - // Delete agent user relationships + // Delete user agents var usersDir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER); if (Directory.Exists(usersDir)) { @@ -546,6 +546,24 @@ namespace BotSharp.Core.Repository } } + // Delete role agents + var rolesDir = Path.Combine(_dbSettings.FileRepository, ROLES_FOLDER); + if (Directory.Exists(rolesDir)) + { + foreach (var roleDir in Directory.GetDirectories(rolesDir)) + { + var roleAgentFile = Directory.GetFiles(roleDir).FirstOrDefault(x => Path.GetFileName(x) == ROLE_AGENT_FILE); + if (string.IsNullOrEmpty(roleAgentFile)) continue; + + var text = File.ReadAllText(roleAgentFile); + var roleAgents = JsonSerializer.Deserialize>(text, _options); + if (roleAgents.IsNullOrEmpty()) continue; + + roleAgents = roleAgents?.Where(x => x.AgentId != agentId)?.ToList() ?? []; + File.WriteAllText(roleAgentFile, JsonSerializer.Serialize(roleAgents, _options)); + } + } + // Delete agent folder Directory.Delete(agentDir, true); Reset(); @@ -561,6 +579,7 @@ namespace BotSharp.Core.Repository { _agents = []; _userAgents = []; + _roleAgents = []; } } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Role.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Role.cs index 13036a91..74acc45d 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Role.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Role.cs @@ -1,10 +1,39 @@ -using BotSharp.Abstraction.Users.Models; using System.IO; namespace BotSharp.Core.Repository; public partial class FileRepository { + public bool RefreshRoles(IEnumerable roles) + { + if (roles.IsNullOrEmpty()) return false; + + var validRoles = roles.Where(x => !string.IsNullOrWhiteSpace(x.Id) + && !string.IsNullOrWhiteSpace(x.Name)).ToList(); + if (validRoles.IsNullOrEmpty()) return false; + + var baseDir = Path.Combine(_dbSettings.FileRepository, ROLES_FOLDER); + if (Directory.Exists(baseDir)) + { + Directory.Delete(baseDir, true); + } + + Directory.CreateDirectory(baseDir); + + foreach (var role in validRoles) + { + var dir = Path.Combine(baseDir, role.Id); + Directory.CreateDirectory(dir); + Thread.Sleep(50); + var roleFile = Path.Combine(dir, ROLE_FILE); + role.CreatedTime = DateTime.UtcNow; + role.UpdatedTime = DateTime.UtcNow; + File.WriteAllText(roleFile, JsonSerializer.Serialize(role, _options)); + } + + return true; + } + public IEnumerable GetRoles(RoleFilter filter) { var roles = Roles; @@ -22,7 +51,7 @@ public partial class FileRepository return roles.ToList(); } - public Role? GetRoleDetails(string roleId) + public Role? GetRoleDetails(string roleId, bool includeAgent = false) { if (string.IsNullOrWhiteSpace(roleId)) return null; @@ -31,8 +60,20 @@ public partial class FileRepository var agentActions = new List(); var roleAgents = RoleAgents?.Where(x => x.RoleId == roleId)?.ToList() ?? []; - var agentIds = roleAgents.Select(x => x.AgentId).Distinct().ToList(); + if (!includeAgent) + { + agentActions = roleAgents.Select(x => new RoleAgentAction + { + Id = x.Id, + AgentId = x.AgentId, + Actions = x.Actions + }).ToList(); + role.AgentActions = agentActions; + return role; + } + + var agentIds = roleAgents.Select(x => x.AgentId).Distinct().ToList(); if (!agentIds.IsNullOrEmpty()) { var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); @@ -56,7 +97,7 @@ public partial class FileRepository return role; } - public bool UpdateRole(Role role, bool isUpdateRoleAgents = false) + public bool UpdateRole(Role role, bool updateRoleAgents = false) { if (string.IsNullOrEmpty(role?.Id) || string.IsNullOrEmpty(role?.Name)) { @@ -74,7 +115,7 @@ public partial class FileRepository role.UpdatedTime = DateTime.UtcNow; File.WriteAllText(roleFile, JsonSerializer.Serialize(role, _options)); - if (isUpdateRoleAgents) + if (updateRoleAgents) { var roleAgents = role.AgentActions?.Select(x => new RoleAgent { diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs index c55f248c..9600bbc8 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs @@ -1,7 +1,5 @@ -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Users.Models; -using System; using System.IO; namespace BotSharp.Core.Repository; @@ -97,6 +95,10 @@ public partial class FileRepository { users = users.Where(x => filter.Roles.Contains(x.Role)); } + if (!filter.Types.IsNullOrEmpty()) + { + users = users.Where(x => filter.Types.Contains(x.Type)); + } if (!filter.Sources.IsNullOrEmpty()) { users = users.Where(x => filter.Sources.Contains(x.Source)); @@ -109,17 +111,29 @@ public partial class FileRepository }; } - public User? GetUserDetails(string userId) + public User? GetUserDetails(string userId, bool includeAgent = false) { if (string.IsNullOrWhiteSpace(userId)) return null; - var user = Users.FirstOrDefault(x => x.Id == userId); + var user = Users.FirstOrDefault(x => x.Id == userId || x.ExternalId == userId); if (user == null) return null; var agentActions = new List(); var userAgents = UserAgents?.Where(x => x.UserId == userId)?.ToList() ?? []; - var agentIds = userAgents.Select(x => x.AgentId)?.Distinct().ToList(); + if (!includeAgent) + { + agentActions = userAgents.Select(x => new UserAgentAction + { + Id = x.Id, + AgentId = x.AgentId, + Actions = x.Actions + }).ToList(); + user.AgentActions = agentActions; + return user; + } + + var agentIds = userAgents.Select(x => x.AgentId)?.Distinct().ToList(); if (!agentIds.IsNullOrEmpty()) { var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); @@ -143,7 +157,7 @@ public partial class FileRepository return user; } - public bool UpdateUser(User user, bool isUpdateUserAgents = false) + public bool UpdateUser(User user, bool updateUserAgents = false) { if (string.IsNullOrEmpty(user?.Id)) return false; @@ -157,7 +171,7 @@ public partial class FileRepository user.UpdatedTime = DateTime.UtcNow; File.WriteAllText(userFile, JsonSerializer.Serialize(user, _options)); - if (isUpdateUserAgents) + if (updateUserAgents) { var userAgents = user.AgentActions?.Select(x => new UserAgent { diff --git a/src/Infrastructure/BotSharp.Core/Roles/Services/RoleService.cs b/src/Infrastructure/BotSharp.Core/Roles/Services/RoleService.cs index b9f04cfe..4319d06e 100644 --- a/src/Infrastructure/BotSharp.Core/Roles/Services/RoleService.cs +++ b/src/Infrastructure/BotSharp.Core/Roles/Services/RoleService.cs @@ -16,6 +16,15 @@ public class RoleService : IRoleService _logger = logger; } + public async Task RefreshRoles() + { + var allRoles = await GetRoleOptions(); + var roles = allRoles.Select(x => new Role { Id = Guid.NewGuid().ToString(), Name = x }).ToList(); + + var db = _services.GetRequiredService(); + return db.RefreshRoles(roles); + } + public async Task> GetRoleOptions() { var fields = typeof(UserRole).GetFields(BindingFlags.Public | BindingFlags.Static) @@ -37,7 +46,7 @@ public class RoleService : IRoleService public async Task GetRoleDetails(string roleId) { var db = _services.GetRequiredService(); - var role = db.GetRoleDetails(roleId); + var role = db.GetRoleDetails(roleId, includeAgent: true); return role; } diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index 9fa2b73a..94cf845c 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -407,10 +407,54 @@ public class UserService : IUserService return users; } + public async Task IsAuthorizedUser(string userId) + { + var db = _services.GetRequiredService(); + var user = db.GetUserById(userId); + return user != null && UserConstant.AdminRoles.Contains(user.Role); + } + + public async Task GetUserAuthorizations(string? agentId = null) + { + var db = _services.GetRequiredService(); + var user = db.GetUserById(_user.Id); + var auth = new UserAuthorization(); + + if (user == null) return auth; + + var permissions = user.Permissions; + + var role = db.GetRoles(new RoleFilter { Names = [ user.Role ] }).FirstOrDefault(); + if (role != null && !permissions.Any()) + { + permissions = role.Permissions ?? []; + } + + auth.IsAdmin = UserConstant.AdminRoles.Contains(user.Role); + auth.Permissions = permissions; + + if (string.IsNullOrEmpty(agentId)) + { + return auth; + } + + var userAgent = db.GetUserDetails(user.Id)?.AgentActions?.FirstOrDefault(x => x.AgentId == agentId); + var actions = userAgent?.Actions ?? []; + + if (role != null && !actions.Any()) + { + var roleAgent = db.GetRoleDetails(role.Id)?.AgentActions?.FirstOrDefault(x => x.AgentId == agentId); + actions = roleAgent?.Actions ?? []; + } + + auth.AgentActions = actions; + return auth; + } + public async Task GetUserDetails(string userId) { var db = _services.GetRequiredService(); - return db.GetUserDetails(userId); + return db.GetUserDetails(userId, includeAgent: true); } public async Task UpdateUser(User user, bool isUpdateUserAgents = false) diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs index 54c9b75e..06cfc77e 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs @@ -58,20 +58,11 @@ public class AgentController : ControllerBase rule.RedirectToAgentName = found.Name; } - var editable = true; - var chatable = true; var userService = _services.GetRequiredService(); - var user = await userService.GetUser(_user.Id); - if (!UserConstant.AdminRoles.Contains(user?.Role)) - { - var userAgents = await _agentService.GetUserAgents(user?.Id); - var actions = userAgents?.FirstOrDefault(x => x.AgentId == targetAgent.Id)?.Actions ?? []; - editable = actions.Contains(UserAction.Edit); - chatable = actions.Contains(UserAction.Chat); - } + var auth = await userService.GetUserAuthorizations(targetAgent.Id); - targetAgent.Editable = editable; - targetAgent.Chatable = chatable; + targetAgent.Editable = auth.IsAdmin || auth.AgentActions.Contains(UserAction.Edit); + targetAgent.Chatable = auth.IsAdmin || auth.AgentActions.Contains(UserAction.Chat); return targetAgent; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs index e4ec4aa8..b6178f54 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs @@ -22,9 +22,8 @@ public class PluginController : ControllerBase [HttpGet("/plugins")] public async Task> GetPlugins([FromQuery] PluginFilter filter) { - var userService = _services.GetRequiredService(); - var user = await userService.GetUser(_user.Id); - if (!UserConstant.AdminRoles.Contains(user?.Role)) + var isValid = await IsValidUser(); + if (!isValid) { return new PagedItems(); } @@ -55,7 +54,11 @@ public class PluginController : ControllerBase { Roles = new List { UserRole.Root, UserRole.Admin } }, - new PluginMenuDef("Users", link: "page/users", icon: "bx bx-user", weight: 33) + new PluginMenuDef("Roles", link: "page/roles", icon: "bx bx-group", weight: 33) + { + Roles = new List { UserRole.Root, UserRole.Admin } + }, + new PluginMenuDef("Users", link: "page/users", icon: "bx bx-user", weight: 34) { Roles = new List { UserRole.Root, UserRole.Admin } } @@ -91,4 +94,10 @@ public class PluginController : ControllerBase var loader = _services.GetRequiredService(); return loader.UpdatePluginStatus(_services, id, false); } + + private async Task IsValidUser() + { + var userService = _services.GetRequiredService(); + return await userService.IsAuthorizedUser(_user.Id); + } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RoleController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/RoleController.cs index 5eedc928..0bff6917 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/RoleController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/RoleController.cs @@ -21,6 +21,19 @@ public class RoleController : ControllerBase _user = user; } + [HttpPost("/role/refresh")] + public async Task RefreshRoles() + { + var isValid = await IsValidUser(); + if (!isValid) + { + return false; + } + + return await _roleService.RefreshRoles(); + } + + [HttpGet("/role/options")] public async Task> GetRoleOptions() { @@ -35,6 +48,12 @@ public class RoleController : ControllerBase filter = RoleFilter.Empty(); } + var isValid = await IsValidUser(); + if (!isValid) + { + return Enumerable.Empty(); + } + var roles = await _roleService.GetRoles(filter); return roles.Select(x => RoleViewModel.FromRole(x)).ToList(); } @@ -51,9 +70,8 @@ public class RoleController : ControllerBase { if (model == null) return false; - var userService = _services.GetRequiredService(); - var user = await userService.GetUser(_user.Id); - if (user == null || !UserConstant.AdminRoles.Contains(user.Role)) + var isValid = await IsValidUser(); + if (!isValid) { return false; } @@ -61,4 +79,10 @@ public class RoleController : ControllerBase var role = RoleUpdateModel.ToRole(model); return await _roleService.UpdateRole(role, isUpdateRoleAgents: true); } + + private async Task IsValidUser() + { + var userService = _services.GetRequiredService(); + return await userService.IsAuthorizedUser(_user.Id); + } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs index 366bb91b..5ea5f18c 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs @@ -182,8 +182,8 @@ public class UserController : ControllerBase public async Task> GetUsers([FromBody] UserFilter filter) { var userService = _services.GetRequiredService(); - var user = await userService.GetUser(_user.Id); - if (user == null || !UserConstant.AdminRoles.Contains(user.Role)) + var isValid = await IsValidUser(); + if (!isValid) { return new PagedItems(); } @@ -211,13 +211,13 @@ public class UserController : ControllerBase { if (model == null) return false; - var userService = _services.GetRequiredService(); - var user = await userService.GetUser(_user.Id); - if (user == null || !UserConstant.AdminRoles.Contains(user.Role)) + var isValid = await IsValidUser(); + if (!isValid) { return false; } + var userService = _services.GetRequiredService(); var updated = await userService.UpdateUser(UserUpdateModel.ToUser(model), isUpdateUserAgents: true); return updated; } @@ -252,6 +252,12 @@ public class UserController : ControllerBase #region Private methods + private async Task IsValidUser() + { + var userService = _services.GetRequiredService(); + return await userService.IsAuthorizedUser(_user.Id); + } + private FileContentResult BuildFileResult(string file) { var fileStorage = _services.GetRequiredService(); diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleViewModel.cs index e060b482..d4e8de33 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleViewModel.cs @@ -18,10 +18,10 @@ public class RoleViewModel public IEnumerable AgentActions { get; set; } = []; [JsonPropertyName("create_date")] - public DateTime CreateDate { get; set; } + public DateTime? CreateDate { get; set; } [JsonPropertyName("update_date")] - public DateTime UpdateDate { get; set; } + public DateTime? UpdateDate { get; set; } public static RoleViewModel FromRole(Role? role) { @@ -33,8 +33,8 @@ public class RoleViewModel Name = role.Name, Permissions = role.Permissions, AgentActions = role.AgentActions?.Select(x => RoleAgentActionViewModel.ToViewModel(x)) ?? [], - CreateDate = role.CreatedTime, - UpdateDate = role.UpdatedTime + CreateDate = role.CreatedTime != default ? role.CreatedTime : null, + UpdateDate = role.UpdatedTime != default ? role.UpdatedTime : null }; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/RoleAgentDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/RoleAgentDocument.cs new file mode 100644 index 00000000..037158d0 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/RoleAgentDocument.cs @@ -0,0 +1,25 @@ +using BotSharp.Abstraction.Roles.Models; + +namespace BotSharp.Plugin.MongoStorage.Collections; + +public class RoleAgentDocument : MongoBase +{ + public string RoleId { get; set; } + public string AgentId { get; set; } + public IEnumerable Actions { get; set; } = []; + public DateTime CreatedTime { get; set; } + public DateTime UpdatedTime { get; set; } + + public RoleAgent ToRoleAgent() + { + return new RoleAgent + { + Id = Id, + RoleId = RoleId, + AgentId = AgentId, + Actions = Actions, + CreatedTime = CreatedTime, + UpdatedTime = UpdatedTime + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/RoleDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/RoleDocument.cs new file mode 100644 index 00000000..557f219a --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/RoleDocument.cs @@ -0,0 +1,24 @@ +using BotSharp.Abstraction.Roles.Models; + +namespace BotSharp.Plugin.MongoStorage.Collections; + +public class RoleDocument : MongoBase +{ + public string Name { get; set; } + public IEnumerable Permissions { get; set; } = []; + public DateTime CreatedTime { get; set; } + public DateTime UpdatedTime { get; set; } + + + public Role ToRole() + { + return new Role + { + Id = Id, + Name = Name, + Permissions = Permissions, + CreatedTime = CreatedTime, + UpdatedTime = UpdatedTime + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs index af7c8b8f..b91c35fe 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs @@ -159,4 +159,10 @@ public class MongoDbContext public IMongoCollection KnowledgeCollectionFileMeta => Database.GetCollection($"{_collectionPrefix}_KnowledgeCollectionFileMeta"); + + public IMongoCollection Roles + => Database.GetCollection($"{_collectionPrefix}_Roles"); + + public IMongoCollection RoleAgents + => Database.GetCollection($"{_collectionPrefix}_RoleAgents"); } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs index b0146903..3e6dc000 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -2,6 +2,7 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Routing.Models; +using MongoDB.Driver; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -332,8 +333,6 @@ public partial class MongoRepository if (found.IsNullOrEmpty()) return []; - var agentIds = found.Select(x => x.AgentId).Distinct().ToList(); - var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); var res = found.Select(x => new UserAgent { Id = x.Id, @@ -344,6 +343,8 @@ public partial class MongoRepository UpdatedTime = x.UpdatedTime }).ToList(); + var agentIds = found.Select(x => x.AgentId).Distinct().ToList(); + var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); foreach (var item in res) { var agent = agents.FirstOrDefault(x => x.Id == item.AgentId); @@ -455,6 +456,7 @@ public partial class MongoRepository try { _dc.UserAgents.DeleteMany(Builders.Filter.Empty); + _dc.RoleAgents.DeleteMany(Builders.Filter.Empty); _dc.Agents.DeleteMany(Builders.Filter.Empty); return true; } @@ -472,10 +474,12 @@ public partial class MongoRepository var agentFilter = Builders.Filter.Eq(x => x.Id, agentId); var userAgentFilter = Builders.Filter.Eq(x => x.AgentId, agentId); + var roleAgentFilter = Builders.Filter.Eq(x => x.AgentId, agentId); var agentTaskFilter = Builders.Filter.Eq(x => x.AgentId, agentId); _dc.Agents.DeleteOne(agentFilter); _dc.UserAgents.DeleteMany(userAgentFilter); + _dc.RoleAgents.DeleteMany(roleAgentFilter); _dc.AgentTasks.DeleteMany(agentTaskFilter); return true; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Role.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Role.cs new file mode 100644 index 00000000..ad3cf8db --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Role.cs @@ -0,0 +1,152 @@ +using BotSharp.Abstraction.Repositories.Filters; +using BotSharp.Abstraction.Roles.Models; + +namespace BotSharp.Plugin.MongoStorage.Repository; + +public partial class MongoRepository +{ + public bool RefreshRoles(IEnumerable roles) + { + if (roles.IsNullOrEmpty()) return false; + + var validRoles = roles.Where(x => !string.IsNullOrWhiteSpace(x.Id) + && !string.IsNullOrWhiteSpace(x.Name)).ToList(); + if (validRoles.IsNullOrEmpty()) return false; + + + // Clear data + _dc.RoleAgents.DeleteMany(Builders.Filter.Empty); + _dc.Roles.DeleteMany(Builders.Filter.Empty); + + var roleDocs = validRoles.Select(x => new RoleDocument + { + Id = x.Id, + Name = x.Name, + Permissions = x.Permissions, + CreatedTime = DateTime.UtcNow, + UpdatedTime = DateTime.UtcNow + }); + _dc.Roles.InsertMany(roleDocs); + + return true; + } + + + public IEnumerable GetRoles(RoleFilter filter) + { + if (filter == null) + { + filter = RoleFilter.Empty(); + } + + var roleBuilder = Builders.Filter; + var roleFilters = new List>() { roleBuilder.Empty }; + + // Apply filters + if (!filter.Names.IsNullOrEmpty()) + { + roleFilters.Add(roleBuilder.In(x => x.Name, filter.Names)); + } + + // Search + var roleDocs = _dc.Roles.Find(roleBuilder.And(roleFilters)).ToList(); + var roles = roleDocs.Select(x => x.ToRole()).ToList(); + + return roles; + } + + public Role? GetRoleDetails(string roleId, bool includeAgent = false) + { + if (string.IsNullOrWhiteSpace(roleId)) return null; + + var roleDoc = _dc.Roles.Find(Builders.Filter.Eq(x => x.Id, roleId)).FirstOrDefault(); + if (roleDoc == null) return null; + + var agentActions = new List(); + var role = roleDoc.ToRole(); + var roleAgentDocs = _dc.RoleAgents.Find(Builders.Filter.Eq(x => x.RoleId, roleId)).ToList(); + + if (!includeAgent) + { + agentActions = roleAgentDocs.Select(x => new RoleAgentAction + { + Id = x.Id, + AgentId = x.AgentId, + Actions = x.Actions + }).ToList(); + role.AgentActions = agentActions; + return role; + } + + var agentIds = roleAgentDocs.Select(x => x.AgentId).Distinct().ToList(); + if (!agentIds.IsNullOrEmpty()) + { + var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); + + foreach (var item in roleAgentDocs) + { + var found = agents.FirstOrDefault(x => x.Id == item.AgentId); + if (found == null) continue; + + agentActions.Add(new RoleAgentAction + { + Id = item.Id, + AgentId = found.Id, + Agent = found, + Actions = item.Actions + }); + } + } + + role.AgentActions = agentActions; + return role; + } + + public bool UpdateRole(Role role, bool updateRoleAgents = false) + { + if (string.IsNullOrEmpty(role?.Id)) return false; + + var roleFilter = Builders.Filter.Eq(x => x.Id, role.Id); + var roleUpdate = Builders.Update + .Set(x => x.Name, role.Name) + .Set(x => x.Permissions, role.Permissions) + .Set(x => x.CreatedTime, DateTime.UtcNow) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + + _dc.Roles.UpdateOne(roleFilter, roleUpdate, _options); + + if (updateRoleAgents) + { + var roleAgentDocs = role.AgentActions?.Select(x => new RoleAgentDocument + { + Id = !string.IsNullOrEmpty(x.Id) ? x.Id : Guid.NewGuid().ToString(), + RoleId = role.Id, + AgentId = x.AgentId, + Actions = x.Actions, + CreatedTime = DateTime.UtcNow, + UpdatedTime = DateTime.UtcNow + })?.ToList() ?? []; + + var toDelete = _dc.RoleAgents.Find(Builders.Filter.And( + Builders.Filter.Eq(x => x.RoleId, role.Id), + Builders.Filter.Nin(x => x.Id, roleAgentDocs.Select(x => x.Id)) + )).ToList(); + + _dc.RoleAgents.DeleteMany(Builders.Filter.In(x => x.Id, toDelete.Select(x => x.Id))); + foreach (var doc in roleAgentDocs) + { + var roleAgentFilter = Builders.Filter.Eq(x => x.Id, doc.Id); + var roleAgentUpdate = Builders.Update + .Set(x => x.Id, doc.Id) + .Set(x => x.RoleId, role.Id) + .Set(x => x.AgentId, doc.AgentId) + .Set(x => x.Actions, doc.Actions) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + + _dc.RoleAgents.UpdateOne(roleAgentFilter, roleAgentUpdate, _options); + } + } + + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs index d7115c36..374d2ab0 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs @@ -200,6 +200,10 @@ public partial class MongoRepository { userFilters.Add(userBuilder.In(x => x.Role, filter.Roles)); } + if (!filter.Types.IsNullOrEmpty()) + { + userFilters.Add(userBuilder.In(x => x.Type, filter.Types)); + } if (!filter.Sources.IsNullOrEmpty()) { userFilters.Add(userBuilder.In(x => x.Source, filter.Sources)); @@ -221,15 +225,15 @@ public partial class MongoRepository }; } - public User? GetUserDetails(string userId) + public User? GetUserDetails(string userId, bool includeAgent = false) { if (string.IsNullOrWhiteSpace(userId)) return null; - var userDoc = _dc.Users.Find(Builders.Filter.Eq(x => x.Id, userId)).FirstOrDefault(); + var userDoc = _dc.Users.AsQueryable().FirstOrDefault(x => x.Id == userId || x.ExternalId == userId); if (userDoc == null) return null; + var agentActions = new List(); var user = userDoc.ToUser(); - var userAgents = _dc.UserAgents.AsQueryable().Where(x => x.UserId == userId).Select(x => new UserAgent { Id = x.Id, @@ -238,9 +242,19 @@ public partial class MongoRepository Actions = x.Actions ?? Enumerable.Empty() }).ToList(); - var agentActions = new List(); + if (!includeAgent) + { + agentActions = userAgents.Select(x => new UserAgentAction + { + Id = x.Id, + AgentId = x.AgentId, + Actions = x.Actions + }).ToList(); + user.AgentActions = agentActions; + return user; + } + var agentIds = userAgents.Select(x => x.AgentId)?.Distinct().ToList(); - if (!agentIds.IsNullOrEmpty()) { var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); @@ -264,7 +278,7 @@ public partial class MongoRepository return user; } - public bool UpdateUser(User user, bool isUpdateUserAgents = false) + public bool UpdateUser(User user, bool updateUserAgents = false) { if (string.IsNullOrEmpty(user?.Id)) return false; @@ -277,7 +291,7 @@ public partial class MongoRepository _dc.Users.UpdateOne(userFilter, userUpdate); - if (isUpdateUserAgents) + if (updateUserAgents) { var userAgentDocs = user.AgentActions?.Select(x => new UserAgentDocument {