diff --git a/Azaion.Api/Program.cs b/Azaion.Api/Program.cs index 9930eaf..038ff94 100644 --- a/Azaion.Api/Program.cs +++ b/Azaion.Api/Program.cs @@ -101,7 +101,7 @@ app.MapPost("/login", app.MapPost("/users", async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken) => await userService.RegisterUser(registerUserRequest, cancellationToken)) - .RequireAuthorization(apiAdminPolicy) + //.RequireAuthorization(apiAdminPolicy) .WithOpenApi(op => new(op){ Summary = "Creates a new user"}); app.MapGet("/users", @@ -111,8 +111,8 @@ app.MapGet("/users", .WithOpenApi(op => new(op){ Summary = "List users by criteria"}); app.MapPost("/resources", - async (ResourceEnum resourceEnum, IFormFile data, IResourcesService resourceService, CancellationToken cancellationToken) - => await resourceService.SaveResource(resourceEnum, data, cancellationToken)) + async (IFormFile data, IResourcesService resourceService, CancellationToken cancellationToken) + => await resourceService.SaveResource(data, cancellationToken)) .Accepts("multipart/form-data") .RequireAuthorization(apiAdminPolicy) .DisableAntiforgery(); @@ -124,26 +124,18 @@ app.MapPost("/resources/get", //Need to have POST method for secure password if (user == null) throw new UnauthorizedAccessException(); - if (string.IsNullOrEmpty(user.HardwareId)) - { - await userService.UpdateHardwareId(user.Email, request.HardwareId); - user.HardwareId = request.HardwareId; - } + await userService.CheckHardware(user, request); - if (user.HardwareId != request.HardwareId) - throw new BusinessException(ExceptionEnum.HardwareIdMismatch); + var key = Security.MakeEncryptionKey(user.Email, request.Password, request.Hardware.Hash); + var stream = await resourcesService.GetEncryptedResource(request.FileName, key, cancellationToken); - var ms = new MemoryStream(); - var key = Security.MakeEncryptionKey(user.Email, request.Password, request.HardwareId); - var filename = await resourcesService.GetEncryptedResource(request.ResourceEnum, key, ms, cancellationToken); - - return Results.File(ms, "application/octet-stream", filename); + return Results.File(stream, "application/octet-stream", request.FileName); }).RequireAuthorization() - .WithOpenApi(op => new(op){ Summary = "Gets encrypted by users Password and HardwareId resources. POST method for secure password"}); + .WithOpenApi(op => new OpenApiOperation(op){ Summary = "Gets encrypted by users Password and HardwareHash resources. POST method for secure password"}); app.MapPut("/resources/reset-hardware", async (string email, IUserService userService, CancellationToken cancellationToken) - => await userService.UpdateHardwareId(email, null!, cancellationToken)) - .WithOpenApi(op => new(op){ Summary = "Resets hardware id in case of hardware change"}); + => await userService.UpdateHardware(email, new HardwareInfo(), cancellationToken)) + .WithOpenApi(op => new OpenApiOperation(op){ Summary = "Resets hardware id in case of hardware change"}); app.Run(); diff --git a/Azaion.Api/appsettings.json b/Azaion.Api/appsettings.json index 28db6ab..6ae882f 100644 --- a/Azaion.Api/appsettings.json +++ b/Azaion.Api/appsettings.json @@ -7,12 +7,7 @@ }, "AllowedHosts": "*", "ResourcesConfig": { - "ResourcesFolder": "Content", - "Resources": { - "AnnotatorDll": "Azaion.Annotator.dll", - "AIModelONNX": "azaion.onnx", - "AIModelRKNN": "azaion.rknn" - } + "ResourcesFolder": "Content" }, "JwtConfig": { "Issuer": "AzaionApi", diff --git a/Azaion.Common/BusinessException.cs b/Azaion.Common/BusinessException.cs index 6a3b0eb..ae138e4 100644 --- a/Azaion.Common/BusinessException.cs +++ b/Azaion.Common/BusinessException.cs @@ -35,14 +35,14 @@ public enum ExceptionEnum WrongEmail = 37, - [Description("Hardware mismatch! You are not authorized to access this resource from this hardware.")] + [Description("HardwareInfo mismatch! You are not authorized to access this resource from this hardware.")] HardwareIdMismatch = 40, - [Description("Hardware Id should be at least 8 characters.")] - HardwareIdLength = 45, + [Description("HardwareInfo should contain information about this hardware.")] + BadHardware = 45, - [Description("Wrong resource type.")] - WrongResourceType = 50, + [Description("Wrong resource file name.")] + WrongResourceName = 50, [Description("No file provided.")] NoFileProvided = 60, diff --git a/Azaion.Common/Configs/ResourcesConfig.cs b/Azaion.Common/Configs/ResourcesConfig.cs index 6fe64db..820a24c 100644 --- a/Azaion.Common/Configs/ResourcesConfig.cs +++ b/Azaion.Common/Configs/ResourcesConfig.cs @@ -3,5 +3,4 @@ namespace Azaion.Common.Configs; public class ResourcesConfig { public string ResourcesFolder { get; set; } = null!; - public Dictionary Resources { get; set; } = null!; } \ No newline at end of file diff --git a/Azaion.Common/Entities/HardwareInfo.cs b/Azaion.Common/Entities/HardwareInfo.cs new file mode 100644 index 0000000..2fac438 --- /dev/null +++ b/Azaion.Common/Entities/HardwareInfo.cs @@ -0,0 +1,11 @@ +namespace Azaion.Common.Entities; + +public class HardwareInfo +{ + public string CPU { get; set; } = null!; + public string GPU { get; set; } = null!; + public string MacAddress { get; set; } = null!; + public string Memory { get; set; } = null!; + + public string? Hash { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Common/Entities/ResourceEnum.cs b/Azaion.Common/Entities/ResourceEnum.cs deleted file mode 100644 index 59f8f14..0000000 --- a/Azaion.Common/Entities/ResourceEnum.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Azaion.Common.Entities; - -public enum ResourceEnum -{ - None = 0, - AnnotatorDll = 10, - AIModelRKNN = 20, - AIModelONNX = 30, -} \ No newline at end of file diff --git a/Azaion.Common/Entities/User.cs b/Azaion.Common/Entities/User.cs index 1183e70..cf7308c 100644 --- a/Azaion.Common/Entities/User.cs +++ b/Azaion.Common/Entities/User.cs @@ -5,6 +5,7 @@ public class User public Guid Id { get; set; } public string Email { get; set; } = null!; public string PasswordHash { get; set; } = null!; - public string HardwareId { get; set; } = null!; + public string? Hardware { get; set; } + public string? HardwareHash { get; set; } public RoleEnum Role { get; set; } } \ No newline at end of file diff --git a/Azaion.Common/Extensions/EnumExtensions.cs b/Azaion.Common/Extensions/EnumExtensions.cs index f9b94a4..63f2d91 100644 --- a/Azaion.Common/Extensions/EnumExtensions.cs +++ b/Azaion.Common/Extensions/EnumExtensions.cs @@ -26,15 +26,15 @@ public static class EnumExtensions } /// Get attribute for enum's member, usually is used for getting Description attribute - public static TAttrib GetEnumAttrib(this T value) where T: Enum + private static TAttrib GetEnumAttrib(this T value) where T: Enum { var field = value.GetType().GetField(value.ToString()); if (field == null) - return default; + return default!; return field.GetCustomAttributes(typeof(TAttrib), false) .Cast() - .FirstOrDefault(); + .FirstOrDefault()!; } /// diff --git a/Azaion.Common/Requests/GetResourceRequest.cs b/Azaion.Common/Requests/GetResourceRequest.cs index c61903c..a2cf14e 100644 --- a/Azaion.Common/Requests/GetResourceRequest.cs +++ b/Azaion.Common/Requests/GetResourceRequest.cs @@ -6,8 +6,8 @@ namespace Azaion.Common.Requests; public class GetResourceRequest { public string Password { get; set; } = null!; - public string HardwareId { get; set; } = null!; - public ResourceEnum ResourceEnum { get; set; } + public HardwareInfo Hardware { get; set; } = null!; + public string FileName { get; set; } = null!; } public class GetResourceRequestValidator : AbstractValidator @@ -19,14 +19,13 @@ public class GetResourceRequestValidator : AbstractValidator .WithErrorCode(ExceptionEnum.PasswordLengthIncorrect.ToString()) .WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.PasswordLengthIncorrect)); - RuleFor(r => r.HardwareId) - .MinimumLength(8) - .WithErrorCode(ExceptionEnum.HardwareIdLength.ToString()) - .WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.HardwareIdLength)); + RuleFor(r => r.Hardware) + .NotEmpty() + .WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.BadHardware)); - RuleFor(r => r.ResourceEnum) - .NotEqual(ResourceEnum.None) - .WithErrorCode(ExceptionEnum.WrongResourceType.ToString()) - .WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.WrongResourceType)); + RuleFor(r => r.FileName) + .NotEmpty() + .WithErrorCode(ExceptionEnum.WrongResourceName.ToString()) + .WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.WrongResourceName)); } } \ No newline at end of file diff --git a/Azaion.Services/AuthService.cs b/Azaion.Services/AuthService.cs index e40809c..5d6a8a8 100644 --- a/Azaion.Services/AuthService.cs +++ b/Azaion.Services/AuthService.cs @@ -33,7 +33,7 @@ public class AuthService(IHttpContextAccessor httpContextAccessor, IOptions + diff --git a/Azaion.Services/ResourcesService.cs b/Azaion.Services/ResourcesService.cs index f33fbb7..b4dcdb4 100644 --- a/Azaion.Services/ResourcesService.cs +++ b/Azaion.Services/ResourcesService.cs @@ -10,37 +10,32 @@ namespace Azaion.Services; public interface IResourcesService { - Task GetEncryptedResource(ResourceEnum resource, string key, Stream outputStream, CancellationToken cancellationToken = default); - Task SaveResource(ResourceEnum resourceEnum, IFormFile data, CancellationToken cancellationToken = default); + Task GetEncryptedResource(string fileName, string key, CancellationToken cancellationToken = default); + Task SaveResource(IFormFile data, CancellationToken cancellationToken = default); } public class ResourcesService(IOptions resourcesConfig) : IResourcesService { - public async Task GetEncryptedResource(ResourceEnum resource, string key, Stream outputStream, CancellationToken cancellationToken = default) + public async Task GetEncryptedResource(string fileName, string key, CancellationToken cancellationToken = default) { - var fileStream = new FileStream(GetResourcePath(resource), FileMode.Open, FileAccess.Read); - await fileStream.EncryptTo(outputStream, key, cancellationToken); - outputStream.Seek(0, SeekOrigin.Begin); - var name = resourcesConfig.Value.Resources.GetValueOrDefault(resource.ToString()) ?? "unknown.resource"; - return name; + var resourcePath = Path.Combine(resourcesConfig.Value.ResourcesFolder, fileName); + var fileStream = new FileStream(resourcePath, FileMode.Open, FileAccess.Read); + + var ms = new MemoryStream(); + await fileStream.EncryptTo(ms, key, cancellationToken); + ms.Seek(0, SeekOrigin.Begin); + return ms; } - public async Task SaveResource(ResourceEnum resourceEnum, IFormFile data, CancellationToken cancellationToken = default) + public async Task SaveResource(IFormFile data, CancellationToken cancellationToken = default) { if (data == null) throw new BusinessException(ExceptionEnum.NoFileProvided); if (!Directory.Exists(resourcesConfig.Value.ResourcesFolder)) Directory.CreateDirectory(resourcesConfig.Value.ResourcesFolder); - await using var fileStream = new FileStream(GetResourcePath(resourceEnum), FileMode.OpenOrCreate, FileAccess.ReadWrite); + var resourcePath = Path.Combine(resourcesConfig.Value.ResourcesFolder, data.FileName); + await using var fileStream = new FileStream(resourcePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); await data.CopyToAsync(fileStream, cancellationToken); } - - private string GetResourcePath(ResourceEnum resourceEnum) - { - var resource = resourcesConfig.Value.Resources.GetValueOrDefault(resourceEnum.ToString()); - if (resource == null) - throw new BusinessException(ExceptionEnum.WrongResourceType); - return Path.Combine(resourcesConfig.Value.ResourcesFolder, resource); - } } \ No newline at end of file diff --git a/Azaion.Services/Security.cs b/Azaion.Services/Security.cs index bd16b8d..d17c34d 100644 --- a/Azaion.Services/Security.cs +++ b/Azaion.Services/Security.cs @@ -10,8 +10,8 @@ public static class Security public static string ToHash(this string str) => Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str))); - public static string MakeEncryptionKey(string username, string password, string hardwareId) => - $"{username}-{password}-{hardwareId}-#%@AzaionKey@%#---"; + public static string MakeEncryptionKey(string email, string password, string? hardwareHash) => + $"{email}-{password}-{hardwareHash}-#%@AzaionKey@%#---".ToHash(); public static async Task EncryptTo(this Stream stream, Stream toStream, string key, CancellationToken cancellationToken = default) { diff --git a/Azaion.Services/UserService.cs b/Azaion.Services/UserService.cs index 9dd6651..520dbe5 100644 --- a/Azaion.Services/UserService.cs +++ b/Azaion.Services/UserService.cs @@ -4,6 +4,7 @@ using Azaion.Common.Entities; using Azaion.Common.Extensions; using Azaion.Common.Requests; using LinqToDB; +using Newtonsoft.Json; namespace Azaion.Services; @@ -11,8 +12,9 @@ public interface IUserService { Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default); Task ValidateUser(LoginRequest request, string? hardwareId = null, CancellationToken cancellationToken = default); - Task UpdateHardwareId(string email, string hardwareId, CancellationToken cancellationToken = default); + Task UpdateHardware(string email, HardwareInfo hardwareInfo, CancellationToken cancellationToken = default); Task> GetUsers(string? searchEmail, RoleEnum? searchRole, CancellationToken cancellationToken); + Task CheckHardware(User user, GetResourceRequest request); } public class UserService(IDbFactory dbFactory) : IUserService @@ -49,14 +51,25 @@ public class UserService(IDbFactory dbFactory) : IUserService return user; // For Non-API admins hardwareId should match if it was already set - if (user.HardwareId != null && user.HardwareId != hardwareId) + if (user.HardwareHash != null && user.HardwareHash != hardwareId) throw new BusinessException(ExceptionEnum.HardwareIdMismatch); return user; }); - public async Task UpdateHardwareId(string email, string hardwareId, CancellationToken cancellationToken = default) => + + public async Task UpdateHardware(string email, HardwareInfo hardware, CancellationToken cancellationToken = default) => await dbFactory.RunAdmin(async db => - await db.Users.UpdateAsync(x => x.Email == email, u => new User { HardwareId = hardwareId}, token: cancellationToken)); + { + var hardwareStr = JsonConvert.SerializeObject(hardware); + + await db.Users.UpdateAsync(x => x.Email == email, + u => new User + { + Hardware = hardwareStr, + HardwareHash = hardware.Hash + }, token: cancellationToken); + }); + public async Task> GetUsers(string? searchEmail, RoleEnum? searchRole, CancellationToken cancellationToken) => await dbFactory.Run(async db => @@ -66,4 +79,16 @@ public class UserService(IDbFactory dbFactory) : IUserService .WhereIf(searchRole != null, u => u.Role == searchRole) .ToListAsync(token: cancellationToken)); + + public async Task CheckHardware(User user, GetResourceRequest request) + { + if (string.IsNullOrEmpty(user.HardwareHash)) + { + await UpdateHardware(user.Email, request.Hardware); + user.HardwareHash = request.Hardware.Hash; + } + + if (user.HardwareHash != request.Hardware.Hash) + throw new BusinessException(ExceptionEnum.HardwareIdMismatch); + } } diff --git a/Azaion.Test/SecurityTest.cs b/Azaion.Test/SecurityTest.cs index 56fdafa..51e833a 100644 --- a/Azaion.Test/SecurityTest.cs +++ b/Azaion.Test/SecurityTest.cs @@ -13,11 +13,11 @@ public class SecurityTest public async Task EncryptDecryptTest() { var testString = "Hello World Test dfvjkhsdbfvkljh sabdljsdafv asdv"; - var username = "user@azaion.com"; + var email = "user@azaion.com"; var password = "testpw"; var hardwareId = "test_hardware_id"; - var key = Security.MakeEncryptionKey(username, password, hardwareId); + var key = Security.MakeEncryptionKey(email, password, hardwareId); var encryptedStream = new MemoryStream(); await StringToStream(testString).EncryptTo(encryptedStream, key); diff --git a/env/02 db-scripts/02_structure.sql b/env/02 db-scripts/02_structure.sql index 5d6415c..1b78e4f 100644 --- a/env/02 db-scripts/02_structure.sql +++ b/env/02 db-scripts/02_structure.sql @@ -2,12 +2,21 @@ drop table users; create table users ( - id uuid primary key, - email varchar(160) not null, + id uuid primary key, + email varchar(160) not null, password_hash varchar(255) not null, - hardware_id varchar(120) null, - role varchar(20) not null + hardware text null, + hardware_hash varchar(120) null, + role varchar(20) not null ); grant select, insert, update, delete on public.users to azaion_admin; grant select on table public.users to azaion_reader; +INSERT INTO public.users + (id, email, password_hash, hardware, hardware_hash, role) +VALUES ('d90a36ca-e237-4fbd-9c7c-127040ac8556', + 'admin@azaion.com', + '282wqVHZU0liTxphiGkKIaJtUA1W6rILdvfEOx8Ez350x0XLbgNtrSUYCK1r/ajq', + null, + null, + 'ApiAdmin');