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;