diff --git a/MoonlightServers.ApiServer/Database/Entities/ServerShare.cs b/MoonlightServers.ApiServer/Database/Entities/ServerShare.cs index 4955f81..02636cf 100644 --- a/MoonlightServers.ApiServer/Database/Entities/ServerShare.cs +++ b/MoonlightServers.ApiServer/Database/Entities/ServerShare.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations.Schema; +using MoonlightServers.ApiServer.Models; namespace MoonlightServers.ApiServer.Database.Entities; @@ -9,8 +10,7 @@ public class ServerShare public int UserId { get; set; } public Server Server { get; set; } - [Column(TypeName = "jsonb")] - public string Permissions { get; set; } + public ServerShareContent Content { get; set; } = new(); [Column(TypeName="timestamp with time zone")] public DateTime CreatedAt { get; set; } diff --git a/MoonlightServers.ApiServer/Database/Migrations/20250605210823_AddedServerShares.Designer.cs b/MoonlightServers.ApiServer/Database/Migrations/20250606121013_AddedShares.Designer.cs similarity index 89% rename from MoonlightServers.ApiServer/Database/Migrations/20250605210823_AddedServerShares.Designer.cs rename to MoonlightServers.ApiServer/Database/Migrations/20250606121013_AddedShares.Designer.cs index 8abcf77..570c6b5 100644 --- a/MoonlightServers.ApiServer/Database/Migrations/20250605210823_AddedServerShares.Designer.cs +++ b/MoonlightServers.ApiServer/Database/Migrations/20250606121013_AddedShares.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace MoonlightServers.ApiServer.Database.Migrations { [DbContext(typeof(ServersDataContext))] - [Migration("20250605210823_AddedServerShares")] - partial class AddedServerShares + [Migration("20250606121013_AddedShares")] + partial class AddedShares { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -194,10 +194,6 @@ namespace MoonlightServers.ApiServer.Database.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); - b.Property("Permissions") - .IsRequired() - .HasColumnType("jsonb"); - b.Property("ServerId") .HasColumnType("integer"); @@ -434,6 +430,50 @@ namespace MoonlightServers.ApiServer.Database.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.OwnsOne("MoonlightServers.ApiServer.Models.ServerShareContent", "Content", b1 => + { + b1.Property("ServerShareId") + .HasColumnType("integer"); + + b1.HasKey("ServerShareId"); + + b1.ToTable("Servers_ServerShares"); + + b1.ToJson("Content"); + + b1.WithOwner() + .HasForeignKey("ServerShareId"); + + b1.OwnsMany("MoonlightServers.ApiServer.Models.ServerSharePermission", "Permissions", b2 => + { + b2.Property("ServerShareContentServerShareId") + .HasColumnType("integer"); + + b2.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Type") + .HasColumnType("integer"); + + b2.HasKey("ServerShareContentServerShareId", "__synthesizedOrdinal"); + + b2.ToTable("Servers_ServerShares"); + + b2.WithOwner() + .HasForeignKey("ServerShareContentServerShareId"); + }); + + b1.Navigation("Permissions"); + }); + + b.Navigation("Content") + .IsRequired(); + b.Navigation("Server"); }); diff --git a/MoonlightServers.ApiServer/Database/Migrations/20250605210823_AddedServerShares.cs b/MoonlightServers.ApiServer/Database/Migrations/20250606121013_AddedShares.cs similarity index 91% rename from MoonlightServers.ApiServer/Database/Migrations/20250605210823_AddedServerShares.cs rename to MoonlightServers.ApiServer/Database/Migrations/20250606121013_AddedShares.cs index 25d4cfc..fe5dd2c 100644 --- a/MoonlightServers.ApiServer/Database/Migrations/20250605210823_AddedServerShares.cs +++ b/MoonlightServers.ApiServer/Database/Migrations/20250606121013_AddedShares.cs @@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace MoonlightServers.ApiServer.Database.Migrations { /// - public partial class AddedServerShares : Migration + public partial class AddedShares : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -20,9 +20,9 @@ namespace MoonlightServers.ApiServer.Database.Migrations .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), UserId = table.Column(type: "integer", nullable: false), ServerId = table.Column(type: "integer", nullable: false), - Permissions = table.Column(type: "jsonb", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Content = table.Column(type: "jsonb", nullable: false) }, constraints: table => { diff --git a/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs b/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs index 9136078..e136036 100644 --- a/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs +++ b/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs @@ -191,10 +191,6 @@ namespace MoonlightServers.ApiServer.Database.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); - b.Property("Permissions") - .IsRequired() - .HasColumnType("jsonb"); - b.Property("ServerId") .HasColumnType("integer"); @@ -431,6 +427,50 @@ namespace MoonlightServers.ApiServer.Database.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.OwnsOne("MoonlightServers.ApiServer.Models.ServerShareContent", "Content", b1 => + { + b1.Property("ServerShareId") + .HasColumnType("integer"); + + b1.HasKey("ServerShareId"); + + b1.ToTable("Servers_ServerShares"); + + b1.ToJson("Content"); + + b1.WithOwner() + .HasForeignKey("ServerShareId"); + + b1.OwnsMany("MoonlightServers.ApiServer.Models.ServerSharePermission", "Permissions", b2 => + { + b2.Property("ServerShareContentServerShareId") + .HasColumnType("integer"); + + b2.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Type") + .HasColumnType("integer"); + + b2.HasKey("ServerShareContentServerShareId", "__synthesizedOrdinal"); + + b2.ToTable("Servers_ServerShares"); + + b2.WithOwner() + .HasForeignKey("ServerShareContentServerShareId"); + }); + + b1.Navigation("Permissions"); + }); + + b.Navigation("Content") + .IsRequired(); + b.Navigation("Server"); }); diff --git a/MoonlightServers.ApiServer/Database/ServersDataContext.cs b/MoonlightServers.ApiServer/Database/ServersDataContext.cs index b34fbd4..625fe82 100644 --- a/MoonlightServers.ApiServer/Database/ServersDataContext.cs +++ b/MoonlightServers.ApiServer/Database/ServersDataContext.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using MoonCore.Extended.SingleDb; using Moonlight.ApiServer.Configuration; using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Models; namespace MoonlightServers.ApiServer.Database; @@ -30,4 +31,26 @@ public class ServersDataContext : DatabaseContext Database = configuration.Database.Database }; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + #region Shares + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + modelBuilder.Entity(builder => + { + builder.OwnsOne(x => x.Content, navigationBuilder => + { + navigationBuilder.ToJson(); + + navigationBuilder.OwnsMany(x => x.Permissions); + }); + }); + + #endregion + } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs index 9a13198..c0a130f 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs @@ -164,8 +164,13 @@ public class FilesController : Controller if (server == null) throw new HttpApiException("No server with this id found", 404); - if (!await AuthorizeService.Authorize(User, server, permission => permission.Name == "files" && permission.Type >= type)) - throw new HttpApiException("No server with this id found", 404); + var authorizeResult = await AuthorizeService.Authorize( + User, server, + permission => permission.Name == "files" && permission.Type >= type + ); + + if (!authorizeResult.Succeeded) + throw new HttpApiException("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 3f6364d..19dc6dd 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/PowerController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/PowerController.cs @@ -68,9 +68,14 @@ public class PowerController : Controller if (server == null) throw new HttpApiException("No server with this id found", 404); - if (!await AuthorizeService.Authorize(User, server, permission => permission is { Name: "power", Type: ServerPermissionType.ReadWrite })) - throw new HttpApiException("No server with this id found", 404); + var authorizeResult = await AuthorizeService.Authorize( + User, server, + permission => permission.Name == "power" && permission.Type >= ServerPermissionType.ReadWrite + ); + if (!authorizeResult.Succeeded) + throw new HttpApiException("No permission for the requested resource", 403); + return server; } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs index e0ba7ae..d5553b6 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs @@ -148,7 +148,9 @@ public class ServersController : Controller if (server == null) throw new HttpApiException("No server with this id found", 404); - if (!await AuthorizeService.Authorize(User, server)) + var authorizationResult = await AuthorizeService.Authorize(User, server); + + if (!authorizationResult.Succeeded) throw new HttpApiException("No server with this id found", 404); return new ServerDetailResponse() @@ -256,8 +258,10 @@ public class ServersController : Controller if (server == null) throw new HttpApiException("No server with this id found", 404); - if (!await AuthorizeService.Authorize(User, server, filter)) - throw new HttpApiException("No server with this id found", 404); + var authorizeResult = await AuthorizeService.Authorize(User, server, filter); + + if (!authorizeResult.Succeeded) + throw new HttpApiException("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 ca1e7b8..89b0cbf 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/SettingsController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/SettingsController.cs @@ -47,9 +47,14 @@ public class SettingsController : Controller if (server == null) throw new HttpApiException("No server with this id found", 404); - if (!await AuthorizeService.Authorize(User, server, permission => permission is { Name: "settings", Type: ServerPermissionType.ReadWrite })) - throw new HttpApiException("No server with this id found", 404); + var authorizeResult = await AuthorizeService.Authorize( + User, server, + permission => permission is { Name: "settings", Type: >= ServerPermissionType.ReadWrite } + ); + if (!authorizeResult.Succeeded) + throw new HttpApiException("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 ba30513..b4f3eab 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/VariablesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/VariablesController.cs @@ -132,9 +132,13 @@ public class VariablesController : Controller if (server == null) throw new HttpApiException("No server with this id found", 404); - if (!await AuthorizeService.Authorize(User, server, - permission => permission.Name == "variables" && permission.Type >= type)) - throw new HttpApiException("No server with this id found", 404); + var authorizeResult = await AuthorizeService.Authorize( + User, server, + permission => permission.Name == "variables" && permission.Type >= type + ); + + if (!authorizeResult.Succeeded) + throw new HttpApiException("No permission for the requested resource", 403); return server; } diff --git a/MoonlightServers.ApiServer/Models/ServerShareContent.cs b/MoonlightServers.ApiServer/Models/ServerShareContent.cs new file mode 100644 index 0000000..be98268 --- /dev/null +++ b/MoonlightServers.ApiServer/Models/ServerShareContent.cs @@ -0,0 +1,6 @@ +namespace MoonlightServers.ApiServer.Models; + +public class ServerShareContent +{ + public List Permissions { get; set; } = []; +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Services/ServerAuthorizeService.cs b/MoonlightServers.ApiServer/Services/ServerAuthorizeService.cs index a233068..07cbf23 100644 --- a/MoonlightServers.ApiServer/Services/ServerAuthorizeService.cs +++ b/MoonlightServers.ApiServer/Services/ServerAuthorizeService.cs @@ -24,50 +24,54 @@ public class ServerAuthorizeService ShareRepository = shareRepository; } - 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 && await AuthorizeViaUser(userIdClaim, server, filter)) - return true; + if (userIdClaim != null) + { + var result = await AuthorizeViaUser(userIdClaim, server, filter); + + if (result.Succeeded) + return result; + } // Permission specific authorization return await AuthorizeViaPermission(user); } - private async Task AuthorizeViaUser(Claim userIdClaim, Server server, Func? filter = null) + private async Task AuthorizeViaUser(Claim userIdClaim, Server server, Func? filter = null) { var userId = int.Parse(userIdClaim.Value); if (server.OwnerId == userId) - return true; + return AuthorizationResult.Success(); var possibleShare = await ShareRepository .Get() .FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.UserId == userId); if (possibleShare == null) - return false; + 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 true; + return AuthorizationResult.Success(); - var permissionsOfShare = ParsePermissions(possibleShare.Permissions); + if(possibleShare.Content.Permissions.Any(filter)) + return AuthorizationResult.Success(); - return permissionsOfShare.Any(filter); + return AuthorizationResult.Failed(); } - private async Task AuthorizeViaPermission(ClaimsPrincipal user) + private async Task AuthorizeViaPermission(ClaimsPrincipal user) { - var authorizeResult = await AuthorizationService.AuthorizeAsync( + return await AuthorizationService.AuthorizeAsync( user, "permissions:admin.servers.get" ); - - return authorizeResult.Succeeded; } private ServerSharePermission[] ParsePermissions(string permissionsString) @@ -96,34 +100,4 @@ public class ServerAuthorizeService return result.ToArray(); } - - private bool CheckSharePermission(ServerShare share, string permission, ServerPermissionType type) - { - if (string.IsNullOrEmpty(share.Permissions)) - return false; - - var permissions = share.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries); - - foreach (var sharePermission in permissions) - { - if (!sharePermission.StartsWith(permission)) - continue; - - var typeParts = sharePermission.Split(':', StringSplitOptions.RemoveEmptyEntries); - - // Missing permission type - if (typeParts.Length != 2) - return false; - - // Parse type id - if (!int.TryParse(typeParts[1], out var typeId)) - return false; // Malformed - - var requiredId = (int)type; - - return typeId >= requiredId; - } - - return false; - } } \ No newline at end of file