From aad60f08916e5bb21eac1046789c314f998e2605 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Wed, 23 Mar 2022 11:22:50 +0000 Subject: [PATCH] Add support for dynamic permissions --- .../Extensions/CommandContextExtensions.cs | 76 +++++++++++++++- BrackeysBot.API/Permissions/Permission.cs | 91 +++++++++++++++++++ BrackeysBot.API/Permissions/PermissionType.cs | 22 +++++ .../Permissions/RequirePermissionAttribute.cs | 36 ++++++++ BrackeysBot.API/Plugins/IPlugin.cs | 24 +++++ BrackeysBot.API/Plugins/MonoPlugin.cs | 20 ++++ 6 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 BrackeysBot.API/Permissions/Permission.cs create mode 100644 BrackeysBot.API/Permissions/PermissionType.cs create mode 100644 BrackeysBot.API/Permissions/RequirePermissionAttribute.cs diff --git a/BrackeysBot.API/Extensions/CommandContextExtensions.cs b/BrackeysBot.API/Extensions/CommandContextExtensions.cs index 8413d89..b9811dc 100644 --- a/BrackeysBot.API/Extensions/CommandContextExtensions.cs +++ b/BrackeysBot.API/Extensions/CommandContextExtensions.cs @@ -1,5 +1,12 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BrackeysBot.API.Permissions; +using BrackeysBot.API.Plugins; using DisCatSharp.CommandsNext; +using DisCatSharp.CommandsNext.Attributes; +using Microsoft.Extensions.DependencyInjection; namespace BrackeysBot.API.Extensions; @@ -16,4 +23,71 @@ public static Task AcknowledgeAsync(this CommandContext context) { return context.Message.AcknowledgeAsync(); } + + /// + /// Returns a value indicating whether the user which invoked the command has a specified permission. + /// + /// The command context. + /// The permission to validate. + /// + /// if the invoking user has the specified permission; otherwise, . + /// + /// is . + public static bool HasPermission(this CommandContext context, Permission permission) + { + if (permission is null) throw new ArgumentNullException(nameof(permission)); + + PermissionType permissionType = permission.Type; + + if (context.Member is not { } member) + { + if (context.Command.ExecutionChecks.Any(check => check is RequireGuildAttribute)) + return false; + + return permissionType == PermissionType.Everyone || + (permissionType == PermissionType.User && FilterPermissionIds(permission.Ids).Contains(context.User.Id)); + } + + switch (permissionType) + { + case PermissionType.Everyone: + return true; + + case PermissionType.Role: + IEnumerable roleIds = member.Roles.Select(r => r.Id); + return FilterPermissionIds(permission.Ids).Any(id => roleIds.Contains(id)); + + case PermissionType.User: + return FilterPermissionIds(permission.Ids).Contains(member.Id); + + default: + return false; + } + } + + /// + /// Returns a value indicating whether the user which invoked the command has a specified permission. + /// + /// The command context. + /// The permission to validate. + /// + /// if the invoking user has the specified permission; otherwise, . + /// + /// is . + public static bool HasPermission(this CommandContext context, string permissionName) + { + var plugin = context.Services.GetRequiredService(); + Permission? permission = plugin.GetPermission(permissionName); + return permission is not null && context.HasPermission(permission); + } + + private static IEnumerable FilterPermissionIds(IEnumerable source) + { + return source.Select(value => + { + if (value.StartsWith('-') && ulong.TryParse(value[1..], out ulong id)) + return id; + return ulong.TryParse(value, out id) ? id : 0; + }).Where(id => id > 0); + } } diff --git a/BrackeysBot.API/Permissions/Permission.cs b/BrackeysBot.API/Permissions/Permission.cs new file mode 100644 index 0000000..855f6a5 --- /dev/null +++ b/BrackeysBot.API/Permissions/Permission.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json.Serialization; + +namespace BrackeysBot.API.Permissions; + +/// +/// Represents a permission. +/// +public sealed class Permission : IEquatable +{ + [JsonPropertyName("ids")] [JsonInclude] + private string[] _ids; + + [JsonConstructor] + private Permission() + { + _ids = Array.Empty(); + Name = string.Empty; + Type = PermissionType.Everyone; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the permission. + /// The type of the permission. + /// The set of applicable IDs. + public Permission(string name, PermissionType type, params string[] ids) + { + Name = name; + Type = type; + _ids = ids.ToArray(); // defensive copy + } + + /// + /// Gets the set of IDs applicable to this permission. + /// + /// The set of applicable IDs. + public IReadOnlyList Ids => _ids.ToArray(); // defensive copy + + /// + /// Gets the name of the permission. + /// + /// The name of the permission. + [JsonPropertyName("name")] + [JsonInclude] + public string Name { get; private set; } + + /// + /// Gets the type of the permission. + /// + /// The type of the permission. + [JsonPropertyName("type")] + [JsonInclude] + public PermissionType Type { get; private set; } + + /// + public bool Equals(Permission? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Name == other.Name; + } + + public static bool operator ==(Permission? left, Permission? right) + { + if (left is null) return right is null; + return left.Equals(right); + } + + public static bool operator !=(Permission? left, Permission? right) + { + return !(left == right); + } + + /// + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || (obj is Permission other && Equals(other)); + } + + /// + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() + { + return Name.GetHashCode(); + } +} diff --git a/BrackeysBot.API/Permissions/PermissionType.cs b/BrackeysBot.API/Permissions/PermissionType.cs new file mode 100644 index 0000000..b49d5b4 --- /dev/null +++ b/BrackeysBot.API/Permissions/PermissionType.cs @@ -0,0 +1,22 @@ +namespace BrackeysBot.API.Permissions; + +/// +/// An enumeration of permission types. +/// +public enum PermissionType +{ + /// + /// Defines that the permission is granted to everybody. + /// + Everyone, + + /// + /// Defines that the permission is granted to a set of roles. + /// + Role, + + /// + /// Defines that the permission is granted to a set of users. + /// + User +} diff --git a/BrackeysBot.API/Permissions/RequirePermissionAttribute.cs b/BrackeysBot.API/Permissions/RequirePermissionAttribute.cs new file mode 100644 index 0000000..08c54eb --- /dev/null +++ b/BrackeysBot.API/Permissions/RequirePermissionAttribute.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using BrackeysBot.API.Extensions; +using DisCatSharp.CommandsNext; +using DisCatSharp.CommandsNext.Attributes; + +namespace BrackeysBot.API.Permissions; + +/// +/// Defines valid permissions for this command or group. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public sealed class RequirePermissionAttribute : CheckBaseAttribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The permission node name. + public RequirePermissionAttribute(string permissionName) + { + if (string.IsNullOrWhiteSpace(permissionName)) throw new ArgumentNullException(nameof(permissionName)); + PermissionName = permissionName; + } + + /// + /// Gets or sets the name of the permission node. + /// + /// The permission node name. + public string PermissionName { get; } + + /// + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + { + return Task.FromResult(ctx.HasPermission(PermissionName)); + } +} diff --git a/BrackeysBot.API/Plugins/IPlugin.cs b/BrackeysBot.API/Plugins/IPlugin.cs index 4733541..4a1b553 100644 --- a/BrackeysBot.API/Plugins/IPlugin.cs +++ b/BrackeysBot.API/Plugins/IPlugin.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using BrackeysBot.API.Configuration; +using BrackeysBot.API.Permissions; using NLog; namespace BrackeysBot.API.Plugins; @@ -31,6 +33,18 @@ public interface IPlugin : IDisposable, IConfigurationHolder /// The plugin's logger. ILogger Logger { get; } + /// + /// Gets a list of the default permissions for this plugin. + /// + /// The default permissions. + IReadOnlyList PermissionDefaults { get; } + + /// + /// Gets a list of the current permissions for this plugin. + /// + /// The current permissions. + IReadOnlyList Permissions { get; } + /// /// Gets the information about this plugin. /// @@ -48,4 +62,14 @@ public interface IPlugin : IDisposable, IConfigurationHolder /// /// The service provider. public IServiceProvider ServiceProvider { get; } + + /// + /// Gets a permission by its name. + /// + /// The name of the permission. + /// The permission with the specified name, or if no matching permission was found. + /// + /// is , empty, or consists of only whitespace. + /// + Permission? GetPermission(string name); } diff --git a/BrackeysBot.API/Plugins/MonoPlugin.cs b/BrackeysBot.API/Plugins/MonoPlugin.cs index 633e1ae..aac6e86 100644 --- a/BrackeysBot.API/Plugins/MonoPlugin.cs +++ b/BrackeysBot.API/Plugins/MonoPlugin.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.IO; using System.Runtime.Loader; using System.Threading.Tasks; using BrackeysBot.API.Configuration; +using BrackeysBot.API.Permissions; using DisCatSharp; using Microsoft.Extensions.DependencyInjection; using NLog; @@ -34,6 +36,12 @@ public abstract class MonoPlugin : IPlugin /// public ILogger Logger { get; internal set; } = null!; + /// + public IReadOnlyList PermissionDefaults { get; internal set; } = Array.Empty(); + + /// + public IReadOnlyList Permissions { get; internal set; } = Array.Empty(); + /// public PluginInfo PluginInfo { get; internal set; } = null!; @@ -48,6 +56,18 @@ public virtual void Dispose() { } + /// + public Permission? GetPermission(string name) + { + for (var index = 0; index < Permissions.Count; index++) + { + if (string.Equals(name, Permissions[index].Name, StringComparison.Ordinal)) + return Permissions[index]; + } + + return null; + } + /// ~MonoPlugin() {