diff --git a/src/Argon.Api/Controllers/FilesController.cs b/src/Argon.Api/Controllers/FilesController.cs index dd5d683..341a6a9 100644 --- a/src/Argon.Api/Controllers/FilesController.cs +++ b/src/Argon.Api/Controllers/FilesController.cs @@ -12,6 +12,8 @@ namespace Argon.Api.Controllers; using Microsoft.Extensions.Options; using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Argon.Features; +using Contracts.Models.ArchetypeModel; public class FilesController( IOptions cdnOptions, @@ -44,9 +46,11 @@ public async ValueTask Files( [HttpPost("/files/server/{serverId:guid}/avatar"), Authorize(JwtBearerDefaults.AuthenticationScheme)] public async ValueTask UploadServerAvatar([FromRoute] Guid serverId, IFormFile file) { - // TODO - if (!permissions.CanAccess("server.avatar.upload", PropertyBag.Empty.Set(serverId))) + var userId = HttpContext.GetUserId(); + + if (!await permissions.CanAccess(ArgonEntitlement.ManageServer, userId, serverId)) return StatusCode(401); + var assetId = AssetId.Avatar(); var ns = StorageNameSpace.ForServer(serverId); var result = await cdn.CreateAssetAsync(ns, assetId, file); diff --git a/src/Argon.Api/Features/Pex/IPermissionProvider.cs b/src/Argon.Api/Features/Pex/IPermissionProvider.cs index 473eb1c..8074bdc 100644 --- a/src/Argon.Api/Features/Pex/IPermissionProvider.cs +++ b/src/Argon.Api/Features/Pex/IPermissionProvider.cs @@ -1,25 +1,45 @@ namespace Argon.Api.Features.Pex; using ActualLab.Collections; - +using Contracts.Models.ArchetypeModel; +using Entities; +using Microsoft.EntityFrameworkCore; public static class PexFeature { public static IServiceCollection AddArgonPermissions(this WebApplicationBuilder builder) { - builder.Services.AddSingleton(); + builder.Services.AddScoped(); return builder.Services; } } - public interface IPermissionProvider { - public bool CanAccess(string scope, PropertyBag bag); + public ValueTask CanAccess(ArgonEntitlement scope, Guid userId, Guid serverId); } public class NullPermissionProvider : IPermissionProvider { - public bool CanAccess(string scope, PropertyBag bag) - => true; + public ValueTask CanAccess(ArgonEntitlement scope, Guid userId, Guid serverId) + => new(true); +} + +public class ArgonPermissionProvider(ApplicationDbContext ctx) : IPermissionProvider +{ + public async ValueTask CanAccess(ArgonEntitlement scope, Guid userId, Guid serverId) + { + // todo cache + var serverMember = await ctx.UsersToServerRelations + .Include(sm => sm.ServerMemberArchetypes) + .ThenInclude(smr => smr.Archetype) + .FirstOrDefaultAsync(sm => sm.ServerId == serverId && sm.UserId == userId); + + if (serverMember is null) + return false; + + var basePermissions = EntitlementEvaluator.GetBasePermissions(serverMember); + + return (basePermissions & scope) == scope; + } } \ No newline at end of file diff --git a/src/Argon.Contracts/Models/ArchetypeModel/Archetype.cs b/src/Argon.Contracts/Models/ArchetypeModel/Archetype.cs new file mode 100644 index 0000000..2ede6ce --- /dev/null +++ b/src/Argon.Contracts/Models/ArchetypeModel/Archetype.cs @@ -0,0 +1,44 @@ +namespace Argon.Contracts.Models.ArchetypeModel; + +using System.ComponentModel.DataAnnotations; +using System.Drawing; +using MessagePack; + +[MessagePackObject(true)] +public record Archetype : ArgonEntityWithOwnership, IArchetype +{ + public static readonly Guid DefaultArchetype_Everyone + = Guid.Parse("11111111-3333-0000-1111-111111111111"); + public static readonly Guid DefaultArchetype_Owner + = Guid.Parse("11111111-4444-0000-1111-111111111111"); + + [IgnoreMember] + public virtual Server Server { get; set; } + public Guid ServerId { get; set; } + + [MaxLength(64)] + public string Name { get; set; } = string.Empty; + [MaxLength(512)] + public string Description { get; set; } = string.Empty; + + public ArgonEntitlement Entitlement { get; set; } + + public bool IsMentionable { get; set; } + public bool IsLocked { get; set; } + public bool IsHidden { get; set; } + + public Color Colour { get; set; } + [MaxLength(128)] + public string? IconFileId { get; set; } = null; + + public virtual ICollection ServerMemberRoles { get; set; } + = new List(); +} + +public interface IArchetype +{ + Guid Id { get; } + string Name { get; } + + ArgonEntitlement Entitlement { get; } +} \ No newline at end of file diff --git a/src/Argon.Contracts/Models/ArchetypeModel/ArgonEntitlement.cs b/src/Argon.Contracts/Models/ArchetypeModel/ArgonEntitlement.cs new file mode 100644 index 0000000..fff2e80 --- /dev/null +++ b/src/Argon.Contracts/Models/ArchetypeModel/ArgonEntitlement.cs @@ -0,0 +1,76 @@ +namespace Argon.Contracts.Models.ArchetypeModel; + +[Flags] +public enum ArgonEntitlement : ulong +{ + None = 0UL, + + // base entitlement + ViewChannel = 1UL << 0, + ReadHistory = 1UL << 1, + + Base = ViewChannel | ReadHistory, + + // chats entitlement + SendMessages = 1UL << 5, + SendVoice = 1UL << 6, + AttachFiles = 1UL << 7, + AddReactions = 1UL << 8, + AnyMentions = 1UL << 9, + MentionEveryone = 1UL << 10, + ExternalEmoji = 1UL << 11, + ExternalStickers = 1UL << 12, + UseCommands = 1UL << 13, + PostEmbeddedLinks = 1UL << 14, + + + BaseChat = SendMessages | SendVoice | AttachFiles | + AddReactions | AnyMentions | ExternalEmoji | + ExternalStickers | UseCommands | PostEmbeddedLinks, + + // media entitlement + Connect = 1UL << 20, + Speak = 1UL << 21, + Video = 1UL << 22, + Stream = 1UL << 23, + + BaseMedia = Connect | Speak | Video | Stream, + + + BaseMember = Base | BaseChat | BaseMedia, + + + // extended user entitlement + + UseASIO = 1UL << 30, + AdditionalStreams = 1UL << 31, + + BaseExtended = UseASIO | AdditionalStreams, + + // moderation entitlement + DisconnectMember = 1UL << 40, + MoveMember = 1UL << 41, + BanMember = 1UL << 42, + MuteMember = 1UL << 43, + KickMember = 1UL << 44, + + ModerateMembers = DisconnectMember | MoveMember | BanMember | MuteMember | KickMember, + + + // admin entitlement + ManageChannels = 1UL << 50, + ManageArchetype = 1UL << 51, + ManageBots = 1UL << 52, + ManageEvents = 1UL << 53, + ManageBehaviour = 1UL << 54, + ManageServer = 1UL << 55, + + + ControlServer = + ManageChannels | ManageArchetype | + ManageBots | ManageEvents | + ManageBehaviour | ManageServer, + + + Administrator = ulong.MaxValue +} \ No newline at end of file diff --git a/src/Argon.Contracts/Models/ArchetypeModel/EntitlementEvaluator.cs b/src/Argon.Contracts/Models/ArchetypeModel/EntitlementEvaluator.cs new file mode 100644 index 0000000..34f01f5 --- /dev/null +++ b/src/Argon.Contracts/Models/ArchetypeModel/EntitlementEvaluator.cs @@ -0,0 +1,57 @@ +namespace Argon.Contracts.Models.ArchetypeModel; + +public static class EntitlementEvaluator +{ + public static ArgonEntitlement CalculatePermissions(ServerMember member, Guid serverId) + { + if (member.ServerId != serverId) + return ArgonEntitlement.None; + + var permissions = GetBasePermissions(member); + + if (permissions.HasFlag(ArgonEntitlement.Administrator)) + return ArgonEntitlement.Administrator; + return member.ServerMemberArchetypes.Aggregate(ArgonEntitlement.None, (current, smr) => current | smr.Archetype.Entitlement); + } + + public static ArgonEntitlement CalculatePermissions(ServerMember member, Channel channel) + { + var permissions = GetBasePermissions(member); + + if (permissions.HasFlag(ArgonEntitlement.Administrator)) + return ArgonEntitlement.Administrator; + + permissions = ApplyPermissionOverwrites(permissions, member, channel); + + return permissions; + } + + public static ArgonEntitlement GetBasePermissions(ServerMember member) + => member.ServerMemberArchetypes.Aggregate(ArgonEntitlement.None, (current, smr) => current | smr.Archetype.Entitlement); + + public static ArgonEntitlement ApplyPermissionOverwrites(ArgonEntitlement permissions, ServerMember member, Channel channel) + { + var roleOverwrites = channel.EntitlementOverwrites + .Where(po => po.Scope == IArchetypeScope.Archetype) + .Where(po => member.ServerMemberArchetypes.Any(smr => smr.ArchetypeId == po.ArchetypeId)) + .ToList(); + + foreach (var overwrite in roleOverwrites) + { + permissions &= ~overwrite.Deny; + permissions |= overwrite.Allow; + } + + var overwrites = channel.EntitlementOverwrites + .Where(po => po.Scope == IArchetypeScope.Member) + .FirstOrDefault(po => po.ServerMemberId == member.Id); + + if (overwrites == null) + return permissions; + + permissions &= ~overwrites.Deny; + permissions |= overwrites.Allow; + + return permissions; + } +} \ No newline at end of file diff --git a/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeObject.cs b/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeObject.cs new file mode 100644 index 0000000..6f64a27 --- /dev/null +++ b/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeObject.cs @@ -0,0 +1,6 @@ +namespace Argon.Contracts.Models.ArchetypeModel; + +public interface IArchetypeObject +{ + ICollection Overwrites { get; } +} \ No newline at end of file diff --git a/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeOverwrite.cs b/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeOverwrite.cs new file mode 100644 index 0000000..b278e13 --- /dev/null +++ b/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeOverwrite.cs @@ -0,0 +1,9 @@ +namespace Argon.Contracts.Models.ArchetypeModel; + +public interface IArchetypeOverwrite +{ + Guid ChannelId { get; } + IArchetypeScope Scope { get; } + ArgonEntitlement Allow { get; } + ArgonEntitlement Deny { get; } +} \ No newline at end of file diff --git a/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeScope.cs b/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeScope.cs new file mode 100644 index 0000000..3259297 --- /dev/null +++ b/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeScope.cs @@ -0,0 +1,7 @@ +namespace Argon.Contracts.Models.ArchetypeModel; + +public enum IArchetypeScope +{ + Archetype, + Member +} \ No newline at end of file diff --git a/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeSubject.cs b/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeSubject.cs new file mode 100644 index 0000000..2d9211d --- /dev/null +++ b/src/Argon.Contracts/Models/ArchetypeModel/IArchetypeSubject.cs @@ -0,0 +1,6 @@ +namespace Argon.Contracts.Models.ArchetypeModel; +// subject with assigned archetype +public interface IArchetypeSubject +{ + ICollection SubjectArchetypes { get; } +} \ No newline at end of file