diff --git a/Moonlight.Api/Http/Controllers/Admin/DiagnoseController.cs b/Moonlight.Api/Http/Controllers/Admin/DiagnoseController.cs index 4cc69827..598f6368 100644 --- a/Moonlight.Api/Http/Controllers/Admin/DiagnoseController.cs +++ b/Moonlight.Api/Http/Controllers/Admin/DiagnoseController.cs @@ -1,11 +1,14 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Moonlight.Api.Mappers; using Moonlight.Api.Services; +using Moonlight.Shared; using Moonlight.Shared.Http.Responses.Admin; namespace Moonlight.Api.Http.Controllers.Admin; [ApiController] +[Authorize(Policy = Permissions.System.Diagnose)] [Route("api/admin/system/diagnose")] public class DiagnoseController : Controller { diff --git a/Moonlight.Api/Http/Controllers/Admin/RoleMembersController.cs b/Moonlight.Api/Http/Controllers/Admin/RoleMembersController.cs index a4910dd0..662516a4 100644 --- a/Moonlight.Api/Http/Controllers/Admin/RoleMembersController.cs +++ b/Moonlight.Api/Http/Controllers/Admin/RoleMembersController.cs @@ -1,14 +1,17 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Moonlight.Api.Database; using Moonlight.Api.Database.Entities; using Moonlight.Api.Mappers; +using Moonlight.Shared; using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses.Users; namespace Moonlight.Api.Http.Controllers.Admin; [ApiController] +[Authorize(Policy = Permissions.Roles.Members)] [Route("api/admin/roles/{roleId:int}/members")] public class RoleMembersController : Controller { @@ -59,6 +62,7 @@ public class RoleMembersController : Controller // Pagination var items = query + .OrderBy(x => x.Id) .Skip(startIndex) .Take(length) .ProjectToDto() @@ -100,6 +104,7 @@ public class RoleMembersController : Controller // Pagination var items = query + .OrderBy(x => x.Id) .Skip(startIndex) .Take(length) .ProjectToDto() diff --git a/Moonlight.Api/Http/Controllers/Admin/RolesController.cs b/Moonlight.Api/Http/Controllers/Admin/RolesController.cs index 4df2077b..9e828f9b 100644 --- a/Moonlight.Api/Http/Controllers/Admin/RolesController.cs +++ b/Moonlight.Api/Http/Controllers/Admin/RolesController.cs @@ -1,8 +1,10 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Moonlight.Api.Database; using Moonlight.Api.Database.Entities; using Moonlight.Api.Mappers; +using Moonlight.Shared; using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests.Roles; using Moonlight.Shared.Http.Responses; @@ -22,6 +24,7 @@ public class RolesController : Controller } [HttpGet] + [Authorize(Policy = Permissions.Roles.View)] public async Task>> GetAsync( [FromQuery] int startIndex, [FromQuery] int length, @@ -57,6 +60,7 @@ public class RolesController : Controller // Pagination var data = await query + .OrderBy(x => x.Id) .ProjectToDto() .Skip(startIndex) .Take(length) @@ -68,6 +72,7 @@ public class RolesController : Controller } [HttpGet("{id:int}")] + [Authorize(Policy = Permissions.Roles.View)] public async Task> GetAsync([FromRoute] int id) { var role = await RoleRepository @@ -81,6 +86,7 @@ public class RolesController : Controller } [HttpPost] + [Authorize(Policy = Permissions.Roles.Create)] public async Task> CreateAsync([FromBody] CreateRoleDto request) { var role = RoleMapper.ToEntity(request); @@ -91,6 +97,7 @@ public class RolesController : Controller } [HttpPatch("{id:int}")] + [Authorize(Policy = Permissions.Roles.Edit)] public async Task> UpdateAsync([FromRoute] int id, [FromBody] UpdateRoleDto request) { var role = await RoleRepository @@ -108,6 +115,7 @@ public class RolesController : Controller } [HttpDelete("{id:int}")] + [Authorize(Policy = Permissions.Roles.Delete)] public async Task DeleteAsync([FromRoute] int id) { var role = await RoleRepository diff --git a/Moonlight.Api/Http/Controllers/Admin/SystemController.cs b/Moonlight.Api/Http/Controllers/Admin/SystemController.cs index fba690b9..50d74229 100644 --- a/Moonlight.Api/Http/Controllers/Admin/SystemController.cs +++ b/Moonlight.Api/Http/Controllers/Admin/SystemController.cs @@ -1,5 +1,7 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Moonlight.Api.Services; +using Moonlight.Shared; using Moonlight.Shared.Http.Responses.Admin; namespace Moonlight.Api.Http.Controllers.Admin; @@ -16,6 +18,7 @@ public class SystemController : Controller } [HttpGet("info")] + [Authorize(Policy = Permissions.System.Info)] public async Task> GetInfoAsync() { var cpuUsage = await ApplicationService.GetCpuUsageAsync(); diff --git a/Moonlight.Api/Http/Controllers/Admin/UserActionsController.cs b/Moonlight.Api/Http/Controllers/Admin/UserActionsController.cs new file mode 100644 index 00000000..006e201d --- /dev/null +++ b/Moonlight.Api/Http/Controllers/Admin/UserActionsController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Moonlight.Api.Database; +using Moonlight.Api.Database.Entities; +using Moonlight.Api.Services; +using Moonlight.Shared; + +namespace Moonlight.Api.Http.Controllers.Admin; + +[ApiController] +[Route("api/admin/users/{id:int}")] +public class UserActionsController : Controller +{ + // Consider building a service for deletion and logout or actions in general + + private readonly DatabaseRepository UsersRepository; + private readonly IMemoryCache Cache; + + public UserActionsController(DatabaseRepository usersRepository, IMemoryCache cache) + { + UsersRepository = usersRepository; + Cache = cache; + } + + [HttpPost("logout")] + [Authorize(Policy = Permissions.Users.Logout)] + public async Task LogoutAsync([FromRoute] int id) + { + var user = await UsersRepository + .Query() + .FirstOrDefaultAsync(u => u.Id == id); + + if(user == null) + return Problem("User not found", statusCode: 404); + + user.InvalidateTimestamp = DateTimeOffset.UtcNow; + await UsersRepository.UpdateAsync(user); + + Cache.Remove(string.Format(UserAuthService.ValidationCacheKeyPattern, id)); + + return NoContent(); + } +} \ No newline at end of file diff --git a/Moonlight.Api/Http/Controllers/Admin/UsersController.cs b/Moonlight.Api/Http/Controllers/Admin/UsersController.cs index bb5f03d4..79110804 100644 --- a/Moonlight.Api/Http/Controllers/Admin/UsersController.cs +++ b/Moonlight.Api/Http/Controllers/Admin/UsersController.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Moonlight.Api.Database; using Moonlight.Api.Database.Entities; using Moonlight.Api.Mappers; +using Moonlight.Shared; using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests.Users; using Moonlight.Shared.Http.Responses; @@ -24,6 +25,7 @@ public class UsersController : Controller } [HttpGet] + [Authorize(Policy = Permissions.Users.View)] public async Task>> GetAsync( [FromQuery] int startIndex, [FromQuery] int length, @@ -62,6 +64,7 @@ public class UsersController : Controller // Pagination var data = await query + .OrderBy(x => x.Id) .ProjectToDto() .Skip(startIndex) .Take(length) @@ -73,6 +76,7 @@ public class UsersController : Controller } [HttpGet("{id:int}")] + [Authorize(Policy = Permissions.Users.View)] public async Task> GetAsync([FromRoute] int id) { var user = await UserRepository @@ -86,6 +90,7 @@ public class UsersController : Controller } [HttpPost] + [Authorize(Policy = Permissions.Users.Create)] public async Task> CreateAsync([FromBody] CreateUserDto request) { var user = UserMapper.ToEntity(request); @@ -97,6 +102,7 @@ public class UsersController : Controller } [HttpPatch("{id:int}")] + [Authorize(Policy = Permissions.Users.Edit)] public async Task> UpdateAsync([FromRoute] int id, [FromBody] UpdateUserDto request) { var user = await UserRepository @@ -113,6 +119,7 @@ public class UsersController : Controller } [HttpDelete("{id:int}")] + [Authorize(Policy = Permissions.Users.Delete)] public async Task DeleteAsync([FromRoute] int id) { var user = await UserRepository diff --git a/Moonlight.Api/Implementations/PermissionAuthorizationHandler.cs b/Moonlight.Api/Implementations/PermissionAuthorizationHandler.cs index 86194a87..dd378d57 100644 --- a/Moonlight.Api/Implementations/PermissionAuthorizationHandler.cs +++ b/Moonlight.Api/Implementations/PermissionAuthorizationHandler.cs @@ -1,11 +1,29 @@ using Microsoft.AspNetCore.Authorization; +using Moonlight.Shared; namespace Moonlight.Api.Implementations; public class PermissionAuthorizationHandler : AuthorizationHandler { - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) + protected override Task HandleRequirementAsync( + AuthorizationHandlerContext context, + PermissionRequirement requirement) { - + var permissionClaim = context.User.FindFirst(x => + x.Type.Equals(Permissions.ClaimType, StringComparison.OrdinalIgnoreCase) && + x.Value.Equals(requirement.Identifier, StringComparison.OrdinalIgnoreCase) + ); + + if (permissionClaim == null) + { + context.Fail(new AuthorizationFailureReason( + this, + $"User does not have the requested permission '{requirement.Identifier}'" + )); + return Task.CompletedTask; + } + + context.Succeed(requirement); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/Moonlight.Api/Implementations/PermissionPolicyProvider.cs b/Moonlight.Api/Implementations/PermissionPolicyProvider.cs index 5a310778..217c868e 100644 --- a/Moonlight.Api/Implementations/PermissionPolicyProvider.cs +++ b/Moonlight.Api/Implementations/PermissionPolicyProvider.cs @@ -15,13 +15,11 @@ public class PermissionPolicyProvider : IAuthorizationPolicyProvider public async Task GetPolicyAsync(string policyName) { - if (!policyName.StartsWith("Permission:", StringComparison.OrdinalIgnoreCase)) + if (!policyName.StartsWith(Permissions.Prefix, StringComparison.OrdinalIgnoreCase)) return await FallbackProvider.GetPolicyAsync(policyName); - var identifier = policyName.Substring(Permissions.Prefix.Length); - var policy = new AuthorizationPolicyBuilder(); - policy.AddRequirements(new PermissionRequirement(identifier)); + policy.AddRequirements(new PermissionRequirement(policyName)); return policy.Build(); } diff --git a/Moonlight.Api/Services/UserAuthService.cs b/Moonlight.Api/Services/UserAuthService.cs index 06567234..4a3211a9 100644 --- a/Moonlight.Api/Services/UserAuthService.cs +++ b/Moonlight.Api/Services/UserAuthService.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Options; using Moonlight.Api.Configuration; using Moonlight.Api.Database; using Moonlight.Api.Database.Entities; +using Moonlight.Shared; namespace Moonlight.Api.Services; @@ -18,6 +19,8 @@ public class UserAuthService private const string UserIdClaim = "UserId"; private const string IssuedAtClaim = "IssuedAt"; + + public const string ValidationCacheKeyPattern = $"Moonlight.{nameof(UserAuthService)}.{nameof(ValidateAsync)}-{{0}}"; public UserAuthService( DatabaseRepository userRepository, @@ -93,7 +96,10 @@ public class UserAuthService .Query() .AsNoTracking() .Where(u => u.Id == userId) - .Select(u => new UserSession(u.InvalidateTimestamp)) + .Select(u => new UserSession( + u.InvalidateTimestamp, + u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray()) + ) .FirstOrDefaultAsync(); if (user == null) @@ -122,10 +128,17 @@ public class UserAuthService // everything is fine. If not, it means that the token should be invalidated // as it is too old - return issuedAt > user.InvalidateTimestamp; + if (issuedAt < user.InvalidateTimestamp) + return false; + + principal.Identities.First().AddClaims( + user.Permissions.Select(x => new Claim(Permissions.ClaimType, x)) + ); + + return true; } // A small model which contains data queried per session validation after the defined cache time. // Used for projection - private record UserSession(DateTimeOffset InvalidateTimestamp); + private record UserSession(DateTimeOffset InvalidateTimestamp, string[] Permissions); } \ No newline at end of file diff --git a/Moonlight.Api/Startup/Startup.Auth.cs b/Moonlight.Api/Startup/Startup.Auth.cs index f85c8a9a..35a53b10 100644 --- a/Moonlight.Api/Startup/Startup.Auth.cs +++ b/Moonlight.Api/Startup/Startup.Auth.cs @@ -1,10 +1,12 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Moonlight.Api.Configuration; +using Moonlight.Api.Implementations; using Moonlight.Api.Services; namespace Moonlight.Api.Startup; @@ -80,7 +82,8 @@ public partial class Startup options.GetClaimsFromUserInfoEndpoint = true; }); - builder.Services.AddAuthorization(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); } private static void UseAuth(WebApplication application) diff --git a/Moonlight.Frontend/Implementations/PermissionAuthorizationHandler.cs b/Moonlight.Frontend/Implementations/PermissionAuthorizationHandler.cs new file mode 100644 index 00000000..dbfeb446 --- /dev/null +++ b/Moonlight.Frontend/Implementations/PermissionAuthorizationHandler.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Authorization; +using Moonlight.Shared; + +namespace Moonlight.Frontend.Implementations; + +public class PermissionAuthorizationHandler : AuthorizationHandler +{ + protected override Task HandleRequirementAsync( + AuthorizationHandlerContext context, + PermissionRequirement requirement) + { + var permissionClaim = context.User.FindFirst(x => + x.Type.Equals(Permissions.ClaimType, StringComparison.OrdinalIgnoreCase) && + x.Value.Equals(requirement.Identifier, StringComparison.OrdinalIgnoreCase) + ); + + if (permissionClaim == null) + { + context.Fail(new AuthorizationFailureReason( + this, + $"User does not have the requested permission '{requirement.Identifier}'" + )); + return Task.CompletedTask; + } + + context.Succeed(requirement); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight.Frontend/Implementations/PermissionPolicyProvider.cs b/Moonlight.Frontend/Implementations/PermissionPolicyProvider.cs new file mode 100644 index 00000000..f5df4924 --- /dev/null +++ b/Moonlight.Frontend/Implementations/PermissionPolicyProvider.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using Moonlight.Shared; + +namespace Moonlight.Frontend.Implementations; + +public class PermissionPolicyProvider : IAuthorizationPolicyProvider +{ + private readonly DefaultAuthorizationPolicyProvider FallbackProvider; + + public PermissionPolicyProvider(IOptions options) + { + FallbackProvider = new DefaultAuthorizationPolicyProvider(options); + } + + public async Task GetPolicyAsync(string policyName) + { + if (!policyName.StartsWith(Permissions.Prefix, StringComparison.OrdinalIgnoreCase)) + return await FallbackProvider.GetPolicyAsync(policyName); + + var policy = new AuthorizationPolicyBuilder(); + policy.AddRequirements(new PermissionRequirement(policyName)); + + return policy.Build(); + } + + public Task GetDefaultPolicyAsync() + => FallbackProvider.GetDefaultPolicyAsync(); + + public Task GetFallbackPolicyAsync() + => FallbackProvider.GetFallbackPolicyAsync(); +} + +public class PermissionRequirement : IAuthorizationRequirement +{ + public string Identifier { get; } + + public PermissionRequirement(string identifier) + { + Identifier = identifier; + } +} \ No newline at end of file diff --git a/Moonlight.Frontend/Implementations/PermissionProvider.cs b/Moonlight.Frontend/Implementations/PermissionProvider.cs index 47d39669..22a09991 100644 --- a/Moonlight.Frontend/Implementations/PermissionProvider.cs +++ b/Moonlight.Frontend/Implementations/PermissionProvider.cs @@ -10,11 +10,23 @@ public sealed class PermissionProvider : IPermissionProvider public Task GetPermissionsAsync() { return Task.FromResult([ - new PermissionCategory("User Management", typeof(UsersRoundIcon), [ - new Permission(Permissions.Admin.Users.Create, "Create", "Create new users"), - new Permission(Permissions.Admin.Users.View, "View", "View all users"), - new Permission(Permissions.Admin.Users.Edit, "Edit", "Edit user details"), - new Permission(Permissions.Admin.Users.Delete, "Delete", "Delete user accounts"), + new PermissionCategory("Users", typeof(UserRoundIcon), [ + new Permission(Permissions.Users.Create, "Create", "Create new users"), + new Permission(Permissions.Users.View, "View", "View all users"), + new Permission(Permissions.Users.Edit, "Edit", "Edit user details"), + new Permission(Permissions.Users.Delete, "Delete", "Delete user accounts"), + new Permission(Permissions.Users.Logout, "Logout", "Logout user accounts"), + ]), + new PermissionCategory("Roles", typeof(UsersRoundIcon), [ + new Permission(Permissions.Roles.Create, "Create", "Create new roles"), + new Permission(Permissions.Roles.View, "View", "View all roles"), + new Permission(Permissions.Roles.Edit, "Edit", "Edit role details"), + new Permission(Permissions.Roles.Delete, "Delete", "Delete role accounts"), + new Permission(Permissions.Roles.Members, "Members", "Manage role members"), + ]), + new PermissionCategory("System", typeof(CogIcon), [ + new Permission(Permissions.System.Info, "Info", "View system info"), + new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"), ]), ]); } diff --git a/Moonlight.Frontend/Implementations/SidebarProvider.cs b/Moonlight.Frontend/Implementations/SidebarProvider.cs index 98d729b2..8cd52510 100644 --- a/Moonlight.Frontend/Implementations/SidebarProvider.cs +++ b/Moonlight.Frontend/Implementations/SidebarProvider.cs @@ -1,6 +1,7 @@ using LucideBlazor; using Moonlight.Frontend.Interfaces; using Moonlight.Frontend.Models; +using Moonlight.Shared; namespace Moonlight.Frontend.Implementations; @@ -24,7 +25,8 @@ public sealed class SidebarProvider : ISidebarProvider Path = "/admin", IsExactPath = true, Group = "Admin", - Order = 0 + Order = 0, + Policy = Permissions.System.Info }, new() { @@ -33,7 +35,8 @@ public sealed class SidebarProvider : ISidebarProvider Path = "/admin/users", IsExactPath = false, Group = "Admin", - Order = 10 + Order = 10, + Policy = Permissions.Users.View }, new() { @@ -42,7 +45,8 @@ public sealed class SidebarProvider : ISidebarProvider Path = "/admin/system", IsExactPath = false, Group = "Admin", - Order = 20 + Order = 20, + Policy = Permissions.System.Info } ]); } diff --git a/Moonlight.Frontend/Models/SidebarItem.cs b/Moonlight.Frontend/Models/SidebarItem.cs index 613870ca..66c73ec7 100644 --- a/Moonlight.Frontend/Models/SidebarItem.cs +++ b/Moonlight.Frontend/Models/SidebarItem.cs @@ -14,4 +14,6 @@ public record SidebarItem // need it to work properly [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public Type IconType { get; init; } + + public string? Policy { get; set; } } \ No newline at end of file diff --git a/Moonlight.Frontend/Startup/Startup.Auth.cs b/Moonlight.Frontend/Startup/Startup.Auth.cs index 0b2f0038..0e64c73b 100644 --- a/Moonlight.Frontend/Startup/Startup.Auth.cs +++ b/Moonlight.Frontend/Startup/Startup.Auth.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; @@ -16,5 +17,8 @@ public partial class Startup builder.Services.AddCascadingAuthenticationState(); builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); } } \ No newline at end of file diff --git a/Moonlight.Frontend/UI/Admin/Modals/ManageRoleMembersDialog.razor b/Moonlight.Frontend/UI/Admin/Modals/ManageRoleMembersDialog.razor index d71fc0fa..6abb099a 100644 --- a/Moonlight.Frontend/UI/Admin/Modals/ManageRoleMembersDialog.razor +++ b/Moonlight.Frontend/UI/Admin/Modals/ManageRoleMembersDialog.razor @@ -49,9 +49,11 @@ - - - +
+ + + +
diff --git a/Moonlight.Frontend/UI/Admin/Views/Sys/Diagnose.razor b/Moonlight.Frontend/UI/Admin/Views/Sys/Diagnose.razor index 4187c446..0cac7f81 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Sys/Diagnose.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Sys/Diagnose.razor @@ -1,4 +1,7 @@ @using LucideBlazor +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Moonlight.Shared @using Moonlight.Shared.Http.Responses.Admin @using ShadcnBlazor.Accordions @using ShadcnBlazor.Alerts @@ -9,6 +12,7 @@ @using ShadcnBlazor.Spinners @inject HttpClient HttpClient +@inject IAuthorizationService AuthorizationService
@@ -40,7 +44,7 @@ - + Start diagnostics @@ -216,10 +220,21 @@ @code { + [CascadingParameter] public Task AuthState { get; set; } + + private AuthorizationResult AccessResult; + private bool IsLoading = false; private bool HasDiagnosed = false; private DiagnoseResultDto[] Entries; + protected override async Task OnInitializedAsync() + { + var authState = await AuthState; + + AccessResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Diagnose); + } + private async Task DiagnoseAsync() { IsLoading = true; diff --git a/Moonlight.Frontend/UI/Admin/Views/Users/Index.razor b/Moonlight.Frontend/UI/Admin/Views/Users/Index.razor index 40a218ca..0578535a 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Users/Index.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Users/Index.razor @@ -1,9 +1,13 @@ @page "/admin/users" @using LucideBlazor +@using Microsoft.AspNetCore.Authorization +@using Moonlight.Shared @using ShadcnBlazor.Tab @inject NavigationManager Navigation +@attribute [Authorize(Policy = Permissions.Users.View)] + diff --git a/Moonlight.Frontend/UI/Admin/Views/Users/Roles.razor b/Moonlight.Frontend/UI/Admin/Views/Users/Roles.razor index 61190202..7d7753ce 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Users/Roles.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Users/Roles.razor @@ -1,5 +1,8 @@ @using LucideBlazor +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization @using Moonlight.Frontend.UI.Admin.Modals +@using Moonlight.Shared @using Moonlight.Shared.Http.Requests @using Moonlight.Shared.Http.Requests.Roles @using Moonlight.Shared.Http.Responses @@ -16,6 +19,7 @@ @inject DialogService DialogService @inject ToastService ToastService @inject AlertDialogService AlertDialogService +@inject IAuthorizationService AuthorizationService
@@ -25,7 +29,7 @@
- @@ -63,20 +67,21 @@ - + Members - + Edit + Variant="DropdownMenuItemVariant.Destructive" + Disabled="@(!DeleteAccess.Succeeded)"> Delete @@ -93,8 +98,25 @@ @code { + [CascadingParameter] public Task AuthState { get; set; } + private DataGrid Grid; + private AuthorizationResult MembersAccess; + private AuthorizationResult EditAccess; + private AuthorizationResult DeleteAccess; + private AuthorizationResult CreateAccess; + + protected override async Task OnInitializedAsync() + { + var authState = await AuthState; + + MembersAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Roles.Members); + EditAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Roles.Edit); + DeleteAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Roles.Delete); + CreateAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Roles.Create); + } + private async Task> LoadAsync(DataGridRequest request) { var query = $"?startIndex={request.StartIndex}&length={request.Length}"; @@ -147,6 +169,12 @@ private async Task MembersAsync(RoleDto role) { + if (!MembersAccess.Succeeded) + { + await ToastService.ErrorAsync("Permission denied", "You dont have the required permission to manage members"); + return; + } + await DialogService.LaunchAsync(parameters => { parameters[nameof(ManageRoleMembersDialog.Role)] = role; }, model => { model.ClassName = "sm:max-w-xl"; }); } @@ -157,7 +185,9 @@ $"Do you really want to delete the role {role.Name} with {role.MemberCount} members? This action cannot be undone", async () => { - await HttpClient.DeleteAsync($"api/admin/roles/{role.Id}"); + var response = await HttpClient.DeleteAsync($"api/admin/roles/{role.Id}"); + response.EnsureSuccessStatusCode(); + await ToastService.SuccessAsync("User deletion", $"Successfully deleted role {role.Name}"); await Grid.RefreshAsync(); diff --git a/Moonlight.Frontend/UI/Admin/Views/Users/Users.razor b/Moonlight.Frontend/UI/Admin/Views/Users/Users.razor index a106d65f..55f49383 100644 --- a/Moonlight.Frontend/UI/Admin/Views/Users/Users.razor +++ b/Moonlight.Frontend/UI/Admin/Views/Users/Users.razor @@ -1,5 +1,8 @@ @using LucideBlazor +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization @using Moonlight.Frontend.UI.Admin.Modals +@using Moonlight.Shared @using ShadcnBlazor.Buttons @using ShadcnBlazor.DataGrids @using ShadcnBlazor.Dropdowns @@ -16,6 +19,7 @@ @inject AlertDialogService AlertDialogService @inject DialogService DialogService @inject ToastService ToastService +@inject IAuthorizationService AuthorizationService
@@ -25,7 +29,7 @@
- @@ -46,19 +50,28 @@ - - + + Logout + + + + + Edit - + Delete @@ -75,7 +88,24 @@ @code { + [CascadingParameter] public Task AuthState { get; set; } + private DataGrid Grid; + + private AuthorizationResult LogoutAccess; + private AuthorizationResult EditAccess; + private AuthorizationResult DeleteAccess; + private AuthorizationResult CreateAccess; + + protected override async Task OnInitializedAsync() + { + var authState = await AuthState; + + LogoutAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Users.Logout); + EditAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Users.Edit); + DeleteAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Users.Delete); + CreateAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Users.Create); + } private async Task> LoadAsync(DataGridRequest request) { @@ -141,11 +171,30 @@ "Do you really want to delete this user? This action cannot be undone", async () => { - await HttpClient.DeleteAsync($"api/admin/users/{user.Id}"); + var response = await HttpClient.DeleteAsync($"api/admin/users/{user.Id}"); + response.EnsureSuccessStatusCode(); + await ToastService.SuccessAsync("User deletion", $"Successfully deleted user {user.Username}"); await Grid.RefreshAsync(); } ); } + + private async Task LogoutAsync(UserDto user) + { + await AlertDialogService.ConfirmDangerAsync( + $"Logout all session of user {user.Username}", + "Do you really want to logout all session of this user? This action cannot be undone", + async () => + { + var response = await HttpClient.PostAsync($"api/admin/users/{user.Id}/logout", null); + response.EnsureSuccessStatusCode(); + + await ToastService.SuccessAsync("User logout", $"Successfully logged out all session of user {user.Username}"); + + await Grid.RefreshAsync(); + } + ); + } } \ No newline at end of file diff --git a/Moonlight.Frontend/UI/App.razor b/Moonlight.Frontend/UI/App.razor index f05f6fd4..f9c663dd 100644 --- a/Moonlight.Frontend/UI/App.razor +++ b/Moonlight.Frontend/UI/App.razor @@ -1,4 +1,5 @@ -@using LucideBlazor +@using System.Net +@using LucideBlazor @using Microsoft.AspNetCore.Components.Authorization @using Moonlight.Frontend.UI.Shared @using ShadcnBlazor.Emptys @@ -35,20 +36,27 @@ -
- - - - - - - Critical Application Error - - - @context.ToString() - - - -
+ @if (context is HttpRequestException { StatusCode: HttpStatusCode.Unauthorized }) + { + + } + else + { +
+ + + + + + + Critical Application Error + + + @context.ToString() + + + +
+ }
\ No newline at end of file diff --git a/Moonlight.Frontend/UI/Shared/Partials/AppSidebar.razor b/Moonlight.Frontend/UI/Shared/Partials/AppSidebar.razor index 0fbc9d2a..2112841d 100644 --- a/Moonlight.Frontend/UI/Shared/Partials/AppSidebar.razor +++ b/Moonlight.Frontend/UI/Shared/Partials/AppSidebar.razor @@ -1,8 +1,11 @@ -@using Moonlight.Frontend.Interfaces +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Moonlight.Frontend.Interfaces @using Moonlight.Frontend.Models @using ShadcnBlazor.Sidebars @inject NavigationManager Navigation +@inject IAuthorizationService AuthorizationService @inject IEnumerable Providers @implements IDisposable @@ -68,15 +71,30 @@ @code { + [CascadingParameter] public Task AuthState { get; set; } + private readonly List Items = new(); protected override async Task OnInitializedAsync() { + var authState = await AuthState; + foreach (var provider in Providers) { - Items.AddRange( - await provider.GetItemsAsync() - ); + var items = await provider.GetItemsAsync(); + + foreach (var item in items) + { + if (!string.IsNullOrWhiteSpace(item.Policy)) + { + var result = await AuthorizationService.AuthorizeAsync(authState.User, item.Policy); + + if(!result.Succeeded) + continue; + } + + Items.Add(item); + } } Navigation.LocationChanged += OnLocationChanged; diff --git a/Moonlight.Shared/Permissions.cs b/Moonlight.Shared/Permissions.cs index fd532c7c..4c773f9a 100644 --- a/Moonlight.Shared/Permissions.cs +++ b/Moonlight.Shared/Permissions.cs @@ -5,16 +5,33 @@ public static class Permissions public const string Prefix = "Permissions:"; public const string ClaimType = "Permissions"; - public static class Admin + public static class Users { - public static class Users - { - private const string Section = "Users"; + private const string Section = "Users"; - public const string View = $"{Prefix}{Section}.View"; - public const string Edit = $"{Prefix}{Section}.Edit"; - public const string Create = $"{Prefix}{Section}.Create"; - public const string Delete = $"{Prefix}{Section}.Delete"; - } + public const string View = $"{Prefix}{Section}.{nameof(View)}"; + public const string Edit = $"{Prefix}{Section}.{nameof(Edit)}"; + public const string Create = $"{Prefix}{Section}.{nameof(Create)}"; + public const string Delete = $"{Prefix}{Section}.{nameof(Delete)}"; + public const string Logout = $"{Prefix}{Section}.{nameof(Logout)}"; + } + + public static class Roles + { + private const string Section = "Roles"; + + public const string View = $"{Prefix}{Section}.{nameof(View)}"; + public const string Edit = $"{Prefix}{Section}.{nameof(Edit)}"; + public const string Create = $"{Prefix}{Section}.{nameof(Create)}"; + public const string Delete = $"{Prefix}{Section}.{nameof(Delete)}"; + public const string Members = $"{Prefix}{Section}.{nameof(Members)}"; + } + + public static class System + { + private const string Section = "System"; + + public const string Info = $"{Prefix}{Section}.{nameof(Info)}"; + public const string Diagnose = $"{Prefix}{Section}.{nameof(Diagnose)}"; } } \ No newline at end of file