Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/permissions #60

Merged
merged 2 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/Argon.Api/Controllers/FilesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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> cdnOptions,
Expand Down Expand Up @@ -44,9 +46,11 @@ public async ValueTask<IActionResult> Files(
[HttpPost("/files/server/{serverId:guid}/avatar"), Authorize(JwtBearerDefaults.AuthenticationScheme)]
public async ValueTask<IActionResult> 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);
Expand Down
32 changes: 26 additions & 6 deletions src/Argon.Api/Features/Pex/IPermissionProvider.cs
Original file line number Diff line number Diff line change
@@ -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<IPermissionProvider, NullPermissionProvider>();
builder.Services.AddScoped<IPermissionProvider, ArgonPermissionProvider>();
return builder.Services;
}
}


public interface IPermissionProvider
{
public bool CanAccess(string scope, PropertyBag bag);
public ValueTask<bool> CanAccess(ArgonEntitlement scope, Guid userId, Guid serverId);
}

public class NullPermissionProvider : IPermissionProvider
{
public bool CanAccess(string scope, PropertyBag bag)
=> true;
public ValueTask<bool> CanAccess(ArgonEntitlement scope, Guid userId, Guid serverId)
=> new(true);
}

public class ArgonPermissionProvider(ApplicationDbContext ctx) : IPermissionProvider
{
public async ValueTask<bool> 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;
}
}
44 changes: 44 additions & 0 deletions src/Argon.Contracts/Models/ArchetypeModel/Archetype.cs
Original file line number Diff line number Diff line change
@@ -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<ServerMemberArchetype> ServerMemberRoles { get; set; }
= new List<ServerMemberArchetype>();
}

public interface IArchetype
{
Guid Id { get; }
string Name { get; }

ArgonEntitlement Entitlement { get; }
}
76 changes: 76 additions & 0 deletions src/Argon.Contracts/Models/ArchetypeModel/ArgonEntitlement.cs
Original file line number Diff line number Diff line change
@@ -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
}
57 changes: 57 additions & 0 deletions src/Argon.Contracts/Models/ArchetypeModel/EntitlementEvaluator.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 6 additions & 0 deletions src/Argon.Contracts/Models/ArchetypeModel/IArchetypeObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Argon.Contracts.Models.ArchetypeModel;

public interface IArchetypeObject
{
ICollection<IArchetypeOverwrite> Overwrites { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Argon.Contracts.Models.ArchetypeModel;

public interface IArchetypeOverwrite
{
Guid ChannelId { get; }
IArchetypeScope Scope { get; }
ArgonEntitlement Allow { get; }
ArgonEntitlement Deny { get; }
}
7 changes: 7 additions & 0 deletions src/Argon.Contracts/Models/ArchetypeModel/IArchetypeScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Argon.Contracts.Models.ArchetypeModel;

public enum IArchetypeScope
{
Archetype,
Member
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Argon.Contracts.Models.ArchetypeModel;
// subject with assigned archetype
public interface IArchetypeSubject
{
ICollection<IArchetype> SubjectArchetypes { get; }
}
Loading