From 3917dda658241e198cf6661b6989f9b33e77176d Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 25 Feb 2024 14:18:41 +0000 Subject: [PATCH] feat: add permission system to User --- OliverBooth/Data/Blog/BlogContext.cs | 1 - OliverBooth/Data/Permission.cs | 37 ++++++++++++++++ OliverBooth/Data/PermissionListConverter.cs | 42 +++++++++++++++++++ .../Web/Configuration/UserConfiguration.cs | 1 + OliverBooth/Data/Web/IUser.cs | 24 +++++++++++ OliverBooth/Data/Web/User.cs | 17 ++++++++ OliverBooth/Data/Web/WebContext.cs | 1 - 7 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 OliverBooth/Data/Permission.cs create mode 100644 OliverBooth/Data/PermissionListConverter.cs diff --git a/OliverBooth/Data/Blog/BlogContext.cs b/OliverBooth/Data/Blog/BlogContext.cs index c20282b..4d9db4f 100644 --- a/OliverBooth/Data/Blog/BlogContext.cs +++ b/OliverBooth/Data/Blog/BlogContext.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using OliverBooth.Data.Blog.Configuration; -using OliverBooth.Data.Web; namespace OliverBooth.Data.Blog; diff --git a/OliverBooth/Data/Permission.cs b/OliverBooth/Data/Permission.cs new file mode 100644 index 0000000..6ad2376 --- /dev/null +++ b/OliverBooth/Data/Permission.cs @@ -0,0 +1,37 @@ +namespace OliverBooth.Data; + +/// +/// Represents a permission. +/// +public struct Permission +{ + /// + /// Represents a permission that grants all scopes. + /// + public static readonly Permission Administrator = new("*"); + + /// + /// Initializes a new instance of the struct. + /// + /// The name of the permission. + /// + /// if the permission is allowed; otherwise, . + /// + public Permission(string name, bool isAllowed = true) + { + Name = name; + IsAllowed = isAllowed; + } + + /// + /// Gets the name of this permission. + /// + /// The name. + public string Name { get; } + + /// + /// Gets a value indicating whether this permission is allowed. + /// + /// if the permission is allowed; otherwise, . + public bool IsAllowed { get; } +} diff --git a/OliverBooth/Data/PermissionListConverter.cs b/OliverBooth/Data/PermissionListConverter.cs new file mode 100644 index 0000000..3dd86a2 --- /dev/null +++ b/OliverBooth/Data/PermissionListConverter.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace OliverBooth.Data; + +internal sealed class PermissionListConverter : ValueConverter, string> +{ + public PermissionListConverter() : this(';') + { + } + + public PermissionListConverter(char separator) : + base(v => ToProvider(v, separator), + s => FromProvider(s, separator)) + { + } + + private static IReadOnlyList FromProvider(string source, char separator = ';') + { + var permissions = new List(); + + foreach (string permission in source.Split(separator)) + { + string name = permission; + var allowed = true; + + if (name.Length > 1 && name[0] == '-') + { + name = name[1..]; + allowed = false; + } + + permissions.Add(new Permission(name, allowed)); + } + + return permissions.AsReadOnly(); + } + + private static string ToProvider(IEnumerable permissions, char separator = ';') + { + return string.Join(separator, permissions.Select(p => $"{(p.IsAllowed ? "-" : "")}{p.Name}")); + } +} diff --git a/OliverBooth/Data/Web/Configuration/UserConfiguration.cs b/OliverBooth/Data/Web/Configuration/UserConfiguration.cs index 5c20fe0..c6be0cc 100644 --- a/OliverBooth/Data/Web/Configuration/UserConfiguration.cs +++ b/OliverBooth/Data/Web/Configuration/UserConfiguration.cs @@ -18,5 +18,6 @@ internal sealed class UserConfiguration : IEntityTypeConfiguration builder.Property(e => e.Salt).HasMaxLength(255).IsRequired(); builder.Property(e => e.Registered).IsRequired(); builder.Property(e => e.Totp); + builder.Property(e => e.Permissions).HasConversion(); } } diff --git a/OliverBooth/Data/Web/IUser.cs b/OliverBooth/Data/Web/IUser.cs index e021546..83f4aa8 100644 --- a/OliverBooth/Data/Web/IUser.cs +++ b/OliverBooth/Data/Web/IUser.cs @@ -35,6 +35,12 @@ public interface IUser /// The registration date and time. DateTimeOffset Registered { get; } + /// + /// Gets the permissions this user is granted. + /// + /// A read-only view of the permissions this user is granted. + IReadOnlyList Permissions { get; } + /// /// Gets the user's TOTP token. /// @@ -48,6 +54,24 @@ public interface IUser /// The URL of the user's avatar. Uri GetAvatarUrl(int size = 28); + /// + /// Determines whether the user has the specified permission. + /// + /// The permission to test. + /// + /// if the user has the specified permission; otherwise, . + /// + bool HasPermission(Permission permission); + + /// + /// Determines whether the user has the specified permission. + /// + /// The permission to test. + /// + /// if the user has the specified permission; otherwise, . + /// + bool HasPermission(string permission); + /// /// Returns a value indicating whether the specified password is valid for the user. /// diff --git a/OliverBooth/Data/Web/User.cs b/OliverBooth/Data/Web/User.cs index d713b0a..c8eaa66 100644 --- a/OliverBooth/Data/Web/User.cs +++ b/OliverBooth/Data/Web/User.cs @@ -24,6 +24,9 @@ internal sealed class User : IUser, IBlogAuthor /// public Guid Id { get; private set; } = Guid.NewGuid(); + /// + public IReadOnlyList Permissions { get; private set; } = ArraySegment.Empty; + /// public DateTimeOffset Registered { get; private set; } = DateTimeOffset.UtcNow; @@ -75,6 +78,20 @@ internal sealed class User : IUser, IBlogAuthor return new Uri($"https://www.gravatar.com/avatar/{builder}?size={size}"); } + /// + public bool HasPermission(Permission permission) + { + return HasPermission(permission.Name); + } + + /// + public bool HasPermission(string permission) + { + return (Permissions.Any(p => p.IsAllowed && p.Name == permission) || + Permissions.Any(p => p is { IsAllowed: true, Name: "*" })) && + !Permissions.Any(p => !p.IsAllowed && p.Name == permission); + } + /// public bool TestCredentials(string password) { diff --git a/OliverBooth/Data/Web/WebContext.cs b/OliverBooth/Data/Web/WebContext.cs index c445a3b..9f884c6 100644 --- a/OliverBooth/Data/Web/WebContext.cs +++ b/OliverBooth/Data/Web/WebContext.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using OliverBooth.Data.Blog.Configuration; using OliverBooth.Data.Web.Configuration; namespace OliverBooth.Data.Web;