From 66a73431ef44b3583ba6f0f95226675f34030317 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 5 Dec 2024 16:42:55 +0100 Subject: [PATCH 1/9] Add initial WIP POC for consideration --- .../Account/Authenticated/ChangeUsername.cs | 2 +- API/Controller/Admin/DeleteUser.cs | 2 +- API/Controller/Admin/_ApiController.cs | 3 ++- API/Controller/Devices/DeviceOtaController.cs | 2 +- API/Controller/Devices/DevicesController.cs | 2 +- API/Controller/Sessions/DeleteSessions.cs | 2 +- API/Controller/Shares/DeleteShareCode.cs | 2 +- .../Shares/Links/DeleteShareLink.cs | 2 +- .../Shares/Links/DeleteShockerShareLink.cs | 2 +- .../Shockers/DeleteShockerController.cs | 2 +- API/Controller/Tokens/TokenController.cs | 12 ++++----- API/Controller/Users/GetSelf.cs | 4 +-- .../UserSessionAuthentication.cs | 3 ++- .../Authentication/OpenShockAuthPolicies.cs | 6 ----- Common/Extensions/UserExtensions.cs | 18 +++++-------- Common/IQueryableExtensions.cs | 9 ++----- Common/Models/RankUtils.cs | 6 ----- Common/Models/{RankType.cs => RoleType.cs} | 2 +- Common/OpenShockDb/AdminUsersView.cs | 2 +- Common/OpenShockDb/OpenShockContext.cs | 14 +++++----- Common/OpenShockDb/User.cs | 2 +- Common/OpenShockServiceHelper.cs | 26 +------------------ Cron/DashboardAdminAuth.cs | 3 ++- 23 files changed, 43 insertions(+), 85 deletions(-) delete mode 100644 Common/Models/RankUtils.cs rename Common/Models/{RankType.cs => RoleType.cs} (84%) diff --git a/API/Controller/Account/Authenticated/ChangeUsername.cs b/API/Controller/Account/Authenticated/ChangeUsername.cs index ac59b15a..1d31bba6 100644 --- a/API/Controller/Account/Authenticated/ChangeUsername.cs +++ b/API/Controller/Account/Authenticated/ChangeUsername.cs @@ -24,7 +24,7 @@ public sealed partial class AuthenticatedAccountController public async Task ChangeUsername(ChangeUsernameRequest data) { var result = await _accountService.ChangeUsername(CurrentUser.Id, data.Username, - CurrentUser.Rank.IsAllowed(RankType.Staff)); + CurrentUser.Roles.Any(r => r is RoleType.Staff or RoleType.Admin or RoleType.System)); return result.Match( success => Ok(), diff --git a/API/Controller/Admin/DeleteUser.cs b/API/Controller/Admin/DeleteUser.cs index 0745f3e8..8906d440 100644 --- a/API/Controller/Admin/DeleteUser.cs +++ b/API/Controller/Admin/DeleteUser.cs @@ -27,7 +27,7 @@ public async Task DeleteUser([FromRoute] Guid userId) return Problem(AdminError.UserNotFound); } - if (user.Rank >= RankType.Admin) + if (user.Roles.Any(r => r is RoleType.Admin or RoleType.System)) { return Problem(AdminError.CannotDeletePrivledgedAccount); } diff --git a/API/Controller/Admin/_ApiController.cs b/API/Controller/Admin/_ApiController.cs index ac6c33fe..aa5f5580 100644 --- a/API/Controller/Admin/_ApiController.cs +++ b/API/Controller/Admin/_ApiController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; +using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; using Redis.OM.Contracts; @@ -9,7 +10,7 @@ namespace OpenShock.API.Controller.Admin; [ApiController] [Route("/{version:apiVersion}/admin")] -[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie, Policy = OpenShockAuthPolicies.AdminAccess)] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie, Roles = "Admin")] public sealed partial class AdminController : AuthenticatedSessionControllerBase { private readonly OpenShockContext _db; diff --git a/API/Controller/Devices/DeviceOtaController.cs b/API/Controller/Devices/DeviceOtaController.cs index b38f08f0..4fc4e62a 100644 --- a/API/Controller/Devices/DeviceOtaController.cs +++ b/API/Controller/Devices/DeviceOtaController.cs @@ -25,7 +25,7 @@ public sealed partial class DevicesController /// Could not find device or you do not have access to it [HttpGet("{deviceId}/ota")] [MapToApiVersion("1")] - [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] + [Authorize(Roles = "User")] [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // DeviceNotFound public async Task GetOtaUpdateHistory([FromRoute] Guid deviceId, [FromServices] IOtaService otaService) diff --git a/API/Controller/Devices/DevicesController.cs b/API/Controller/Devices/DevicesController.cs index ba39e629..08c8d415 100644 --- a/API/Controller/Devices/DevicesController.cs +++ b/API/Controller/Devices/DevicesController.cs @@ -132,7 +132,7 @@ public async Task RegenerateDeviceToken([FromRoute] Guid deviceId [MapToApiVersion("1")] public async Task RemoveDevice([FromRoute] Guid deviceId, [FromServices] IDeviceUpdateService updateService) { - var affected = await _db.Devices.Where(x => x.Id == deviceId).WhereIsUserOrAdmin(x => x.OwnerNavigation, CurrentUser).ExecuteDeleteAsync(); + var affected = await _db.Devices.Where(x => x.Id == deviceId).WhereIsUserOrPrivileged(x => x.OwnerNavigation, CurrentUser).ExecuteDeleteAsync(); if (affected <= 0) return Problem(DeviceError.DeviceNotFound); await updateService.UpdateDeviceForAllShared(CurrentUser.Id, deviceId, DeviceUpdateType.Deleted); diff --git a/API/Controller/Sessions/DeleteSessions.cs b/API/Controller/Sessions/DeleteSessions.cs index 9446e4c8..1cbd72da 100644 --- a/API/Controller/Sessions/DeleteSessions.cs +++ b/API/Controller/Sessions/DeleteSessions.cs @@ -17,7 +17,7 @@ public async Task DeleteSession(Guid sessionId) var loginSession = await _sessionService.GetSessionByPulbicId(sessionId); // If the session was not found, or the user does not have the privledges to access it, return NotFound - if (loginSession == null || !CurrentUser.IsUserOrRank(loginSession.UserId, RankType.Admin)) + if (loginSession == null || !CurrentUser.IsUserOrRole(loginSession.UserId, RoleType.Admin)) { return Problem(SessionError.SessionNotFound); } diff --git a/API/Controller/Shares/DeleteShareCode.cs b/API/Controller/Shares/DeleteShareCode.cs index c3d8db64..74fe480c 100644 --- a/API/Controller/Shares/DeleteShareCode.cs +++ b/API/Controller/Shares/DeleteShareCode.cs @@ -26,7 +26,7 @@ public async Task DeleteShareCode([FromRoute] Guid shareCodeId) { var affected = await _db.ShockerShareCodes .Where(x => x.Id == shareCodeId) - .WhereIsUserOrAdmin(x => x.Shocker.DeviceNavigation.OwnerNavigation, CurrentUser) + .WhereIsUserOrPrivileged(x => x.Shocker.DeviceNavigation.OwnerNavigation, CurrentUser) .ExecuteDeleteAsync(); if (affected <= 0) { diff --git a/API/Controller/Shares/Links/DeleteShareLink.cs b/API/Controller/Shares/Links/DeleteShareLink.cs index 2ad0c92c..92a63628 100644 --- a/API/Controller/Shares/Links/DeleteShareLink.cs +++ b/API/Controller/Shares/Links/DeleteShareLink.cs @@ -24,7 +24,7 @@ public async Task DeleteShareLink([FromRoute] Guid shareLinkId) { var result = await _db.ShockerSharesLinks .Where(x => x.Id == shareLinkId) - .WhereIsUserOrAdmin(x => x.Owner, CurrentUser) + .WhereIsUserOrPrivileged(x => x.Owner, CurrentUser) .ExecuteDeleteAsync(); return result > 0 diff --git a/API/Controller/Shares/Links/DeleteShockerShareLink.cs b/API/Controller/Shares/Links/DeleteShockerShareLink.cs index 63a97474..1b485f81 100644 --- a/API/Controller/Shares/Links/DeleteShockerShareLink.cs +++ b/API/Controller/Shares/Links/DeleteShockerShareLink.cs @@ -26,7 +26,7 @@ public async Task RemoveShocker([FromRoute] Guid shareLinkId, [Fr { var exists = await _db.ShockerSharesLinks .Where(x => x.Id == shareLinkId) - .WhereIsUserOrAdmin(x => x.Owner, CurrentUser) + .WhereIsUserOrPrivileged(x => x.Owner, CurrentUser) .AnyAsync(); if (!exists) { diff --git a/API/Controller/Shockers/DeleteShockerController.cs b/API/Controller/Shockers/DeleteShockerController.cs index ee2171d8..da67e9c6 100644 --- a/API/Controller/Shockers/DeleteShockerController.cs +++ b/API/Controller/Shockers/DeleteShockerController.cs @@ -32,7 +32,7 @@ public async Task RemoveShocker( { var affected = await _db.Shockers .Where(x => x.Id == shockerId) - .WhereIsUserOrAdmin(x => x.DeviceNavigation.OwnerNavigation, CurrentUser) + .WhereIsUserOrPrivileged(x => x.DeviceNavigation.OwnerNavigation, CurrentUser) .FirstOrDefaultAsync(); if (affected == null) diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index 67d6ec79..a6587891 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -25,7 +25,7 @@ public sealed partial class TokensController /// /// All tokens for the current user [HttpGet] - [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] + [Authorize(Roles = "User")] [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task> ListTokens() { @@ -52,7 +52,7 @@ public async Task> ListTokens() /// The token /// The token does not exist or you do not have access to it. [HttpGet("{tokenId}")] - [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] + [Authorize(Roles = "User")] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task GetTokenById([FromRoute] Guid tokenId) @@ -81,14 +81,14 @@ public async Task GetTokenById([FromRoute] Guid tokenId) /// Successfully deleted token /// The token does not exist or you do not have access to it. [HttpDelete("{tokenId}")] - [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] + [Authorize(Roles = "User")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task DeleteToken([FromRoute] Guid tokenId) { var apiToken = await _db.ApiTokens .Where(x => x.Id == tokenId) - .WhereIsUserOrAdmin(x => x.User, CurrentUser) + .WhereIsUserOrPrivileged(x => x.User, CurrentUser) .ExecuteDeleteAsync(); if (apiToken <= 0) @@ -105,7 +105,7 @@ public async Task DeleteToken([FromRoute] Guid tokenId) /// /// The created token [HttpPost] - [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] + [Authorize(Roles = "User")] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task CreateToken([FromBody] CreateTokenRequest body) { @@ -139,7 +139,7 @@ public async Task CreateToken([FromBody] CreateTokenReques /// The edited token /// The token does not exist or you do not have access to it. [HttpPatch("{tokenId}")] - [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] + [Authorize(Roles = "User")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task EditToken([FromRoute] Guid tokenId, [FromBody] EditTokenRequest body) diff --git a/API/Controller/Users/GetSelf.cs b/API/Controller/Users/GetSelf.cs index 1e801b05..5663f659 100644 --- a/API/Controller/Users/GetSelf.cs +++ b/API/Controller/Users/GetSelf.cs @@ -21,7 +21,7 @@ public sealed partial class UsersController Name = CurrentUser.Name, Email = CurrentUser.Email, Image = CurrentUser.GetImageLink(), - Rank = CurrentUser.Rank + Roles = CurrentUser.Roles } }; public sealed class SelfResponse @@ -30,6 +30,6 @@ public sealed class SelfResponse public required string Name { get; set; } public required string Email { get; set; } public required Uri Image { get; set; } - public required RankType Rank { get; set; } + public required List Roles { get; set; } } } \ No newline at end of file diff --git a/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs b/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs index ec69aeb9..bcf726c6 100644 --- a/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs +++ b/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs @@ -76,9 +76,10 @@ protected override async Task HandleAuthenticateAsync() List claims = [ new(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.UserSessionCookie), new(ClaimTypes.NameIdentifier, retrievedUser.Id.ToString()), - new(ClaimTypes.Role, retrievedUser.Rank.ToString()) ]; + claims.AddRange(retrievedUser.Roles.Select(r => new Claim(ClaimTypes.Role, r.ToString()))); + var ident = new ClaimsIdentity(claims, nameof(UserSessionAuthentication)); Context.User = new ClaimsPrincipal(ident); diff --git a/Common/Authentication/OpenShockAuthPolicies.cs b/Common/Authentication/OpenShockAuthPolicies.cs index eddc6a03..1df116af 100644 --- a/Common/Authentication/OpenShockAuthPolicies.cs +++ b/Common/Authentication/OpenShockAuthPolicies.cs @@ -2,11 +2,5 @@ public static class OpenShockAuthPolicies { - public const string UserAccess = "UserAccess"; - public const string SupportAccess = "SupportAccess"; - public const string StaffAccess = "StaffAccess"; - public const string AdminAccess = "AdminAccess"; - public const string SystemAccess = "SystemAccess"; - public const string TokenSessionOnly = "ApiTokenOnly"; } diff --git a/Common/Extensions/UserExtensions.cs b/Common/Extensions/UserExtensions.cs index c4ac0cd0..939581f9 100644 --- a/Common/Extensions/UserExtensions.cs +++ b/Common/Extensions/UserExtensions.cs @@ -18,22 +18,18 @@ public static bool IsUser(this User user, User otherUser) return user == otherUser || user.Id == otherUser.Id; } - public static bool IsRank(this User user, RankType rank) + public static bool IsRole(this User user, RoleType role) { - return user.Rank >= rank; + return user.Roles.Contains(role); } - public static bool IsUserOrRank(this User user, User otherUser, RankType rank) + public static bool IsUserOrRole(this User user, Guid otherUserId, RoleType role) { - if (user.IsUser(otherUser)) return true; - - return user.Rank >= rank; + return user.IsUser(otherUserId) || user.IsRole(role); } - public static bool IsUserOrRank(this User user, Guid otherUserId, RankType rank) + public static bool IsUserOrRole(this User user, User otherUser, RoleType role) { - if (user.IsUser(otherUserId)) return true; - - return user.Rank >= rank; + return user.IsUser(otherUser) || user.IsRole(role); } -} \ No newline at end of file +} diff --git a/Common/IQueryableExtensions.cs b/Common/IQueryableExtensions.cs index a1a05f2b..fc47e700 100644 --- a/Common/IQueryableExtensions.cs +++ b/Common/IQueryableExtensions.cs @@ -38,18 +38,13 @@ public static IQueryable WhereIsUser(this IQueryable return source.Where(IsUserMatchExpr(navigationSelector, userId)); } - public static IQueryable WhereIsUserOrRank(this IQueryable source, Expression> navigationSelector, User user, RankType rank) + public static IQueryable WhereIsUserOrPrivileged(this IQueryable source, Expression> navigationSelector, User user) { - if (user.Rank >= rank) + if (user.Roles.Any(r => r is RoleType.Admin or RoleType.System)) { return source; } return WhereIsUser(source, navigationSelector, user.Id); } - - public static IQueryable WhereIsUserOrAdmin(this IQueryable source, Expression> navigationSelector, User user) - { - return WhereIsUserOrRank(source, navigationSelector, user, RankType.Admin); - } } diff --git a/Common/Models/RankUtils.cs b/Common/Models/RankUtils.cs deleted file mode 100644 index 0dec10a8..00000000 --- a/Common/Models/RankUtils.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OpenShock.Common.Models; - -public static class RankUtils -{ - public static bool IsAllowed(this RankType userRank, RankType rankNeeded) => userRank >= rankNeeded; -} \ No newline at end of file diff --git a/Common/Models/RankType.cs b/Common/Models/RoleType.cs similarity index 84% rename from Common/Models/RankType.cs rename to Common/Models/RoleType.cs index 46ad55d4..1d892a20 100644 --- a/Common/Models/RankType.cs +++ b/Common/Models/RoleType.cs @@ -1,6 +1,6 @@ namespace OpenShock.Common.Models; -public enum RankType +public enum RoleType { User = 0, Support = 1, diff --git a/Common/OpenShockDb/AdminUsersView.cs b/Common/OpenShockDb/AdminUsersView.cs index a14e8dd7..7fba6133 100644 --- a/Common/OpenShockDb/AdminUsersView.cs +++ b/Common/OpenShockDb/AdminUsersView.cs @@ -18,7 +18,7 @@ public class AdminUsersView public required bool EmailActivated { get; set; } - public required RankType Rank { get; set; } + public required List Roles { get; set; } public required int ApiTokenCount { get; set; } diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index 3212bda5..173ff25a 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -65,7 +65,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasPostgresEnum("ota_update_status", new[] { "started", "running", "finished", "error", "timeout" }) .HasPostgresEnum("password_encryption_type", new[] { "pbkdf2", "bcrypt_enhanced" }) .HasPostgresEnum("permission_type", new[] { "shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth" }) - .HasPostgresEnum("rank_type", new[] { "user", "support", "staff", "admin", "system" }) + .HasPostgresEnum("role_type", new[] { "user", "support", "staff", "admin", "system" }) .HasPostgresEnum("shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR" }) .HasAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False"); @@ -465,9 +465,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.PasswordHash) .VarCharWithLength(HardLimits.PasswordHashMaxLength) .HasColumnName("password_hash"); - entity.Property(e => e.Rank) - .HasColumnType("rank_type") - .HasColumnName("rank"); + entity.Property(e => e.Roles) + .HasColumnType("role_type[]") + .HasColumnName("roles"); }); modelBuilder.Entity(entity => @@ -577,9 +577,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("created_at"); entity.Property(e => e.EmailActivated) .HasColumnName("email_activated"); - entity.Property(e => e.Rank) - .HasColumnType("rank_type") - .HasColumnName("rank"); + entity.Property(e => e.Roles) + .HasColumnType("role_type[]") + .HasColumnName("roles"); entity.Property(e => e.ApiTokenCount) .HasColumnName("api_token_count"); entity.Property(e => e.PasswordResetCount) diff --git a/Common/OpenShockDb/User.cs b/Common/OpenShockDb/User.cs index a0036816..7146adac 100644 --- a/Common/OpenShockDb/User.cs +++ b/Common/OpenShockDb/User.cs @@ -18,7 +18,7 @@ public partial class User public bool EmailActivated { get; set; } - public RankType Rank { get; set; } + public List Roles { get; set; } = null!; public virtual ICollection ApiTokens { get; set; } = new List(); diff --git a/Common/OpenShockServiceHelper.cs b/Common/OpenShockServiceHelper.cs index ca92de51..48b38afc 100644 --- a/Common/OpenShockServiceHelper.cs +++ b/Common/OpenShockServiceHelper.cs @@ -29,24 +29,6 @@ namespace OpenShock.Common; public static class OpenShockServiceHelper { - // TODO: this is temporary while we still rely on enums for user ranks - static bool HandleRankCheck(AuthorizationHandlerContext context, RankType requiredRank) - { - var ranks = context.User.Identities.SelectMany(ident => ident.Claims.Where(claim => claim.Type == ident.RoleClaimType)).Select(claim => Enum.Parse(claim.Value)).ToList(); - - if (!ranks.Any()) - { - return false; - } - - if (ranks.Max() < requiredRank) - { - return false; - } - - return true; - } - /// /// Register all OpenShock services for PRODUCTION use /// @@ -75,12 +57,6 @@ public static ServicesResult AddOpenShockServices(this IServiceCollection servic services.AddAuthorization(options => { - options.AddPolicy(OpenShockAuthPolicies.SystemAccess, policy => policy.RequireRole(RankType.System.ToString())); - options.AddPolicy(OpenShockAuthPolicies.AdminAccess, policy => policy.RequireAssertion(context => HandleRankCheck(context, RankType.Admin))); - options.AddPolicy(OpenShockAuthPolicies.StaffAccess, policy => policy.RequireAssertion(context => HandleRankCheck(context, RankType.Staff))); - options.AddPolicy(OpenShockAuthPolicies.SupportAccess, policy => policy.RequireAssertion(context => HandleRankCheck(context, RankType.Support))); - options.AddPolicy(OpenShockAuthPolicies.UserAccess, policy => policy.RequireAssertion(context => HandleRankCheck(context, RankType.User))); - options.AddPolicy(OpenShockAuthPolicies.TokenSessionOnly, policy => policy.RequireClaim(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.ApiToken)); // TODO: Add token permission policies }); @@ -190,7 +166,7 @@ public static ServicesResult AddOpenShockServices(this IServiceCollection servic { builder.UseNpgsql(config.Db.Conn, optionsBuilder => { - optionsBuilder.MapEnum(); + optionsBuilder.MapEnum(); optionsBuilder.MapEnum(); optionsBuilder.MapEnum(); optionsBuilder.MapEnum(); diff --git a/Cron/DashboardAdminAuth.cs b/Cron/DashboardAdminAuth.cs index 48fb1a4e..2b686732 100644 --- a/Cron/DashboardAdminAuth.cs +++ b/Cron/DashboardAdminAuth.cs @@ -1,5 +1,6 @@ using Hangfire.Dashboard; using Microsoft.EntityFrameworkCore; +using OpenShock.Common.Extensions; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Redis; @@ -37,6 +38,6 @@ private static async Task SessionAuthAdmin(string sessionKey, IRedisCollec var session = await loginSessions.FindByIdAsync(sessionKey); if (session == null) return false; var retrievedUser = await db.Users.FirstAsync(user => user.Id == session.UserId); - return retrievedUser.Rank == RankType.Admin; + return retrievedUser.IsRole(RoleType.Admin); } } \ No newline at end of file From e265731b4f5a6b5515c8c527e9459f078def2086 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 5 Dec 2024 20:50:39 +0100 Subject: [PATCH 2/9] Add POC migrations --- .../20241205161255_RanksToRoles.Designer.cs | 1131 +++++++++++++++++ .../Migrations/20241205161255_RanksToRoles.cs | 146 +++ .../OpenShockContextModelSnapshot.cs | 16 +- 3 files changed, 1286 insertions(+), 7 deletions(-) create mode 100644 Common/Migrations/20241205161255_RanksToRoles.Designer.cs create mode 100644 Common/Migrations/20241205161255_RanksToRoles.cs diff --git a/Common/Migrations/20241205161255_RanksToRoles.Designer.cs b/Common/Migrations/20241205161255_RanksToRoles.Designer.cs new file mode 100644 index 00000000..76d4943c --- /dev/null +++ b/Common/Migrations/20241205161255_RanksToRoles.Designer.cs @@ -0,0 +1,1131 @@ +// +using System; +using System.Net; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using OpenShock.Common.OpenShockDb; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + [DbContext(typeof(OpenShockContext))] + [Migration("20241205161255_RanksToRoles")] + partial class RanksToRoles + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_type", new[] { "sound", "vibrate", "shock", "stop" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "ota_update_status", new[] { "started", "running", "finished", "error", "timeout" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "password_encryption_type", new[] { "pbkdf2", "bcrypt_enhanced" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "permission_type", new[] { "shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role_type", new[] { "user", "support", "staff", "admin", "system" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.AdminUsersView", b => + { + b.Property("ApiTokenCount") + .HasColumnType("integer") + .HasColumnName("api_token_count"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeviceCount") + .HasColumnType("integer") + .HasColumnName("device_count"); + + b.Property("Email") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("email"); + + b.Property("EmailActivated") + .HasColumnType("boolean") + .HasColumnName("email_activated"); + + b.Property("EmailChangeRequestCount") + .HasColumnType("integer") + .HasColumnName("email_change_request_count"); + + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("name"); + + b.Property("NameChangeRequestCount") + .HasColumnType("integer") + .HasColumnName("name_change_request_count"); + + b.Property("PasswordHashType") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("password_hash_type"); + + b.Property("PasswordResetCount") + .HasColumnType("integer") + .HasColumnName("password_reset_count"); + + b.PrimitiveCollection("Roles") + .IsRequired() + .HasColumnType("role_type[]") + .HasColumnName("roles"); + + b.Property("ShockerControlLogCount") + .HasColumnType("integer") + .HasColumnName("shocker_control_log_count"); + + b.Property("ShockerCount") + .HasColumnType("integer") + .HasColumnName("shocker_count"); + + b.Property("ShockerShareCount") + .HasColumnType("integer") + .HasColumnName("shocker_share_count"); + + b.Property("ShockerShareLinkCount") + .HasColumnType("integer") + .HasColumnName("shocker_share_link_count"); + + b.Property("UserActivationCount") + .HasColumnType("integer") + .HasColumnName("user_activation_count"); + + b.ToTable((string)null); + + b.ToView("admin_users_view", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiToken", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedByIp") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("created_by_ip"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LastUsed") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used") + .HasDefaultValueSql("'-infinity'::timestamp without time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.PrimitiveCollection("Permissions") + .IsRequired() + .HasColumnType("permission_type[]") + .HasColumnName("permissions"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token_hash"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_until"); + + b.HasKey("Id") + .HasName("api_tokens_pkey"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("ValidUntil") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("api_tokens", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("Owner") + .HasColumnType("uuid") + .HasColumnName("owner"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("token"); + + b.HasKey("Id") + .HasName("devices_pkey"); + + b.HasIndex("Owner") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("Token") + .IsUnique(); + + b.ToTable("devices", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.DeviceOtaUpdate", b => + { + b.Property("Device") + .HasColumnType("uuid") + .HasColumnName("device"); + + b.Property("UpdateId") + .HasColumnType("integer") + .HasColumnName("update_id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Message") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("message"); + + b.Property("Status") + .HasColumnType("ota_update_status") + .HasColumnName("status"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("version"); + + b.HasKey("Device", "UpdateId") + .HasName("device_ota_updates_pkey"); + + b.HasIndex(new[] { "CreatedOn" }, "device_ota_updates_created_on_idx") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("device_ota_updates", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PasswordReset", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("secret"); + + b.Property("UsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_on"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("password_resets_pkey"); + + b.HasIndex("UserId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("password_resets", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequest", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Owner") + .HasColumnType("uuid") + .HasColumnName("owner"); + + b.Property("User") + .HasColumnType("uuid") + .HasColumnName("user"); + + b.HasKey("Id") + .HasName("shares_codes_pkey"); + + b.HasIndex("Owner") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("User"); + + b.ToTable("share_requests", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequestsShocker", b => + { + b.Property("ShareRequest") + .HasColumnType("uuid") + .HasColumnName("share_request"); + + b.Property("Shocker") + .HasColumnType("uuid") + .HasColumnName("shocker"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("PermLive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_live"); + + b.Property("PermShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_vibrate"); + + b.HasKey("ShareRequest", "Shocker") + .HasName("share_requests_shockers_pkey"); + + b.HasIndex("Shocker"); + + b.ToTable("share_requests_shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Device") + .HasColumnType("uuid") + .HasColumnName("device"); + + b.Property("Model") + .HasColumnType("shocker_model_type") + .HasColumnName("model"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("Paused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("paused"); + + b.Property("RfId") + .HasColumnType("integer") + .HasColumnName("rf_id"); + + b.HasKey("Id") + .HasName("shockers_pkey"); + + b.HasIndex("Device") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ControlledBy") + .HasColumnType("uuid") + .HasColumnName("controlled_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CustomName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("custom_name"); + + b.Property("Duration") + .HasColumnType("bigint") + .HasColumnName("duration"); + + b.Property("Intensity") + .HasColumnType("smallint") + .HasColumnName("intensity"); + + b.Property("LiveControl") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("live_control"); + + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("Type") + .HasColumnType("control_type") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("shocker_control_logs_pkey"); + + b.HasIndex("ControlledBy"); + + b.HasIndex("ShockerId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shocker_control_logs", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShare", b => + { + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("SharedWith") + .HasColumnType("uuid") + .HasColumnName("shared_with"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("Paused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("paused"); + + b.Property("PermLive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_live"); + + b.Property("PermShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_vibrate"); + + b.HasKey("ShockerId", "SharedWith") + .HasName("shocker_shares_pkey"); + + b.HasIndex("SharedWith") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shocker_shares", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShareCode", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("PermShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_vibrate"); + + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.HasKey("Id") + .HasName("shocker_share_codes_pkey"); + + b.HasIndex("ShockerId"); + + b.ToTable("shocker_share_codes", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLink", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("uuid") + .HasColumnName("owner_id"); + + b.HasKey("Id") + .HasName("shocker_shares_links_pkey"); + + b.HasIndex("OwnerId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shocker_shares_links", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLinksShocker", b => + { + b.Property("ShareLinkId") + .HasColumnType("uuid") + .HasColumnName("share_link_id"); + + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("Cooldown") + .HasColumnType("integer") + .HasColumnName("cooldown"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("Paused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("paused"); + + b.Property("PermLive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_live"); + + b.Property("PermShock") + .HasColumnType("boolean") + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .HasColumnType("boolean") + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .HasColumnType("boolean") + .HasColumnName("perm_vibrate"); + + b.HasKey("ShareLinkId", "ShockerId") + .HasName("shocker_shares_links_shockers_pkey"); + + b.HasIndex("ShockerId"); + + b.ToTable("shocker_shares_links_shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)") + .HasColumnName("email"); + + b.Property("EmailActivated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("email_activated"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("name") + .UseCollation("ndcoll"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("password_hash"); + + b.PrimitiveCollection("Roles") + .IsRequired() + .HasColumnType("role_type[]") + .HasColumnName("roles"); + + b.HasKey("Id") + .HasName("users_pkey"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + NpgsqlIndexBuilderExtensions.UseCollation(b.HasIndex("Name"), new[] { "ndcoll" }); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersActivation", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("secret"); + + b.Property("UsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_on"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("users_activation_pkey"); + + b.HasIndex("UserId"); + + b.ToTable("users_activation", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersEmailChange", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)") + .HasColumnName("email"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("secret"); + + b.Property("UsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_on"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("users_email_change_pkey"); + + b.HasIndex("CreatedOn") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("UsedOn") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("UserId"); + + b.ToTable("users_email_changes", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersNameChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("OldName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("old_name"); + + b.HasKey("Id", "UserId") + .HasName("users_name_changes_pkey"); + + b.HasIndex("CreatedOn") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("OldName") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("UserId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("users_name_changes", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiToken", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("ApiTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "OwnerNavigation") + .WithMany("Devices") + .HasForeignKey("Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("owner_user_id"); + + b.Navigation("OwnerNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.DeviceOtaUpdate", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.Device", "DeviceNavigation") + .WithMany("DeviceOtaUpdates") + .HasForeignKey("Device") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("device_ota_updates_device"); + + b.Navigation("DeviceNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PasswordReset", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("PasswordResets") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequest", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "OwnerNavigation") + .WithMany("ShareRequestOwnerNavigations") + .HasForeignKey("Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_share_requests_owner"); + + b.HasOne("OpenShock.Common.OpenShockDb.User", "UserNavigation") + .WithMany("ShareRequestUserNavigations") + .HasForeignKey("User") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_share_requests_user"); + + b.Navigation("OwnerNavigation"); + + b.Navigation("UserNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequestsShocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.ShareRequest", "ShareRequestNavigation") + .WithMany("ShareRequestsShockers") + .HasForeignKey("ShareRequest") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_share_requests_shockers_share_request"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "ShockerNavigation") + .WithMany("ShareRequestsShockers") + .HasForeignKey("Shocker") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_share_requests_shockers_shocker"); + + b.Navigation("ShareRequestNavigation"); + + b.Navigation("ShockerNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.Device", "DeviceNavigation") + .WithMany("Shockers") + .HasForeignKey("Device") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("device_id"); + + b.Navigation("DeviceNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "ControlledByNavigation") + .WithMany("ShockerControlLogs") + .HasForeignKey("ControlledBy") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_controlled_by"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerControlLogs") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shocker_id"); + + b.Navigation("ControlledByNavigation"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShare", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "SharedWithNavigation") + .WithMany("ShockerShares") + .HasForeignKey("SharedWith") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("shared_with_user_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerShares") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("ref_shocker_id"); + + b.Navigation("SharedWithNavigation"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShareCode", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerShareCodes") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shocker_id"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLink", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner") + .WithMany("ShockerSharesLinks") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("owner_id"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLinksShocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.ShockerSharesLink", "ShareLink") + .WithMany("ShockerSharesLinksShockers") + .HasForeignKey("ShareLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("share_link_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerSharesLinksShockers") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("shocker_id"); + + b.Navigation("ShareLink"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersActivation", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("UsersActivations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersEmailChange", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("UsersEmailChanges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersNameChange", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("UsersNameChanges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.Navigation("DeviceOtaUpdates"); + + b.Navigation("Shockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequest", b => + { + b.Navigation("ShareRequestsShockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.Navigation("ShareRequestsShockers"); + + b.Navigation("ShockerControlLogs"); + + b.Navigation("ShockerShareCodes"); + + b.Navigation("ShockerShares"); + + b.Navigation("ShockerSharesLinksShockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLink", b => + { + b.Navigation("ShockerSharesLinksShockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b => + { + b.Navigation("ApiTokens"); + + b.Navigation("Devices"); + + b.Navigation("PasswordResets"); + + b.Navigation("ShareRequestOwnerNavigations"); + + b.Navigation("ShareRequestUserNavigations"); + + b.Navigation("ShockerControlLogs"); + + b.Navigation("ShockerShares"); + + b.Navigation("ShockerSharesLinks"); + + b.Navigation("UsersActivations"); + + b.Navigation("UsersEmailChanges"); + + b.Navigation("UsersNameChanges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Common/Migrations/20241205161255_RanksToRoles.cs b/Common/Migrations/20241205161255_RanksToRoles.cs new file mode 100644 index 00000000..42c08dce --- /dev/null +++ b/Common/Migrations/20241205161255_RanksToRoles.cs @@ -0,0 +1,146 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + /// + public partial class RanksToRoles : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // We need to drop the view to modify the target table + migrationBuilder.Sql( + """ + DROP VIEW admin_users_view; + ALTER TYPE rank_type RENAME TO role_type; + """ + ); + + migrationBuilder.AddColumn( + name: "roles", + table: "users", + type: "role_type[]", + nullable: false, + defaultValue: Array.Empty()); + + // Migrate data from 'rank' to 'roles' + migrationBuilder.Sql( + """ + UPDATE users + SET roles = CAST(( + CASE + WHEN rank = 'user' THEN ARRAY['user'] + WHEN rank = 'support' THEN ARRAY['user', 'support'] + WHEN rank = 'staff' THEN ARRAY['user', 'support', 'staff'] + WHEN rank = 'admin' THEN ARRAY['user', 'support', 'staff', 'admin'] + WHEN rank = 'system' THEN ARRAY['user', 'support', 'staff', 'admin', 'system'] + ELSE CAST(ARRAY[] AS text[]) + END + ) AS role_type[]); + """ + ); + + migrationBuilder.DropColumn( + name: "rank", + table: "users"); + + // Re-Create the view + migrationBuilder.Sql( + """ + CREATE VIEW admin_users_view AS + SELECT + u.id, + u.name, + u.email, + SPLIT_PART(u.password_hash, ':', 1) AS password_hash_type, + u.created_at, + u.email_activated, + u.roles, + (SELECT COUNT(*) FROM api_tokens ato WHERE ato.user_id = u.id) AS api_token_count, + (SELECT COUNT(*) FROM password_resets pre WHERE pre.user_id = u.id) AS password_reset_count, + (SELECT COUNT(*) FROM shocker_shares ssh WHERE ssh.shared_with = u.id) AS shocker_share_count, + (SELECT COUNT(*) FROM shocker_shares_links ssl WHERE ssl.owner_id = u.id) AS shocker_share_link_count, + (SELECT COUNT(*) FROM users_email_changes uec WHERE uec.user_id = u.id) AS email_change_request_count, + (SELECT COUNT(*) FROM users_name_changes unc WHERE unc.user_id = u.id) AS name_change_request_count, + (SELECT COUNT(*) FROM users_activation uac WHERE uac.user_id = u.id) AS user_activation_count, + (SELECT COUNT(*) FROM devices dev WHERE dev.owner = u.id) AS device_count, + (SELECT COUNT(*) FROM devices dev JOIN shockers sck ON dev.id = sck.device WHERE dev.owner = u.id) AS shocker_count, + (SELECT COUNT(*) FROM devices dev JOIN shockers sck ON dev.id = sck.device JOIN shocker_control_logs scl ON scl.shocker_id = sck.id WHERE dev.owner = u.id) AS shocker_control_log_count + FROM + users u; + """ + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // We need to drop the view to modify the target table + migrationBuilder.Sql( + """ + DROP VIEW admin_users_view; + ALTER TYPE role_type RENAME TO rank_type; + """ + ); + + migrationBuilder.AddColumn( + name: "rank", + table: "users", + type: "rank_type", + nullable: true); + + migrationBuilder.Sql( + """ + UPDATE users + SET rank = CAST(( + CASE + WHEN 'system' = ANY(roles) THEN 'system' + WHEN 'admin' = ANY(roles) THEN 'admin' + WHEN 'staff' = ANY(roles) THEN 'staff' + WHEN 'support' = ANY(roles) THEN 'support' + ELSE 'user' + END + ) AS rank_type); + """ + ); + + migrationBuilder.AlterColumn( + name: "rank", + table: "users", + nullable: false); + + migrationBuilder.DropColumn( + name: "roles", + table: "users"); + + // Re-Create the view + migrationBuilder.Sql( + """ + CREATE VIEW admin_users_view AS + SELECT + u.id, + u.name, + u.email, + SPLIT_PART(u.password_hash, ':', 1) AS password_hash_type, + u.created_at, + u.email_activated, + u.rank, + (SELECT COUNT(*) FROM api_tokens ato WHERE ato.user_id = u.id) AS api_token_count, + (SELECT COUNT(*) FROM password_resets pre WHERE pre.user_id = u.id) AS password_reset_count, + (SELECT COUNT(*) FROM shocker_shares ssh WHERE ssh.shared_with = u.id) AS shocker_share_count, + (SELECT COUNT(*) FROM shocker_shares_links ssl WHERE ssl.owner_id = u.id) AS shocker_share_link_count, + (SELECT COUNT(*) FROM users_email_changes uec WHERE uec.user_id = u.id) AS email_change_request_count, + (SELECT COUNT(*) FROM users_name_changes unc WHERE unc.user_id = u.id) AS name_change_request_count, + (SELECT COUNT(*) FROM users_activation uac WHERE uac.user_id = u.id) AS user_activation_count, + (SELECT COUNT(*) FROM devices dev WHERE dev.owner = u.id) AS device_count, + (SELECT COUNT(*) FROM devices dev JOIN shockers sck ON dev.id = sck.device WHERE dev.owner = u.id) AS shocker_count, + (SELECT COUNT(*) FROM devices dev JOIN shockers sck ON dev.id = sck.device JOIN shocker_control_logs scl ON scl.shocker_id = sck.id WHERE dev.owner = u.id) AS shocker_control_log_count + FROM + users u; + """ + ); + } + } +} diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs index 0f5f566a..2e45fe0a 100644 --- a/Common/Migrations/OpenShockContextModelSnapshot.cs +++ b/Common/Migrations/OpenShockContextModelSnapshot.cs @@ -26,7 +26,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "ota_update_status", new[] { "started", "running", "finished", "error", "timeout" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "password_encryption_type", new[] { "pbkdf2", "bcrypt_enhanced" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "permission_type", new[] { "shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth" }); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "rank_type", new[] { "user", "support", "staff", "admin", "system" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role_type", new[] { "user", "support", "staff", "admin", "system" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -79,9 +79,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("password_reset_count"); - b.Property("Rank") - .HasColumnType("rank_type") - .HasColumnName("rank"); + b.PrimitiveCollection("Roles") + .IsRequired() + .HasColumnType("role_type[]") + .HasColumnName("roles"); b.Property("ShockerControlLogCount") .HasColumnType("integer") @@ -705,9 +706,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(100)") .HasColumnName("password_hash"); - b.Property("Rank") - .HasColumnType("rank_type") - .HasColumnName("rank"); + b.PrimitiveCollection("Roles") + .IsRequired() + .HasColumnType("role_type[]") + .HasColumnName("roles"); b.HasKey("Id") .HasName("users_pkey"); From d50851aa2a2f94bec2bbd7805787ff6052a6a04a Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 5 Dec 2024 23:05:01 +0100 Subject: [PATCH 3/9] Don't include user role --- .../20241205161255_RanksToRoles.Designer.cs | 2 +- .../Migrations/20241205161255_RanksToRoles.cs | 108 ++++++++---------- .../OpenShockContextModelSnapshot.cs | 2 +- Common/Models/RoleType.cs | 9 +- Common/OpenShockDb/OpenShockContext.cs | 2 +- 5 files changed, 53 insertions(+), 70 deletions(-) diff --git a/Common/Migrations/20241205161255_RanksToRoles.Designer.cs b/Common/Migrations/20241205161255_RanksToRoles.Designer.cs index 76d4943c..c2e29d96 100644 --- a/Common/Migrations/20241205161255_RanksToRoles.Designer.cs +++ b/Common/Migrations/20241205161255_RanksToRoles.Designer.cs @@ -29,7 +29,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "ota_update_status", new[] { "started", "running", "finished", "error", "timeout" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "password_encryption_type", new[] { "pbkdf2", "bcrypt_enhanced" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "permission_type", new[] { "shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth" }); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role_type", new[] { "user", "support", "staff", "admin", "system" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role_type", new[] { "support", "staff", "admin", "system" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); diff --git a/Common/Migrations/20241205161255_RanksToRoles.cs b/Common/Migrations/20241205161255_RanksToRoles.cs index 42c08dce..b4fb5236 100644 --- a/Common/Migrations/20241205161255_RanksToRoles.cs +++ b/Common/Migrations/20241205161255_RanksToRoles.cs @@ -10,45 +10,38 @@ public partial class RanksToRoles : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { - // We need to drop the view to modify the target table migrationBuilder.Sql( """ + -- Drop the view temporarily to modify the underlying table DROP VIEW admin_users_view; - ALTER TYPE rank_type RENAME TO role_type; - """ - ); - - migrationBuilder.AddColumn( - name: "roles", - table: "users", - type: "role_type[]", - nullable: false, - defaultValue: Array.Empty()); - - // Migrate data from 'rank' to 'roles' - migrationBuilder.Sql( - """ + + -- Add the roles column as a text array to replace the rank enum + ALTER TABLE users ADD roles text[] NOT NULL DEFAULT ARRAY[]::text[]; + + -- Migrate existing rank values into the roles array column UPDATE users - SET roles = CAST(( + SET roles = ( CASE - WHEN rank = 'user' THEN ARRAY['user'] - WHEN rank = 'support' THEN ARRAY['user', 'support'] - WHEN rank = 'staff' THEN ARRAY['user', 'support', 'staff'] - WHEN rank = 'admin' THEN ARRAY['user', 'support', 'staff', 'admin'] - WHEN rank = 'system' THEN ARRAY['user', 'support', 'staff', 'admin', 'system'] - ELSE CAST(ARRAY[] AS text[]) + WHEN rank = 'support' THEN ARRAY['support'] + WHEN rank = 'staff' THEN ARRAY['support', 'staff'] + WHEN rank = 'admin' THEN ARRAY['support', 'staff', 'admin'] + WHEN rank = 'system' THEN ARRAY['support', 'staff', 'admin', 'system'] + ELSE ARRAY[]::text[] END - ) AS role_type[]); - """ - ); - - migrationBuilder.DropColumn( - name: "rank", - table: "users"); - - // Re-Create the view - migrationBuilder.Sql( - """ + ); + + -- Remove the rank column after migration + ALTER TABLE users DROP COLUMN rank; + + -- Replace the old rank_type enum with a new role_type enum, dropping 'user' in the process + DROP TYPE rank_type; + CREATE TYPE role_type AS ENUM ('support', 'staff', 'admin', 'system'); + + -- Update the roles column to use the new role_type enum array + ALTER TABLE users ALTER COLUMN roles SET DEFAULT ARRAY[]::role_type[]; + ALTER TABLE users ALTER COLUMN roles TYPE role_type[] USING CAST(roles as role_type[]); + + -- Recreate the admin_users_view to reflect the new roles structure CREATE VIEW admin_users_view AS SELECT u.id, @@ -77,24 +70,17 @@ CREATE VIEW admin_users_view AS /// protected override void Down(MigrationBuilder migrationBuilder) { - // We need to drop the view to modify the target table migrationBuilder.Sql( """ + -- Drop the view temporarily to modify the underlying table DROP VIEW admin_users_view; - ALTER TYPE role_type RENAME TO rank_type; - """ - ); - - migrationBuilder.AddColumn( - name: "rank", - table: "users", - type: "rank_type", - nullable: true); - - migrationBuilder.Sql( - """ + + -- Add the rank column back as a temporary nullable text column + ALTER TABLE users ADD rank text; + + -- Migrate roles array values back into a single rank value UPDATE users - SET rank = CAST(( + SET rank = ( CASE WHEN 'system' = ANY(roles) THEN 'system' WHEN 'admin' = ANY(roles) THEN 'admin' @@ -102,22 +88,20 @@ UPDATE users WHEN 'support' = ANY(roles) THEN 'support' ELSE 'user' END - ) AS rank_type); - """ - ); - - migrationBuilder.AlterColumn( - name: "rank", - table: "users", - nullable: false); + ); + + -- Remove the roles column after migration + ALTER TABLE users DROP COLUMN roles; + + -- Restore the old rank_type enum + DROP TYPE role_type; + CREATE TYPE rank_type AS ENUM ('user', 'support', 'staff', 'admin', 'system'); + + -- Change the rank column back to a non-nullable rank_type enum + ALTER TABLE users ALTER COLUMN rank TYPE rank_type USING CAST(rank as rank_type); + ALTER TABLE users ALTER COLUMN rank SET NOT NULL; - migrationBuilder.DropColumn( - name: "roles", - table: "users"); - - // Re-Create the view - migrationBuilder.Sql( - """ + -- Recreate the admin_users_view to restore the original structure CREATE VIEW admin_users_view AS SELECT u.id, diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs index 2e45fe0a..b009a535 100644 --- a/Common/Migrations/OpenShockContextModelSnapshot.cs +++ b/Common/Migrations/OpenShockContextModelSnapshot.cs @@ -26,7 +26,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "ota_update_status", new[] { "started", "running", "finished", "error", "timeout" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "password_encryption_type", new[] { "pbkdf2", "bcrypt_enhanced" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "permission_type", new[] { "shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth" }); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role_type", new[] { "user", "support", "staff", "admin", "system" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role_type", new[] { "support", "staff", "admin", "system" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); diff --git a/Common/Models/RoleType.cs b/Common/Models/RoleType.cs index 1d892a20..1cb64b97 100644 --- a/Common/Models/RoleType.cs +++ b/Common/Models/RoleType.cs @@ -2,9 +2,8 @@ public enum RoleType { - User = 0, - Support = 1, - Staff = 2, - Admin = 3, - System = 4 + Support, + Staff, + Admin, + System } \ No newline at end of file diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index 173ff25a..deb81aa3 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -65,7 +65,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasPostgresEnum("ota_update_status", new[] { "started", "running", "finished", "error", "timeout" }) .HasPostgresEnum("password_encryption_type", new[] { "pbkdf2", "bcrypt_enhanced" }) .HasPostgresEnum("permission_type", new[] { "shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth" }) - .HasPostgresEnum("role_type", new[] { "user", "support", "staff", "admin", "system" }) + .HasPostgresEnum("role_type", new[] { "support", "staff", "admin", "system" }) .HasPostgresEnum("shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR" }) .HasAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False"); From fd2d9fb20fd744f4eb0b3a939a670cc0b21ad863 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 5 Dec 2024 23:11:44 +0100 Subject: [PATCH 4/9] Staff, admins, and system is not support --- Common/Migrations/20241205161255_RanksToRoles.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Common/Migrations/20241205161255_RanksToRoles.cs b/Common/Migrations/20241205161255_RanksToRoles.cs index b4fb5236..48b7c9dc 100644 --- a/Common/Migrations/20241205161255_RanksToRoles.cs +++ b/Common/Migrations/20241205161255_RanksToRoles.cs @@ -23,9 +23,9 @@ UPDATE users SET roles = ( CASE WHEN rank = 'support' THEN ARRAY['support'] - WHEN rank = 'staff' THEN ARRAY['support', 'staff'] - WHEN rank = 'admin' THEN ARRAY['support', 'staff', 'admin'] - WHEN rank = 'system' THEN ARRAY['support', 'staff', 'admin', 'system'] + WHEN rank = 'staff' THEN ARRAY['staff'] + WHEN rank = 'admin' THEN ARRAY['staff', 'admin'] + WHEN rank = 'system' THEN ARRAY['staff', 'admin', 'system'] ELSE ARRAY[]::text[] END ); From 4500ebc5a380aad5a65b478cbf9cd801a3a9e210 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Mon, 3 Feb 2025 23:41:56 +0100 Subject: [PATCH 5/9] Update migration --- ...es.Designer.cs => 20250203224107_RanksToRoles.Designer.cs} | 4 ++-- ...5161255_RanksToRoles.cs => 20250203224107_RanksToRoles.cs} | 4 ++-- Common/Migrations/OpenShockContextModelSnapshot.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename Common/Migrations/{20241205161255_RanksToRoles.Designer.cs => 20250203224107_RanksToRoles.Designer.cs} (99%) rename Common/Migrations/{20241205161255_RanksToRoles.cs => 20250203224107_RanksToRoles.cs} (97%) diff --git a/Common/Migrations/20241205161255_RanksToRoles.Designer.cs b/Common/Migrations/20250203224107_RanksToRoles.Designer.cs similarity index 99% rename from Common/Migrations/20241205161255_RanksToRoles.Designer.cs rename to Common/Migrations/20250203224107_RanksToRoles.Designer.cs index c2e29d96..3c035be9 100644 --- a/Common/Migrations/20241205161255_RanksToRoles.Designer.cs +++ b/Common/Migrations/20250203224107_RanksToRoles.Designer.cs @@ -13,7 +13,7 @@ namespace OpenShock.Common.Migrations { [DbContext(typeof(OpenShockContext))] - [Migration("20241205161255_RanksToRoles")] + [Migration("20250203224107_RanksToRoles")] partial class RanksToRoles { /// @@ -22,7 +22,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False") - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "9.0.1") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_type", new[] { "sound", "vibrate", "shock", "stop" }); diff --git a/Common/Migrations/20241205161255_RanksToRoles.cs b/Common/Migrations/20250203224107_RanksToRoles.cs similarity index 97% rename from Common/Migrations/20241205161255_RanksToRoles.cs rename to Common/Migrations/20250203224107_RanksToRoles.cs index 48b7c9dc..11b30f5c 100644 --- a/Common/Migrations/20241205161255_RanksToRoles.cs +++ b/Common/Migrations/20250203224107_RanksToRoles.cs @@ -24,8 +24,8 @@ UPDATE users CASE WHEN rank = 'support' THEN ARRAY['support'] WHEN rank = 'staff' THEN ARRAY['staff'] - WHEN rank = 'admin' THEN ARRAY['staff', 'admin'] - WHEN rank = 'system' THEN ARRAY['staff', 'admin', 'system'] + WHEN rank = 'admin' THEN ARRAY['admin'] + WHEN rank = 'system' THEN ARRAY['system'] ELSE ARRAY[]::text[] END ); diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs index b009a535..258a7269 100644 --- a/Common/Migrations/OpenShockContextModelSnapshot.cs +++ b/Common/Migrations/OpenShockContextModelSnapshot.cs @@ -19,7 +19,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False") - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "9.0.1") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_type", new[] { "sound", "vibrate", "shock", "stop" }); From 004b70479f6a3e7709af444ad4244653d78e3906 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Mon, 3 Feb 2025 23:50:18 +0100 Subject: [PATCH 6/9] Fix stuffz --- API/Controller/Devices/DeviceOtaController.cs | 2 +- API/Controller/Tokens/TokenController.cs | 10 +++++----- API/Controller/Tokens/TokenSelfController.cs | 2 +- Common/Authentication/OpenShockAuthPolicies.cs | 2 +- Common/OpenShockServiceHelper.cs | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/API/Controller/Devices/DeviceOtaController.cs b/API/Controller/Devices/DeviceOtaController.cs index 4fc4e62a..dd2c025e 100644 --- a/API/Controller/Devices/DeviceOtaController.cs +++ b/API/Controller/Devices/DeviceOtaController.cs @@ -25,7 +25,7 @@ public sealed partial class DevicesController /// Could not find device or you do not have access to it [HttpGet("{deviceId}/ota")] [MapToApiVersion("1")] - [Authorize(Roles = "User")] + [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // DeviceNotFound public async Task GetOtaUpdateHistory([FromRoute] Guid deviceId, [FromServices] IOtaService otaService) diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index a6587891..266ac020 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -25,7 +25,7 @@ public sealed partial class TokensController /// /// All tokens for the current user [HttpGet] - [Authorize(Roles = "User")] + [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task> ListTokens() { @@ -52,7 +52,7 @@ public async Task> ListTokens() /// The token /// The token does not exist or you do not have access to it. [HttpGet("{tokenId}")] - [Authorize(Roles = "User")] + [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task GetTokenById([FromRoute] Guid tokenId) @@ -81,7 +81,7 @@ public async Task GetTokenById([FromRoute] Guid tokenId) /// Successfully deleted token /// The token does not exist or you do not have access to it. [HttpDelete("{tokenId}")] - [Authorize(Roles = "User")] + [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task DeleteToken([FromRoute] Guid tokenId) @@ -105,7 +105,7 @@ public async Task DeleteToken([FromRoute] Guid tokenId) /// /// The created token [HttpPost] - [Authorize(Roles = "User")] + [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task CreateToken([FromBody] CreateTokenRequest body) { @@ -139,7 +139,7 @@ public async Task CreateToken([FromBody] CreateTokenReques /// The edited token /// The token does not exist or you do not have access to it. [HttpPatch("{tokenId}")] - [Authorize(Roles = "User")] + [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task EditToken([FromRoute] Guid tokenId, [FromBody] EditTokenRequest body) diff --git a/API/Controller/Tokens/TokenSelfController.cs b/API/Controller/Tokens/TokenSelfController.cs index 587e3d8f..2ae39f3d 100644 --- a/API/Controller/Tokens/TokenSelfController.cs +++ b/API/Controller/Tokens/TokenSelfController.cs @@ -19,7 +19,7 @@ public sealed partial class TokensController /// /// [HttpGet("self")] - [Authorize(Policy = OpenShockAuthPolicies.TokenSessionOnly)] + [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.ApiToken)] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public TokenResponse GetSelfToken([FromServices] IUserReferenceService userReferenceService) { diff --git a/Common/Authentication/OpenShockAuthPolicies.cs b/Common/Authentication/OpenShockAuthPolicies.cs index 1df116af..1e5f969a 100644 --- a/Common/Authentication/OpenShockAuthPolicies.cs +++ b/Common/Authentication/OpenShockAuthPolicies.cs @@ -2,5 +2,5 @@ public static class OpenShockAuthPolicies { - public const string TokenSessionOnly = "ApiTokenOnly"; + public const string AdminOnly = "AdminOnly"; } diff --git a/Common/OpenShockServiceHelper.cs b/Common/OpenShockServiceHelper.cs index df2fc590..52216218 100644 --- a/Common/OpenShockServiceHelper.cs +++ b/Common/OpenShockServiceHelper.cs @@ -57,7 +57,7 @@ public static ServicesResult AddOpenShockServices(this IServiceCollection servic services.AddAuthorization(options => { - options.AddPolicy(OpenShockAuthPolicies.TokenSessionOnly, policy => policy.RequireClaim(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.ApiToken)); + options.AddPolicy(OpenShockAuthPolicies.AdminOnly, policy => policy.RequireRole("Admin", "System")); // TODO: Add token permission policies }); From 516dc9722d4bfd665fdb0b393b2e26ae0ae7d9f8 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Tue, 4 Feb 2025 01:45:00 +0100 Subject: [PATCH 7/9] Add responses to auth failures --- API/Controller/Tokens/TokenSelfController.cs | 2 +- .../ApiTokenAuthentication.cs | 27 ++++++++++++++++--- .../HubAuthentication.cs | 1 + .../UserSessionAuthentication.cs | 21 +++++++++++++-- .../Authentication/OpenShockAuthPolicies.cs | 5 +++- Common/OpenShockServiceHelper.cs | 5 +++- 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/API/Controller/Tokens/TokenSelfController.cs b/API/Controller/Tokens/TokenSelfController.cs index 2ae39f3d..f746c1e5 100644 --- a/API/Controller/Tokens/TokenSelfController.cs +++ b/API/Controller/Tokens/TokenSelfController.cs @@ -19,7 +19,7 @@ public sealed partial class TokensController /// /// [HttpGet("self")] - [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.ApiToken)] + [Authorize(Policy = OpenShockAuthPolicies.TokenOnly)] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public TokenResponse GetSelfToken([FromServices] IUserReferenceService userReferenceService) { diff --git a/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs b/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs index fd691a78..3c0ce0a0 100644 --- a/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs +++ b/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Authentication; +using System.Net.Mime; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -10,6 +11,7 @@ using System.Security.Claims; using System.Text.Encodings.Web; using System.Text.Json; +using OpenShock.Common.Problems; namespace OpenShock.Common.Authentication.AuthenticationHandlers; @@ -20,6 +22,7 @@ public sealed class ApiTokenAuthentication : AuthenticationHandler options, @@ -42,14 +45,14 @@ protected override async Task HandleAuthenticateAsync() { if (!Context.TryGetApiTokenFromHeader(out var token)) { - return AuthenticateResult.Fail(AuthResultError.HeaderMissingOrInvalid.Title!); + return Fail(AuthResultError.HeaderMissingOrInvalid); } - string tokenHash = HashingUtils.HashSha256(token); + var tokenHash = HashingUtils.HashSha256(token); var tokenDto = await _db.ApiTokens.Include(x => x.User).FirstOrDefaultAsync(x => x.TokenHash == tokenHash && (x.ValidUntil == null || x.ValidUntil >= DateTime.UtcNow)); - if (tokenDto == null) return AuthenticateResult.Fail(AuthResultError.TokenInvalid.Title!); + if (tokenDto == null) return Fail(AuthResultError.TokenInvalid); _batchUpdateService.UpdateTokenLastUsed(tokenDto.Id); _authService.CurrentClient = tokenDto.User; @@ -74,4 +77,20 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Success(ticket); } + + private AuthenticateResult Fail(OpenShockProblem reason) + { + _authResultError = reason; + return AuthenticateResult.Fail(reason.Type!); + } + + /// + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + if (Context.Response.HasStarted) return Task.CompletedTask; + _authResultError ??= AuthResultError.UnknownError; + Response.StatusCode = _authResultError.Status!.Value; + _authResultError.AddContext(Context); + return Context.Response.WriteAsJsonAsync(_authResultError, _serializerOptions, contentType: MediaTypeNames.Application.ProblemJson); + } } \ No newline at end of file diff --git a/Common/Authentication/AuthenticationHandlers/HubAuthentication.cs b/Common/Authentication/AuthenticationHandlers/HubAuthentication.cs index 886d3ede..bfba0d65 100644 --- a/Common/Authentication/AuthenticationHandlers/HubAuthentication.cs +++ b/Common/Authentication/AuthenticationHandlers/HubAuthentication.cs @@ -72,6 +72,7 @@ private AuthenticateResult Fail(OpenShockProblem reason) /// protected override Task HandleChallengeAsync(AuthenticationProperties properties) { + if (Context.Response.HasStarted) return Task.CompletedTask; _authResultError ??= AuthResultError.UnknownError; Response.StatusCode = _authResultError.Status!.Value; _authResultError.AddContext(Context); diff --git a/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs b/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs index af2437f7..ff43ea72 100644 --- a/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs +++ b/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs @@ -25,6 +25,7 @@ public sealed class UserSessionAuthentication : AuthenticationHandler options, @@ -49,11 +50,11 @@ protected override async Task HandleAuthenticateAsync() { if (!Context.TryGetUserSession(out var sessionKey)) { - return AuthenticateResult.Fail(AuthResultError.CookieMissingOrInvalid.Type!); + return Fail(AuthResultError.CookieMissingOrInvalid); } var session = await _sessionService.GetSessionById(sessionKey); - if (session == null) return AuthenticateResult.Fail(AuthResultError.SessionInvalid.Type!); + if (session == null) return Fail(AuthResultError.SessionInvalid); if (session.Expires!.Value < DateTime.UtcNow.Subtract(Duration.LoginSessionExpansionAfter)) { @@ -88,4 +89,20 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Success(ticket); } + + private AuthenticateResult Fail(OpenShockProblem reason) + { + _authResultError = reason; + return AuthenticateResult.Fail(reason.Type!); + } + + /// + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + if (Context.Response.HasStarted) return Task.CompletedTask; + _authResultError ??= AuthResultError.UnknownError; + Response.StatusCode = _authResultError.Status!.Value; + _authResultError.AddContext(Context); + return Context.Response.WriteAsJsonAsync(_authResultError, _serializerOptions, contentType: MediaTypeNames.Application.ProblemJson); + } } \ No newline at end of file diff --git a/Common/Authentication/OpenShockAuthPolicies.cs b/Common/Authentication/OpenShockAuthPolicies.cs index 1e5f969a..f6366b2f 100644 --- a/Common/Authentication/OpenShockAuthPolicies.cs +++ b/Common/Authentication/OpenShockAuthPolicies.cs @@ -2,5 +2,8 @@ public static class OpenShockAuthPolicies { - public const string AdminOnly = "AdminOnly"; + public const string TokenOnly = "TokenOnly"; + public const string UserOnly = "UserOnly"; + + public const string RankAdmin = "AdminOnly"; } diff --git a/Common/OpenShockServiceHelper.cs b/Common/OpenShockServiceHelper.cs index 52216218..65be5e64 100644 --- a/Common/OpenShockServiceHelper.cs +++ b/Common/OpenShockServiceHelper.cs @@ -57,7 +57,10 @@ public static ServicesResult AddOpenShockServices(this IServiceCollection servic services.AddAuthorization(options => { - options.AddPolicy(OpenShockAuthPolicies.AdminOnly, policy => policy.RequireRole("Admin", "System")); + options.AddPolicy(OpenShockAuthPolicies.TokenOnly, policy => policy.RequireClaim(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.ApiToken)); + options.AddPolicy(OpenShockAuthPolicies.UserOnly, policy => policy.RequireClaim(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.UserSessionCookie)); + + options.AddPolicy(OpenShockAuthPolicies.RankAdmin, policy => policy.RequireRole("Admin", "System")); // TODO: Add token permission policies }); From e43fe593d7cc68f8d189549a97a4d06fe6cb0db4 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Wed, 5 Feb 2025 18:18:40 +0100 Subject: [PATCH 8/9] Add response to policy failure --- API/Controller/Devices/DeviceOtaController.cs | 15 +++++++- API/Controller/Tokens/TokenController.cs | 5 --- API/Controller/Tokens/TokenSelfController.cs | 7 +++- API/Controller/Tokens/_ApiController.cs | 7 +--- .../Authentication/OpenShockAuthPolicies.cs | 3 -- ...ockAuthorizationMiddlewareResultHandler.cs | 37 +++++++++++++++++++ Common/Errors/AuthorizationError.cs | 3 ++ Common/OpenShockServiceHelper.cs | 5 +-- .../CustomProblems/PolicyNotMetProblem.cs | 16 ++++++++ 9 files changed, 78 insertions(+), 20 deletions(-) create mode 100644 Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs create mode 100644 Common/Problems/CustomProblems/PolicyNotMetProblem.cs diff --git a/API/Controller/Devices/DeviceOtaController.cs b/API/Controller/Devices/DeviceOtaController.cs index dd2c025e..ca6b13f1 100644 --- a/API/Controller/Devices/DeviceOtaController.cs +++ b/API/Controller/Devices/DeviceOtaController.cs @@ -6,16 +6,28 @@ using Microsoft.EntityFrameworkCore; using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.Attributes; +using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.Errors; using OpenShock.Common.Models; using OpenShock.Common.Models.Services.Ota; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; using OpenShock.Common.Services.Ota; namespace OpenShock.API.Controller.Devices; -public sealed partial class DevicesController +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] +public sealed class DevicesOtaController : AuthenticatedSessionControllerBase { + private readonly OpenShockContext _db; + private readonly ILogger _logger; + + public DevicesOtaController(OpenShockContext db, ILogger logger) + { + _db = db; + _logger = logger; + } + /// /// Gets the OTA update history for a device /// @@ -25,7 +37,6 @@ public sealed partial class DevicesController /// Could not find device or you do not have access to it [HttpGet("{deviceId}/ota")] [MapToApiVersion("1")] - [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // DeviceNotFound public async Task GetOtaUpdateHistory([FromRoute] Guid deviceId, [FromServices] IOtaService otaService) diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index 266ac020..7e6d9b41 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -25,7 +25,6 @@ public sealed partial class TokensController /// /// All tokens for the current user [HttpGet] - [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task> ListTokens() { @@ -52,7 +51,6 @@ public async Task> ListTokens() /// The token /// The token does not exist or you do not have access to it. [HttpGet("{tokenId}")] - [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task GetTokenById([FromRoute] Guid tokenId) @@ -81,7 +79,6 @@ public async Task GetTokenById([FromRoute] Guid tokenId) /// Successfully deleted token /// The token does not exist or you do not have access to it. [HttpDelete("{tokenId}")] - [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task DeleteToken([FromRoute] Guid tokenId) @@ -105,7 +102,6 @@ public async Task DeleteToken([FromRoute] Guid tokenId) /// /// The created token [HttpPost] - [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task CreateToken([FromBody] CreateTokenRequest body) { @@ -139,7 +135,6 @@ public async Task CreateToken([FromBody] CreateTokenReques /// The edited token /// The token does not exist or you do not have access to it. [HttpPatch("{tokenId}")] - [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task EditToken([FromRoute] Guid tokenId, [FromBody] EditTokenRequest body) diff --git a/API/Controller/Tokens/TokenSelfController.cs b/API/Controller/Tokens/TokenSelfController.cs index f746c1e5..c04b5a54 100644 --- a/API/Controller/Tokens/TokenSelfController.cs +++ b/API/Controller/Tokens/TokenSelfController.cs @@ -4,13 +4,17 @@ using OpenShock.API.Models.Response; using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.Attributes; +using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.Authentication.Services; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; namespace OpenShock.API.Controller.Tokens; -public sealed partial class TokensController +[ApiController] +[Route("/{version:apiVersion}/tokens")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.ApiToken)] +public sealed partial class TokensSelfController : AuthenticatedSessionControllerBase { /// /// Gets information about the current token used to access this endpoint @@ -19,7 +23,6 @@ public sealed partial class TokensController /// /// [HttpGet("self")] - [Authorize(Policy = OpenShockAuthPolicies.TokenOnly)] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public TokenResponse GetSelfToken([FromServices] IUserReferenceService userReferenceService) { diff --git a/API/Controller/Tokens/_ApiController.cs b/API/Controller/Tokens/_ApiController.cs index c7b33cfe..1465aa9e 100644 --- a/API/Controller/Tokens/_ApiController.cs +++ b/API/Controller/Tokens/_ApiController.cs @@ -3,23 +3,20 @@ using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.OpenShockDb; -using Redis.OM.Contracts; namespace OpenShock.API.Controller.Tokens; [ApiController] [Route("/{version:apiVersion}/tokens")] -[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] public sealed partial class TokensController : AuthenticatedSessionControllerBase { private readonly OpenShockContext _db; - private readonly IRedisConnectionProvider _redis; private readonly ILogger _logger; - public TokensController(OpenShockContext db, IRedisConnectionProvider redis, ILogger logger) + public TokensController(OpenShockContext db, ILogger logger) { _db = db; - _redis = redis; _logger = logger; } } \ No newline at end of file diff --git a/Common/Authentication/OpenShockAuthPolicies.cs b/Common/Authentication/OpenShockAuthPolicies.cs index f6366b2f..44992c24 100644 --- a/Common/Authentication/OpenShockAuthPolicies.cs +++ b/Common/Authentication/OpenShockAuthPolicies.cs @@ -2,8 +2,5 @@ public static class OpenShockAuthPolicies { - public const string TokenOnly = "TokenOnly"; - public const string UserOnly = "UserOnly"; - public const string RankAdmin = "AdminOnly"; } diff --git a/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs b/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs new file mode 100644 index 00000000..bc4c84ae --- /dev/null +++ b/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs @@ -0,0 +1,37 @@ +using System.Net.Mime; +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.Options; +using OpenShock.Common.Errors; + +namespace OpenShock.Common.Authentication; + +public class OpenShockAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler +{ + private readonly JsonSerializerOptions _serializerOptions; + + private readonly AuthorizationMiddlewareResultHandler + _defaultHandler = new AuthorizationMiddlewareResultHandler(); + + public OpenShockAuthorizationMiddlewareResultHandler(IOptions jsonOptions) + { + _serializerOptions = jsonOptions.Value.SerializerOptions; + } + + public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, + PolicyAuthorizationResult authorizeResult) + { + if (authorizeResult.Forbidden) + { + var failedRequirements = authorizeResult.AuthorizationFailure?.FailedRequirements.Select(x => x.ToString() ?? "error") ?? Array.Empty(); + var problem = AuthorizationError.PolicyNotMet(failedRequirements); + context.Response.StatusCode = problem.Status!.Value; + problem.AddContext(context); + return context.Response.WriteAsJsonAsync(problem, _serializerOptions, contentType: MediaTypeNames.Application.ProblemJson); + } + + return _defaultHandler.HandleAsync(next, context, policy, authorizeResult); + } +} \ No newline at end of file diff --git a/Common/Errors/AuthorizationError.cs b/Common/Errors/AuthorizationError.cs index 25f96f6e..a0d4dc7b 100644 --- a/Common/Errors/AuthorizationError.cs +++ b/Common/Errors/AuthorizationError.cs @@ -1,4 +1,5 @@ using System.Net; +using Microsoft.AspNetCore.Authorization; using OpenShock.Common.Models; using OpenShock.Common.Problems; using OpenShock.Common.Problems.CustomProblems; @@ -20,4 +21,6 @@ public static TokenPermissionProblem TokenPermissionMissing(PermissionType requi IEnumerable grantedPermissions) => new("Authorization.Token.PermissionMissing", $"You do not have the required permission to access this endpoint. Missing permission: {requiredPermission.ToString()}", requiredPermission, grantedPermissions, HttpStatusCode.Forbidden); + + public static PolicyNotMetProblem PolicyNotMet(IEnumerable failedRequirements) => new PolicyNotMetProblem(failedRequirements); } \ No newline at end of file diff --git a/Common/OpenShockServiceHelper.cs b/Common/OpenShockServiceHelper.cs index 65be5e64..f341f440 100644 --- a/Common/OpenShockServiceHelper.cs +++ b/Common/OpenShockServiceHelper.cs @@ -57,13 +57,12 @@ public static ServicesResult AddOpenShockServices(this IServiceCollection servic services.AddAuthorization(options => { - options.AddPolicy(OpenShockAuthPolicies.TokenOnly, policy => policy.RequireClaim(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.ApiToken)); - options.AddPolicy(OpenShockAuthPolicies.UserOnly, policy => policy.RequireClaim(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.UserSessionCookie)); - options.AddPolicy(OpenShockAuthPolicies.RankAdmin, policy => policy.RequireRole("Admin", "System")); // TODO: Add token permission policies }); + services.AddSingleton(); + services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.PropertyNameCaseInsensitive = true; diff --git a/Common/Problems/CustomProblems/PolicyNotMetProblem.cs b/Common/Problems/CustomProblems/PolicyNotMetProblem.cs new file mode 100644 index 00000000..bc98e1ab --- /dev/null +++ b/Common/Problems/CustomProblems/PolicyNotMetProblem.cs @@ -0,0 +1,16 @@ +using System.Net; +using Microsoft.AspNetCore.Authorization; + +namespace OpenShock.Common.Problems.CustomProblems; + +public class PolicyNotMetProblem : OpenShockProblem +{ + public PolicyNotMetProblem(IEnumerable failedRequirements) : base( + "Authorization.Policy.NotMet", + "One or multiple policies were not met", HttpStatusCode.Forbidden, string.Empty) + { + FailedRequirements = failedRequirements; + } + + public IEnumerable FailedRequirements { get; set; } +} \ No newline at end of file From 85294cb5480516f93664a4bae95918f9de04088a Mon Sep 17 00:00:00 2001 From: LucHeart Date: Wed, 5 Feb 2025 19:44:53 +0100 Subject: [PATCH 9/9] Fix admin user view --- Common/OpenShockDb/OpenShockContext.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index deb81aa3..fde75795 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -579,7 +579,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("email_activated"); entity.Property(e => e.Roles) .HasColumnType("role_type[]") - .HasColumnName("roles"); + .HasColumnName("roles") + .HasConversion(x => x.ToArray(), x => x.ToList()); entity.Property(e => e.ApiTokenCount) .HasColumnName("api_token_count"); entity.Property(e => e.PasswordResetCount)