From c3ead63acbde458e584cf16f4d659ac5bb048239 Mon Sep 17 00:00:00 2001 From: Haiping Chen <101423@smsassist.com> Date: Wed, 31 Jan 2024 13:57:54 -0600 Subject: [PATCH] Improve UserIdentity implementation. --- .../Repositories/IBotSharpRepository.cs | 1 + .../Users/IAuthenticationHook.cs | 2 + .../Users/IUserIdentity.cs | 1 + .../Repository/BotSharpDbContext.cs | 21 ++-- .../FileRepository/FileRepository.User.cs | 44 ++++---- .../FileRepository/FileRepository.cs | 5 - .../Users/Services/UserIdentity.cs | 3 + .../Users/Services/UserService.cs | 106 +++++++++++++----- .../Controllers/AgentController.cs | 2 +- .../Controllers/ConversationController.cs | 33 ++++-- .../ViewModels/Users/UserViewModel.cs | 6 +- .../Collections/UserDocument.cs | 19 ++++ .../Repository/MongoRepository.User.cs | 41 ++----- 13 files changed, 175 insertions(+), 109 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 5ca241e6..bf69bec3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -18,6 +18,7 @@ public interface IBotSharpRepository #region User User? GetUserByEmail(string email); User? GetUserById(string id); + User? GetUserByUserName(string userName); void CreateUser(User user); #endregion diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs index 67a50d37..267a997c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs @@ -1,8 +1,10 @@ using BotSharp.Abstraction.Users.Models; +using System.Security.Claims; namespace BotSharp.Abstraction.Users; public interface IAuthenticationHook { Task Authenticate(string id, string password); + void AddClaims(List claims); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs index 2f7a56e9..a0998117 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs @@ -7,4 +7,5 @@ public interface IUserIdentity string UserName { get; } string FirstName { get; } string LastName { get; } + string FullName { get; } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index 8adf711a..fef3c6dc 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -192,20 +192,17 @@ public class BotSharpDbContext : Database, IBotSharpRepository #endregion #region User - public User? GetUserByEmail(string email) - { - throw new NotImplementedException(); - } + public User? GetUserByEmail(string email) + => throw new NotImplementedException(); - public User? GetUserById(string id) - { - throw new NotImplementedException(); - } + public User? GetUserById(string id) + => throw new NotImplementedException(); - public void CreateUser(User user) - { - throw new NotImplementedException(); - } + public User? GetUserByUserName(string userName) + => throw new NotImplementedException(); + + public void CreateUser(User user) + => throw new NotImplementedException(); #endregion diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs index 64c46ad9..bc992a08 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs @@ -1,31 +1,35 @@ using BotSharp.Abstraction.Users.Models; using System.IO; -namespace BotSharp.Core.Repository +namespace BotSharp.Core.Repository; + +public partial class FileRepository { - public partial class FileRepository + public User? GetUserByEmail(string email) { - public User? GetUserByEmail(string email) - { - return Users.FirstOrDefault(x => x.Email == email); - } + return Users.FirstOrDefault(x => x.Email == email.ToLower()); + } - public User? GetUserById(string id = null) - { - return Users.FirstOrDefault(x => x.ExternalId == id || x.Id == id); - } + public User? GetUserById(string id = null) + { + return Users.FirstOrDefault(x => x.Id == id || (x.ExternalId != null && x.ExternalId == id)); + } - public void CreateUser(User user) + public User? GetUserByUserName(string userName = null) + { + return Users.FirstOrDefault(x => x.UserName == userName.ToLower()); + } + + public void CreateUser(User user) + { + var userId = Guid.NewGuid().ToString(); + user.Id = userId; + var dir = Path.Combine(_dbSettings.FileRepository, "users", userId); + if (!Directory.Exists(dir)) { - var userId = Guid.NewGuid().ToString(); - user.Id = userId; - var dir = Path.Combine(_dbSettings.FileRepository, "users", userId); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - var path = Path.Combine(dir, "user.json"); - File.WriteAllText(path, JsonSerializer.Serialize(user, _options)); + Directory.CreateDirectory(dir); } + var path = Path.Combine(dir, "user.json"); + File.WriteAllText(path, JsonSerializer.Serialize(user, _options)); } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs index cca7d2e7..b5d4683b 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs @@ -4,11 +4,6 @@ using FunctionDef = BotSharp.Abstraction.Functions.Models.FunctionDef; using BotSharp.Abstraction.Users.Models; using BotSharp.Abstraction.Agents.Models; using MongoDB.Driver; -using BotSharp.Abstraction.Routing.Models; -using BotSharp.Abstraction.Repositories.Filters; -using BotSharp.Abstraction.Repositories.Models; -using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Abstraction.Evaluations.Settings; using System.Text.Encodings.Web; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Statistics.Settings; diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs index 167d5cfe..b9b8b932 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs @@ -28,4 +28,7 @@ public class UserIdentity : IUserIdentity public string LastName => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value!; + + public string FullName + => $"{FirstName} {LastName}".Trim(); } diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index 85ab30c9..d0060671 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -12,34 +12,43 @@ public class UserService : IUserService { private readonly IServiceProvider _services; private readonly IUserIdentity _user; + private readonly ILogger _logger; - public UserService(IServiceProvider services, IUserIdentity user) + public UserService(IServiceProvider services, IUserIdentity user, ILogger logger) { _services = services; _user = user; + _logger = logger; } public async Task CreateUser(User user) { + if (string.IsNullOrEmpty(user.UserName)) + { + // generate unique name + var name = user.Email.Split("@").First() + "-" + Nanoid.Generate("0123456789botsharp", 6); + user.UserName = name; + } + else + { + user.UserName = user.UserName.ToLower(); + } + var db = _services.GetRequiredService(); - var record = db.GetUserByEmail(user.Email); + var record = db.GetUserByUserName(user.UserName); + if (record != null) { return record; } + if (string.IsNullOrEmpty(user.Id)) + { + user.Id = Guid.NewGuid().ToString(); + } + record = user; - record.Email = user.Email.ToLower(); - if (string.IsNullOrEmpty(user.UserName)) - { - var name = record.Email.Split("@").First() + "-" + Nanoid.Generate("123456789botsharp", 6); - record.UserName = name; - } - else - { - record.UserName = user.UserName.ToLower(); - } record.Salt = Guid.NewGuid().ToString("N"); record.Password = Utilities.HashText(user.Password, record.Salt); @@ -53,18 +62,34 @@ public class UserService : IUserService public async Task GetToken(string authorization) { var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); - var (userEmail, password) = base64.SplitAsTuple(":"); + var (id, password) = base64.SplitAsTuple(":"); var db = _services.GetRequiredService(); - var record = db.GetUserByEmail(userEmail); + var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id); if (record == null) + { + record = db.GetUserByUserName(id); + } + + if (record == null || record.Source != "internal") { // check 3rd party user var validators = _services.GetServices(); foreach (var validator in validators) { - var user = await validator.Authenticate(userEmail, password); - if (user != null) + var user = await validator.Authenticate(id, password); + if (user == null) + { + continue; + } + + if (string.IsNullOrEmpty(user.Source) || user.Source == "internal") + { + _logger.LogError($"Please set source name in the Authenticate hook."); + return null; + } + + if (record == null) { // create a local user record record = new User @@ -74,11 +99,12 @@ public class UserService : IUserService FirstName = user.FirstName, LastName = user.LastName, Source = user.Source, - ExternalId = user.ExternalId + ExternalId = user.ExternalId, + Password = user.Password, }; await CreateUser(record); - break; } + break; } } @@ -105,26 +131,36 @@ public class UserService : IUserService private string GenerateJwtToken(User user) { + var claims = new List + { + new Claim(JwtRegisteredClaimNames.NameId, user.Id), + new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName), + new Claim(JwtRegisteredClaimNames.Email, user.Email), + new Claim(JwtRegisteredClaimNames.GivenName, user.FirstName), + new Claim(JwtRegisteredClaimNames.FamilyName, user.LastName), + new Claim("source", user.Source), + new Claim("external_id", user.ExternalId), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var validators = _services.GetServices(); + foreach (var validator in validators) + { + validator.AddClaims(claims); + } + var config = _services.GetRequiredService(); var issuer = config["Jwt:Issuer"]; var audience = config["Jwt:Audience"]; var key = Encoding.ASCII.GetBytes(config["Jwt:Key"]); var tokenDescriptor = new SecurityTokenDescriptor { - Subject = new ClaimsIdentity(new[] - { - new Claim(JwtRegisteredClaimNames.NameId, user.Id), - new Claim(JwtRegisteredClaimNames.Email, user.Email), - new Claim(JwtRegisteredClaimNames.GivenName, user.FirstName), - new Claim(JwtRegisteredClaimNames.FamilyName, user.LastName), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) - }), - Expires = DateTime.UtcNow.AddMinutes(5), + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddHours(2), Issuer = issuer, Audience = audience, - SigningCredentials = new SigningCredentials - (new SymmetricSecurityKey(key), - SecurityAlgorithms.HmacSha512Signature) + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), + SecurityAlgorithms.HmacSha512Signature) }; var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken(tokenDescriptor); @@ -144,6 +180,16 @@ public class UserService : IUserService { var db = _services.GetRequiredService(); var user = db.GetUserById(id); + if (user == null) + { + user = new User + { + Id = id, + FirstName = "Unknown", + LastName = "Anonymous", + Role = AgentRole.User + }; + } return user; } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs index 35873b9e..1a9389ef 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs @@ -29,7 +29,7 @@ public class AgentController : ControllerBase { AgentIds = new List { id } }); - return agents.Items.First(); + return agents.Items.FirstOrDefault(); } [HttpGet("/agents")] diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index ae65fde9..97bd5158 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Users.Models; + namespace BotSharp.OpenAPI.Controllers; [Authorize] @@ -70,16 +72,29 @@ public class ConversationController : ControllerBase var dialogs = new List(); foreach (var message in history) { - var user = await userService.GetUser(message.SenderId); - - dialogs.Add(new ChatResponseModel + if (message.Role == AgentRole.User) { - ConversationId = conversationId, - MessageId = message.MessageId, - CreatedAt = message.CreatedAt, - Text = message.Content, - Sender = UserViewModel.FromUser(user) - }); + var user = await userService.GetUser(message.SenderId); + + dialogs.Add(new ChatResponseModel + { + ConversationId = conversationId, + MessageId = message.MessageId, + CreatedAt = message.CreatedAt, + Text = message.Content, + Sender = UserViewModel.FromUser(user) + }); + } + else + { + dialogs.Add(new ChatResponseModel + { + ConversationId = conversationId, + MessageId = message.MessageId, + CreatedAt = message.CreatedAt, + Text = message.Content + }); + } } return dialogs; diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs index 1a4706a3..2d7d2922 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs @@ -29,9 +29,9 @@ public class UserViewModel { return new UserViewModel { - FirstName = "AI", - LastName = "Assistant", - Role = AgentRole.Assistant + FirstName = "Unknown", + LastName = "Anonymous", + Role = AgentRole.User }; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs index ae06a708..c21a6e08 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Users.Models; + namespace BotSharp.Plugin.MongoStorage.Collections; public class UserDocument : MongoBase @@ -14,4 +16,21 @@ public class UserDocument : MongoBase public DateTime CreatedTime { get; set; } public DateTime UpdatedTime { get; set; } + + public User ToUser() + { + return new User + { + Id = Id, + UserName = UserName, + FirstName = FirstName, + LastName = LastName, + Email = Email, + Password = Password, + Salt = Salt, + Source = Source, + ExternalId = ExternalId, + Role = Role + }; + } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs index 44faeb42..1b6b8b77 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs @@ -7,38 +7,21 @@ public partial class MongoRepository { public User? GetUserByEmail(string email) { - var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Email == email); - return user != null ? new User - { - Id = user.Id, - UserName = user.UserName, - FirstName = user.FirstName, - LastName = user.LastName, - Email = user.Email, - Password = user.Password, - Salt = user.Salt, - Source = user.Source, - ExternalId = user.ExternalId, - Role = user.Role - } : null; + var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Email == email.ToLower()); + return user != null ? user.ToUser() : null; } public User? GetUserById(string id) { - var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Id == id || x.ExternalId == id); - return user != null ? new User - { - Id = user.Id, - UserName = user.UserName, - FirstName = user.FirstName, - LastName = user.LastName, - Email = user.Email, - Password = user.Password, - Salt = user.Salt, - Source = user.Source, - ExternalId = user.ExternalId, - Role = user.Role - } : null; + var user = _dc.Users.AsQueryable() + .FirstOrDefault(x => x.Id == id || (x.ExternalId != null && x.ExternalId == id)); + return user != null ? user.ToUser() : null; + } + + public User? GetUserByUserName(string userName) + { + var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.UserName == userName.ToLower()); + return user != null ? user.ToUser() : null; } public void CreateUser(User user) @@ -47,7 +30,7 @@ public partial class MongoRepository var userCollection = new UserDocument { - Id = Guid.NewGuid().ToString(), + Id = user.Id ?? Guid.NewGuid().ToString(), UserName = user.UserName, FirstName = user.FirstName, LastName = user.LastName,