From b53140e633a8636a85a142f8790359c21cb4e0a1 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Wed, 11 Jun 2025 21:59:49 +0200 Subject: [PATCH] Implemented basic ui for server sharing. Extracted server authorization. Refactoring and small improvements --- .../Database/ServersDataContext.cs | 1 + .../Controllers/Client/FilesController.cs | 7 +- .../Controllers/Client/PowerController.cs | 7 +- .../Controllers/Client/ServersController.cs | 84 +++++-- .../Controllers/Client/SettingsController.cs | 7 +- .../Controllers/Client/SharesController.cs | 222 +++++++++++++++++- .../Controllers/Client/VariablesController.cs | 7 +- .../ServerAuthFilters/AdminAuthFilter.cs | 33 +++ .../ServerAuthFilters/OwnerAuthFilter.cs | 28 +++ .../ServerAuthFilters/ShareAuthFilter.cs | 49 ++++ .../Interfaces/IServerAuthorizationFilter.cs | 17 ++ .../Models/ServerAuthorizationResult.cs | 28 +++ .../Models/ServerShareContent.cs | 2 + .../MoonlightServers.ApiServer.csproj | 2 - .../Services/ServerAuthorizeService.cs | 93 ++------ .../Services/ServerFileSystemService.cs | 1 - .../Services/StarImportExportService.cs | 1 - .../Startup/PluginStartup.cs | 7 + .../DefaultServerTabProvider.cs | 9 +- MoonlightServers.Frontend/Models/ServerTab.cs | 15 +- .../Services/ServerService.cs | 7 + .../Services/ServerShareService.cs | 34 +++ .../Components/Servers/CreateShareModal.razor | 105 +++++++++ .../UI/Components/Servers/ServerCard.razor | 35 ++- .../Servers/ServerTabs/BaseServerTab.razor | 2 +- .../Servers/ServerTabs/SharesTab.razor | 121 ++++++++++ .../Components/Servers/UpdateShareModal.razor | 101 ++++++++ .../UI/Views/Client/Index.razor | 71 ++++-- .../UI/Views/Client/Manage.razor | 169 ++++++++----- .../Servers/Shares/CreateShareRequest.cs | 12 + .../Servers/Shares/UpdateShareRequest.cs | 8 + .../Client/Servers/ServerDetailResponse.cs | 11 +- .../Servers/Shares/ServerShareResponse.cs | 7 +- .../Models/ServerSharePermission.cs | 4 +- .../MoonlightServers.Shared.csproj | 4 - 35 files changed, 1098 insertions(+), 213 deletions(-) create mode 100644 MoonlightServers.ApiServer/Implementations/ServerAuthFilters/AdminAuthFilter.cs create mode 100644 MoonlightServers.ApiServer/Implementations/ServerAuthFilters/OwnerAuthFilter.cs create mode 100644 MoonlightServers.ApiServer/Implementations/ServerAuthFilters/ShareAuthFilter.cs create mode 100644 MoonlightServers.ApiServer/Interfaces/IServerAuthorizationFilter.cs create mode 100644 MoonlightServers.ApiServer/Models/ServerAuthorizationResult.cs create mode 100644 MoonlightServers.Frontend/Services/ServerShareService.cs create mode 100644 MoonlightServers.Frontend/UI/Components/Servers/CreateShareModal.razor create mode 100644 MoonlightServers.Frontend/UI/Components/Servers/ServerTabs/SharesTab.razor create mode 100644 MoonlightServers.Frontend/UI/Components/Servers/UpdateShareModal.razor create mode 100644 MoonlightServers.Shared/Http/Requests/Client/Servers/Shares/CreateShareRequest.cs create mode 100644 MoonlightServers.Shared/Http/Requests/Client/Servers/Shares/UpdateShareRequest.cs rename {MoonlightServers.ApiServer => MoonlightServers.Shared}/Models/ServerSharePermission.cs (61%) diff --git a/MoonlightServers.ApiServer/Database/ServersDataContext.cs b/MoonlightServers.ApiServer/Database/ServersDataContext.cs index 625fe82..f704c8d 100644 --- a/MoonlightServers.ApiServer/Database/ServersDataContext.cs +++ b/MoonlightServers.ApiServer/Database/ServersDataContext.cs @@ -3,6 +3,7 @@ using MoonCore.Extended.SingleDb; using Moonlight.ApiServer.Configuration; using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Models; +using MoonlightServers.Shared.Models; namespace MoonlightServers.ApiServer.Database; diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs index c0a130f..c208e08 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs @@ -170,7 +170,12 @@ public class FilesController : Controller ); if (!authorizeResult.Succeeded) - throw new HttpApiException("No permission for the requested resource", 403); + { + throw new HttpApiException( + authorizeResult.Message ?? "No permission for the requested resource", + 403 + ); + } return server; } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/PowerController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/PowerController.cs index 19dc6dd..a2ce6c1 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/PowerController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/PowerController.cs @@ -74,7 +74,12 @@ public class PowerController : Controller ); if (!authorizeResult.Succeeded) - throw new HttpApiException("No permission for the requested resource", 403); + { + throw new HttpApiException( + authorizeResult.Message ?? "No permission for the requested resource", + 403 + ); + } return server; } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs index d5553b6..4f0fdc0 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs @@ -13,6 +13,7 @@ using MoonlightServers.ApiServer.Services; using MoonlightServers.Shared.Enums; using MoonlightServers.Shared.Http.Responses.Client.Servers; using MoonlightServers.Shared.Http.Responses.Client.Servers.Allocations; +using MoonlightServers.Shared.Models; namespace MoonlightServers.ApiServer.Http.Controllers.Client; @@ -24,6 +25,7 @@ public class ServersController : Controller private readonly ServerService ServerService; private readonly DatabaseRepository ServerRepository; private readonly DatabaseRepository ShareRepository; + private readonly DatabaseRepository UserRepository; private readonly NodeService NodeService; private readonly ServerAuthorizeService AuthorizeService; @@ -32,7 +34,8 @@ public class ServersController : Controller NodeService nodeService, ServerService serverService, ServerAuthorizeService authorizeService, - DatabaseRepository shareRepository + DatabaseRepository shareRepository, + DatabaseRepository userRepository ) { ServerRepository = serverRepository; @@ -40,6 +43,7 @@ public class ServersController : Controller ServerService = serverService; AuthorizeService = authorizeService; ShareRepository = shareRepository; + UserRepository = userRepository; } [HttpGet] @@ -102,27 +106,46 @@ public class ServersController : Controller var query = ShareRepository .Get() .Include(x => x.Server) - .Where(x => x.UserId == userId) - .Select(x => x.Server); + .ThenInclude(x => x.Node) + .Include(x => x.Server) + .ThenInclude(x => x.Star) + .Include(x => x.Server) + .ThenInclude(x => x.Allocations) + .Where(x => x.UserId == userId); var count = await query.CountAsync(); var items = await query.Skip(page * pageSize).Take(pageSize).ToArrayAsync(); + var ownerIds = items + .Select(x => x.Server.OwnerId) + .Distinct() + .ToArray(); + + var owners = await UserRepository + .Get() + .Where(x => ownerIds.Contains(x.Id)) + .ToArrayAsync(); + var mappedItems = items.Select(x => new ServerDetailResponse() { - Id = x.Id, - Name = x.Name, - NodeName = x.Node.Name, - StarName = x.Star.Name, - Cpu = x.Cpu, - Memory = x.Memory, - Disk = x.Disk, - Allocations = x.Allocations.Select(y => new AllocationDetailResponse() + Id = x.Server.Id, + Name = x.Server.Name, + NodeName = x.Server.Node.Name, + StarName = x.Server.Star.Name, + Cpu = x.Server.Cpu, + Memory = x.Server.Memory, + Disk = x.Server.Disk, + Allocations = x.Server.Allocations.Select(y => new AllocationDetailResponse() { Id = y.Id, Port = y.Port, IpAddress = y.IpAddress - }).ToArray() + }).ToArray(), + Share = new() + { + SharedBy = owners.First(y => y.Id == x.Server.OwnerId).Username, + Permissions = x.Content.Permissions.ToArray() + } }).ToArray(); return new PagedData() @@ -149,11 +172,17 @@ public class ServersController : Controller throw new HttpApiException("No server with this id found", 404); var authorizationResult = await AuthorizeService.Authorize(User, server); - - if (!authorizationResult.Succeeded) - throw new HttpApiException("No server with this id found", 404); - return new ServerDetailResponse() + if (!authorizationResult.Succeeded) + { + throw new HttpApiException( + authorizationResult.Message ?? "No server with this id found", + 404 + ); + } + + // Create mapped response + var response = new ServerDetailResponse() { Id = server.Id, Name = server.Name, @@ -169,6 +198,22 @@ public class ServersController : Controller IpAddress = y.IpAddress }).ToArray() }; + + // Handle requests on shared servers + if (authorizationResult.Share != null) + { + var owner = await UserRepository + .Get() + .FirstAsync(x => x.Id == server.OwnerId); + + response.Share = new() + { + SharedBy = owner.Username, + Permissions = authorizationResult.Share.Content.Permissions.ToArray() + }; + } + + return response; } [HttpGet("{serverId:int}/status")] @@ -261,7 +306,12 @@ public class ServersController : Controller var authorizeResult = await AuthorizeService.Authorize(User, server, filter); if (!authorizeResult.Succeeded) - throw new HttpApiException("No permission for the requested resource", 403); + { + throw new HttpApiException( + authorizeResult.Message ?? "No permission for the requested resource", + 403 + ); + } return server; } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/SettingsController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/SettingsController.cs index 89b0cbf..aa3b24f 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/SettingsController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/SettingsController.cs @@ -53,7 +53,12 @@ public class SettingsController : Controller ); if (!authorizeResult.Succeeded) - throw new HttpApiException("No permission for the requested resource", 403); + { + throw new HttpApiException( + authorizeResult.Message ?? "No permission for the requested resource", + 403 + ); + } return server; } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/SharesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/SharesController.cs index f6441c0..6635e9d 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/SharesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/SharesController.cs @@ -1,5 +1,16 @@ +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MoonCore.Exceptions; +using MoonCore.Extended.Abstractions; +using MoonCore.Models; +using Moonlight.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Services; +using MoonlightServers.Shared.Enums; +using MoonlightServers.Shared.Http.Requests.Client.Servers.Shares; +using MoonlightServers.Shared.Http.Responses.Client.Servers.Shares; namespace MoonlightServers.ApiServer.Http.Controllers.Client; @@ -8,5 +19,214 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client; [Route("api/client/servers")] public class SharesController : Controller { - + private readonly DatabaseRepository ServerRepository; + private readonly DatabaseRepository ShareRepository; + private readonly DatabaseRepository UserRepository; + private readonly ServerAuthorizeService AuthorizeService; + + public SharesController( + DatabaseRepository serverRepository, + DatabaseRepository shareRepository, + DatabaseRepository userRepository, + ServerAuthorizeService authorizeService + ) + { + ServerRepository = serverRepository; + ShareRepository = shareRepository; + UserRepository = userRepository; + AuthorizeService = authorizeService; + } + + [HttpGet("{serverId:int}/shares")] + public async Task> GetAll( + [FromRoute] int serverId, + [FromQuery] [Range(0, int.MaxValue)] int page, + [FromQuery] [Range(1, 100)] int pageSize + ) + { + var server = await GetServerById(serverId); + + var query = ShareRepository + .Get() + .Where(x => x.Server.Id == server.Id); + + var count = await query.CountAsync(); + var items = await query.Skip(page * pageSize).Take(pageSize).ToArrayAsync(); + + var userIds = items + .Select(x => x.UserId) + .Distinct() + .ToArray(); + + var users = await UserRepository + .Get() + .Where(x => userIds.Contains(x.Id)) + .ToArrayAsync(); + + var mappedItems = items.Select(x => new ServerShareResponse() + { + Id = x.Id, + Username = users.First(y => y.Id == x.UserId).Username, + Permissions = x.Content.Permissions.ToArray() + }).ToArray(); + + return new PagedData() + { + Items = mappedItems, + CurrentPage = page, + PageSize = pageSize, + TotalItems = count, + TotalPages = count == 0 ? 0 : count / pageSize + }; + } + + [HttpGet("{serverId:int}/shares/{id:int}")] + public async Task Get( + [FromRoute] int serverId, + [FromRoute] int id + ) + { + var server = await GetServerById(serverId); + + var share = await ShareRepository + .Get() + .FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id); + + if (share == null) + throw new HttpApiException("A share with that id cannot be found", 404); + + var user = await UserRepository + .Get() + .FirstAsync(x => x.Id == share.UserId); + + var mappedItem = new ServerShareResponse() + { + Id = share.Id, + Username = user.Username, + Permissions = share.Content.Permissions.ToArray() + }; + + return mappedItem; + } + + [HttpPost("{serverId:int}/shares")] + public async Task Create( + [FromRoute] int serverId, + [FromBody] CreateShareRequest request + ) + { + var server = await GetServerById(serverId); + + var user = await UserRepository + .Get() + .FirstOrDefaultAsync(x => x.Username == request.Username); + + if (user == null) + throw new HttpApiException("A user with that username could not be found", 400); + + var share = new ServerShare() + { + Server = server, + Content = new() + { + Permissions = request.Permissions + }, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + UserId = user.Id + }; + + var finalShare = await ShareRepository.Add(share); + + var mappedItem = new ServerShareResponse() + { + Id = finalShare.Id, + Username = user.Username, + Permissions = finalShare.Content.Permissions.ToArray() + }; + + return mappedItem; + } + + [HttpPatch("{serverId:int}/shares/{id:int}")] + public async Task Update( + [FromRoute] int serverId, + [FromRoute] int id, + [FromBody] UpdateShareRequest request + ) + { + var server = await GetServerById(serverId); + + var share = await ShareRepository + .Get() + .FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id); + + if (share == null) + throw new HttpApiException("A share with that id cannot be found", 404); + + share.Content.Permissions = request.Permissions; + share.UpdatedAt = DateTime.UtcNow; + + await ShareRepository.Update(share); + + var user = await UserRepository + .Get() + .FirstOrDefaultAsync(x => x.Id == share.UserId); + + if (user == null) + throw new HttpApiException("A user with that id could not be found", 400); + + var mappedItem = new ServerShareResponse() + { + Id = share.Id, + Username = user.Username, + Permissions = share.Content.Permissions.ToArray() + }; + + return mappedItem; + } + + [HttpDelete("{serverId:int}/shares/{id:int}")] + public async Task Delete( + [FromRoute] int serverId, + [FromRoute] int id + ) + { + var server = await GetServerById(serverId); + + var share = await ShareRepository + .Get() + .FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id); + + if (share == null) + throw new HttpApiException("A share with that id cannot be found", 404); + + await ShareRepository.Remove(share); + } + + private async Task GetServerById(int serverId) + { + var server = await ServerRepository + .Get() + .Include(x => x.Node) + .FirstOrDefaultAsync(x => x.Id == serverId); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + var authorizeResult = await AuthorizeService.Authorize( + User, server, + permission => permission is { Name: "shares", Type: >= ServerPermissionType.ReadWrite } + ); + + if (!authorizeResult.Succeeded) + { + throw new HttpApiException( + authorizeResult.Message ?? "No permission for the requested resource", + 403 + ); + } + + return server; + } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/VariablesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/VariablesController.cs index b4f3eab..72e3677 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/VariablesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/VariablesController.cs @@ -138,7 +138,12 @@ public class VariablesController : Controller ); if (!authorizeResult.Succeeded) - throw new HttpApiException("No permission for the requested resource", 403); + { + throw new HttpApiException( + authorizeResult.Message ?? "No permission for the requested resource", + 403 + ); + } return server; } diff --git a/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/AdminAuthFilter.cs b/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/AdminAuthFilter.cs new file mode 100644 index 0000000..06cab67 --- /dev/null +++ b/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/AdminAuthFilter.cs @@ -0,0 +1,33 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using MoonCore.Attributes; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Interfaces; +using MoonlightServers.ApiServer.Models; +using MoonlightServers.Shared.Models; + +namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters; + +public class AdminAuthFilter : IServerAuthorizationFilter +{ + private readonly IAuthorizationService AuthorizationService; + + public AdminAuthFilter(IAuthorizationService authorizationService) + { + AuthorizationService = authorizationService; + } + + public async Task Process( + ClaimsPrincipal user, + Server server, + Func? filter = null + ) + { + var authResult = await AuthorizationService.AuthorizeAsync( + user, + "permissions:admin.servers.manage" + ); + + return authResult.Succeeded ? ServerAuthorizationResult.Success() : null; + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/OwnerAuthFilter.cs b/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/OwnerAuthFilter.cs new file mode 100644 index 0000000..7e33cb1 --- /dev/null +++ b/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/OwnerAuthFilter.cs @@ -0,0 +1,28 @@ +using System.Security.Claims; +using MoonCore.Attributes; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Interfaces; +using MoonlightServers.ApiServer.Models; +using MoonlightServers.Shared.Models; + +namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters; + +public class OwnerAuthFilter : IServerAuthorizationFilter +{ + public Task Process(ClaimsPrincipal user, Server server, Func? filter = null) + { + var userIdValue = user.FindFirstValue("userId"); + + if (string.IsNullOrEmpty(userIdValue)) // This is the case for api keys + return Task.FromResult(null); + + var userId = int.Parse(userIdValue); + + if(server.OwnerId != userId) + return Task.FromResult(null); + + return Task.FromResult( + ServerAuthorizationResult.Success() + ); + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/ShareAuthFilter.cs b/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/ShareAuthFilter.cs new file mode 100644 index 0000000..03dad84 --- /dev/null +++ b/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/ShareAuthFilter.cs @@ -0,0 +1,49 @@ +using System.Security.Claims; +using Microsoft.EntityFrameworkCore; +using MoonCore.Attributes; +using MoonCore.Extended.Abstractions; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Interfaces; +using MoonlightServers.ApiServer.Models; +using MoonlightServers.Shared.Models; + +namespace MoonlightServers.ApiServer.Implementations.ServerAuthFilters; + +public class ShareAuthFilter : IServerAuthorizationFilter +{ + private readonly DatabaseRepository ShareRepository; + + public ShareAuthFilter(DatabaseRepository shareRepository) + { + ShareRepository = shareRepository; + } + + public async Task Process( + ClaimsPrincipal user, + Server server, + Func? filter = null + ) + { + var userIdValue = user.FindFirstValue("userId"); + + if (string.IsNullOrEmpty(userIdValue)) + return null; + + var userId = int.Parse(userIdValue); + + var share = await ShareRepository + .Get() + .FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.UserId == userId); + + if (share == null) + return null; + + if(filter == null) + return ServerAuthorizationResult.Success(share); + + if(share.Content.Permissions.Any(filter)) + return ServerAuthorizationResult.Success(share); + + return null; + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Interfaces/IServerAuthorizationFilter.cs b/MoonlightServers.ApiServer/Interfaces/IServerAuthorizationFilter.cs new file mode 100644 index 0000000..94e9692 --- /dev/null +++ b/MoonlightServers.ApiServer/Interfaces/IServerAuthorizationFilter.cs @@ -0,0 +1,17 @@ +using System.Security.Claims; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Models; +using MoonlightServers.Shared.Models; + +namespace MoonlightServers.ApiServer.Interfaces; + +public interface IServerAuthorizationFilter +{ + // Return null => skip to next filter / handler + // Return any value, instant return + public Task Process( + ClaimsPrincipal user, + Server server, + Func? filter = null + ); +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Models/ServerAuthorizationResult.cs b/MoonlightServers.ApiServer/Models/ServerAuthorizationResult.cs new file mode 100644 index 0000000..5cab9ab --- /dev/null +++ b/MoonlightServers.ApiServer/Models/ServerAuthorizationResult.cs @@ -0,0 +1,28 @@ +using MoonlightServers.ApiServer.Database.Entities; + +namespace MoonlightServers.ApiServer.Models; + +public record ServerAuthorizationResult +{ + public bool Succeeded { get; set; } + public ServerShare? Share { get; set; } + public string? Message { get; set; } + + public static ServerAuthorizationResult Success(ServerShare? share = null) + { + return new() + { + Succeeded = true, + Share = share + }; + } + + public static ServerAuthorizationResult Failed(string? message = null) + { + return new() + { + Succeeded = false, + Message = message + }; + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Models/ServerShareContent.cs b/MoonlightServers.ApiServer/Models/ServerShareContent.cs index be98268..ee0c5ac 100644 --- a/MoonlightServers.ApiServer/Models/ServerShareContent.cs +++ b/MoonlightServers.ApiServer/Models/ServerShareContent.cs @@ -1,3 +1,5 @@ +using MoonlightServers.Shared.Models; + namespace MoonlightServers.ApiServer.Models; public class ServerShareContent diff --git a/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj b/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj index c1227b1..5da5e64 100644 --- a/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj +++ b/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj @@ -29,8 +29,6 @@ - - diff --git a/MoonlightServers.ApiServer/Services/ServerAuthorizeService.cs b/MoonlightServers.ApiServer/Services/ServerAuthorizeService.cs index 07cbf23..f3fb118 100644 --- a/MoonlightServers.ApiServer/Services/ServerAuthorizeService.cs +++ b/MoonlightServers.ApiServer/Services/ServerAuthorizeService.cs @@ -1,103 +1,38 @@ using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; -using Microsoft.EntityFrameworkCore; using MoonCore.Attributes; -using MoonCore.Extended.Abstractions; using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Interfaces; using MoonlightServers.ApiServer.Models; -using MoonlightServers.Shared.Enums; +using MoonlightServers.Shared.Models; namespace MoonlightServers.ApiServer.Services; [Scoped] public class ServerAuthorizeService { - private readonly IAuthorizationService AuthorizationService; - private readonly DatabaseRepository ShareRepository; + private readonly IEnumerable AuthorizationFilters; public ServerAuthorizeService( - IAuthorizationService authorizationService, - DatabaseRepository shareRepository + IEnumerable authorizationFilters ) { - AuthorizationService = authorizationService; - ShareRepository = shareRepository; + AuthorizationFilters = authorizationFilters; } - public async Task Authorize(ClaimsPrincipal user, Server server, Func? filter = null) + public async Task Authorize( + ClaimsPrincipal user, + Server server, + Func? filter = null + ) { - var userIdClaim = user.FindFirst("userId"); - - // User specific authorization - if (userIdClaim != null) + foreach (var authorizationFilter in AuthorizationFilters) { - var result = await AuthorizeViaUser(userIdClaim, server, filter); + var result = await authorizationFilter.Process(user, server, filter); - if (result.Succeeded) + if (result != null) return result; } - // Permission specific authorization - return await AuthorizeViaPermission(user); - } - - private async Task AuthorizeViaUser(Claim userIdClaim, Server server, Func? filter = null) - { - var userId = int.Parse(userIdClaim.Value); - - if (server.OwnerId == userId) - return AuthorizationResult.Success(); - - var possibleShare = await ShareRepository - .Get() - .FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.UserId == userId); - - if (possibleShare == null) - return AuthorizationResult.Failed(); - - // If no filter has been specified every server share is valid - // no matter which permission the share actually has - if (filter == null) - return AuthorizationResult.Success(); - - if(possibleShare.Content.Permissions.Any(filter)) - return AuthorizationResult.Success(); - - return AuthorizationResult.Failed(); - } - - private async Task AuthorizeViaPermission(ClaimsPrincipal user) - { - return await AuthorizationService.AuthorizeAsync( - user, - "permissions:admin.servers.get" - ); - } - - private ServerSharePermission[] ParsePermissions(string permissionsString) - { - var result = new List(); - - var permissions = permissionsString.Split(';', StringSplitOptions.RemoveEmptyEntries); - - foreach (var permission in permissions) - { - var permissionParts = permission.Split(':', StringSplitOptions.RemoveEmptyEntries); - - // Skipped malformed permission parts - if(permissionParts.Length != 2) - continue; - - if(!Enum.TryParse(permissionParts[1], true, out ServerPermissionType permissionType)) - continue; - - result.Add(new() - { - Name = permissionParts[0], - Type = permissionType - }); - } - - return result.ToArray(); + return ServerAuthorizationResult.Failed(); } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Services/ServerFileSystemService.cs b/MoonlightServers.ApiServer/Services/ServerFileSystemService.cs index 2453076..30fc0f3 100644 --- a/MoonlightServers.ApiServer/Services/ServerFileSystemService.cs +++ b/MoonlightServers.ApiServer/Services/ServerFileSystemService.cs @@ -1,4 +1,3 @@ -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MoonCore.Attributes; using MoonCore.Extended.Abstractions; diff --git a/MoonlightServers.ApiServer/Services/StarImportExportService.cs b/MoonlightServers.ApiServer/Services/StarImportExportService.cs index d28d7e6..9b4a335 100644 --- a/MoonlightServers.ApiServer/Services/StarImportExportService.cs +++ b/MoonlightServers.ApiServer/Services/StarImportExportService.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using System.Text.Json.Nodes; using Microsoft.EntityFrameworkCore; using MoonCore.Attributes; using MoonCore.Exceptions; diff --git a/MoonlightServers.ApiServer/Startup/PluginStartup.cs b/MoonlightServers.ApiServer/Startup/PluginStartup.cs index 86227be..ef6bef3 100644 --- a/MoonlightServers.ApiServer/Startup/PluginStartup.cs +++ b/MoonlightServers.ApiServer/Startup/PluginStartup.cs @@ -4,6 +4,8 @@ using Moonlight.ApiServer.Models; using Moonlight.ApiServer.Plugins; using MoonlightServers.ApiServer.Database; using MoonlightServers.ApiServer.Helpers; +using MoonlightServers.ApiServer.Implementations.ServerAuthFilters; +using MoonlightServers.ApiServer.Interfaces; namespace MoonlightServers.ApiServer.Startup; @@ -37,6 +39,11 @@ public class PluginStartup : IPluginStartup Styles = ["css/XtermBlazor.min.css"] }); } + + // Add server auth filters + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); return Task.CompletedTask; } diff --git a/MoonlightServers.Frontend/Implementations/DefaultServerTabProvider.cs b/MoonlightServers.Frontend/Implementations/DefaultServerTabProvider.cs index f0be9e3..814be1c 100644 --- a/MoonlightServers.Frontend/Implementations/DefaultServerTabProvider.cs +++ b/MoonlightServers.Frontend/Implementations/DefaultServerTabProvider.cs @@ -11,10 +11,11 @@ public class DefaultServerTabProvider : IServerTabProvider { ServerTab[] tabs = [ - ServerTab.CreateFromComponent("Console", "console", 0), - ServerTab.CreateFromComponent("Files", "files", 1), - ServerTab.CreateFromComponent("Variables", "variables", 2), - ServerTab.CreateFromComponent("Settings", "settings", 10), + ServerTab.CreateFromComponent("Console", "console", 0, permission => permission.Name == "console"), + ServerTab.CreateFromComponent("Files", "files", 1, permission => permission.Name == "files"), + ServerTab.CreateFromComponent("Shares", "shares", 2, permission => permission.Name == "shares"), + ServerTab.CreateFromComponent("Variables", "variables", 9, permission => permission.Name == "variables"), + ServerTab.CreateFromComponent("Settings", "settings", 10, permission => permission.Name == "settings"), ]; return Task.FromResult(tabs); diff --git a/MoonlightServers.Frontend/Models/ServerTab.cs b/MoonlightServers.Frontend/Models/ServerTab.cs index 9e3126d..82aeae2 100644 --- a/MoonlightServers.Frontend/Models/ServerTab.cs +++ b/MoonlightServers.Frontend/Models/ServerTab.cs @@ -1,22 +1,29 @@ using MoonlightServers.Frontend.UI.Components.Servers.ServerTabs; +using MoonlightServers.Shared.Models; namespace MoonlightServers.Frontend.Models; -public class ServerTab +public record ServerTab { public string Name { get; private set; } public string Path { get; private set; } - public int Priority { get; set; } + public Func? PermissionFilter { get; private set; } + public int Priority { get; private set; } public Type ComponentType { get; private set; } - public static ServerTab CreateFromComponent(string name, string path, int priority) where T : BaseServerTab + public static ServerTab CreateFromComponent( + string name, + string path, + int priority, + Func? filter = null) where T : BaseServerTab { return new() { Name = name, Path = path, Priority = priority, - ComponentType = typeof(T) + ComponentType = typeof(T), + PermissionFilter = filter }; } } \ No newline at end of file diff --git a/MoonlightServers.Frontend/Services/ServerService.cs b/MoonlightServers.Frontend/Services/ServerService.cs index b0ff18b..70bc91d 100644 --- a/MoonlightServers.Frontend/Services/ServerService.cs +++ b/MoonlightServers.Frontend/Services/ServerService.cs @@ -23,6 +23,13 @@ public class ServerService $"api/client/servers?page={page}&pageSize={perPage}" ); } + + public async Task> GetSharedServers(int page, int perPage) + { + return await HttpApiClient.GetJson>( + $"api/client/servers/shared?page={page}&pageSize={perPage}" + ); + } public async Task GetServer(int serverId) { diff --git a/MoonlightServers.Frontend/Services/ServerShareService.cs b/MoonlightServers.Frontend/Services/ServerShareService.cs new file mode 100644 index 0000000..2e4a0ea --- /dev/null +++ b/MoonlightServers.Frontend/Services/ServerShareService.cs @@ -0,0 +1,34 @@ +using MoonCore.Attributes; +using MoonCore.Helpers; +using MoonCore.Models; +using MoonlightServers.Shared.Http.Requests.Client.Servers.Shares; +using MoonlightServers.Shared.Http.Responses.Client.Servers.Shares; + +namespace MoonlightServers.Frontend.Services; + +[Scoped] +public class ServerShareService +{ + private readonly HttpApiClient ApiClient; + + public ServerShareService(HttpApiClient apiClient) + { + ApiClient = apiClient; + } + + public async Task> Get(int id, int page, int pageSize) + => await ApiClient.GetJson>( + $"api/client/servers/{id}/shares?page={page}&pageSize={pageSize}"); + + public async Task Get(int id, int shareId) + => await ApiClient.GetJson($"api/client/servers/{id}/shares/{shareId}"); + + public async Task Create(int id, CreateShareRequest request) + => await ApiClient.PostJson($"api/client/servers/{id}/shares", request); + + public async Task Update(int id, int shareId, UpdateShareRequest request) + => await ApiClient.PatchJson($"api/client/servers/{id}/shares/{shareId}", request); + + public async Task Delete(int id, int shareId) + => await ApiClient.Delete($"api/client/servers/{id}/shares/{shareId}"); +} \ No newline at end of file diff --git a/MoonlightServers.Frontend/UI/Components/Servers/CreateShareModal.razor b/MoonlightServers.Frontend/UI/Components/Servers/CreateShareModal.razor new file mode 100644 index 0000000..55bf752 --- /dev/null +++ b/MoonlightServers.Frontend/UI/Components/Servers/CreateShareModal.razor @@ -0,0 +1,105 @@ +@using MoonCore.Blazor.Tailwind.Components +@using MoonlightServers.Shared.Enums +@using MoonlightServers.Shared.Http.Requests.Client.Servers.Shares +@using MoonlightServers.Shared.Models + +@inherits MoonCore.Blazor.Tailwind.Modals.Components.BaseModal + +
+ Create a new share +
+ + +
+ + +
+
+ @foreach (var name in Names) + { + var i = Permissions.TryGetValue(name, out var permission) ? (int)permission : -1; + +
+ @name +
+ +
+
+ + + +
+
+ } +
+
+ +
+ Cancel + Create +
+ +@code +{ + [Parameter] public string Username { get; set; } + [Parameter] public Func OnSubmit { get; set; } + + private HandleForm HandleForm; + private CreateShareRequest Request; + + private Dictionary Permissions = new(); + + private string[] Names = + [ + "console", + "power", + "shares", + "files", + "variables", + "settings" + ]; + + protected override void OnInitialized() + { + Request = new() + { + Username = Username + }; + } + + private async Task Set(string name, ServerPermissionType type) + { + Permissions[name] = type; + await InvokeAsync(StateHasChanged); + } + + private async Task Reset(string name) + { + Permissions.Remove(name); + await InvokeAsync(StateHasChanged); + } + + private async Task Submit() + => await HandleForm.Submit(); + + private async Task OnValidSubmit() + { + Request.Permissions = Permissions.Select(x => new ServerSharePermission() + { + Name = x.Key, + Type = x.Value + }).ToList(); + + await OnSubmit.Invoke(Request); + await Hide(); + } +} diff --git a/MoonlightServers.Frontend/UI/Components/Servers/ServerCard.razor b/MoonlightServers.Frontend/UI/Components/Servers/ServerCard.razor index d482a12..b8c1576 100644 --- a/MoonlightServers.Frontend/UI/Components/Servers/ServerCard.razor +++ b/MoonlightServers.Frontend/UI/Components/Servers/ServerCard.razor @@ -124,21 +124,34 @@