feat: add permission system to User

This commit is contained in:
Oliver Booth 2024-02-25 14:18:41 +00:00
parent 9a447db891
commit 3917dda658
Signed by: oliverbooth
GPG Key ID: E60B570D1B7557B5
7 changed files with 121 additions and 2 deletions

View File

@ -1,6 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Blog.Configuration; using OliverBooth.Data.Blog.Configuration;
using OliverBooth.Data.Web;
namespace OliverBooth.Data.Blog; namespace OliverBooth.Data.Blog;

View File

@ -0,0 +1,37 @@
namespace OliverBooth.Data;
/// <summary>
/// Represents a permission.
/// </summary>
public struct Permission
{
/// <summary>
/// Represents a permission that grants all scopes.
/// </summary>
public static readonly Permission Administrator = new("*");
/// <summary>
/// Initializes a new instance of the <see cref="Permission" /> struct.
/// </summary>
/// <param name="name">The name of the permission.</param>
/// <param name="isAllowed">
/// <see langword="true" /> if the permission is allowed; otherwise, <see langword="false" />.
/// </param>
public Permission(string name, bool isAllowed = true)
{
Name = name;
IsAllowed = isAllowed;
}
/// <summary>
/// Gets the name of this permission.
/// </summary>
/// <value>The name.</value>
public string Name { get; }
/// <summary>
/// Gets a value indicating whether this permission is allowed.
/// </summary>
/// <value><see langword="true" /> if the permission is allowed; otherwise, <see langword="false" />.</value>
public bool IsAllowed { get; }
}

View File

@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace OliverBooth.Data;
internal sealed class PermissionListConverter : ValueConverter<IReadOnlyList<Permission>, string>
{
public PermissionListConverter() : this(';')
{
}
public PermissionListConverter(char separator) :
base(v => ToProvider(v, separator),
s => FromProvider(s, separator))
{
}
private static IReadOnlyList<Permission> FromProvider(string source, char separator = ';')
{
var permissions = new List<Permission>();
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<Permission> permissions, char separator = ';')
{
return string.Join(separator, permissions.Select(p => $"{(p.IsAllowed ? "-" : "")}{p.Name}"));
}
}

View File

@ -18,5 +18,6 @@ internal sealed class UserConfiguration : IEntityTypeConfiguration<User>
builder.Property(e => e.Salt).HasMaxLength(255).IsRequired(); builder.Property(e => e.Salt).HasMaxLength(255).IsRequired();
builder.Property(e => e.Registered).IsRequired(); builder.Property(e => e.Registered).IsRequired();
builder.Property(e => e.Totp); builder.Property(e => e.Totp);
builder.Property(e => e.Permissions).HasConversion<PermissionListConverter>();
} }
} }

View File

@ -35,6 +35,12 @@ public interface IUser
/// <value>The registration date and time.</value> /// <value>The registration date and time.</value>
DateTimeOffset Registered { get; } DateTimeOffset Registered { get; }
/// <summary>
/// Gets the permissions this user is granted.
/// </summary>
/// <value>A read-only view of the permissions this user is granted.</value>
IReadOnlyList<Permission> Permissions { get; }
/// <summary> /// <summary>
/// Gets the user's TOTP token. /// Gets the user's TOTP token.
/// </summary> /// </summary>
@ -48,6 +54,24 @@ public interface IUser
/// <returns>The URL of the user's avatar.</returns> /// <returns>The URL of the user's avatar.</returns>
Uri GetAvatarUrl(int size = 28); Uri GetAvatarUrl(int size = 28);
/// <summary>
/// Determines whether the user has the specified permission.
/// </summary>
/// <param name="permission">The permission to test.</param>
/// <returns>
/// <see langword="true" /> if the user has the specified permission; otherwise, <see langword="false" />.
/// </returns>
bool HasPermission(Permission permission);
/// <summary>
/// Determines whether the user has the specified permission.
/// </summary>
/// <param name="permission">The permission to test.</param>
/// <returns>
/// <see langword="true" /> if the user has the specified permission; otherwise, <see langword="false" />.
/// </returns>
bool HasPermission(string permission);
/// <summary> /// <summary>
/// Returns a value indicating whether the specified password is valid for the user. /// Returns a value indicating whether the specified password is valid for the user.
/// </summary> /// </summary>

View File

@ -24,6 +24,9 @@ internal sealed class User : IUser, IBlogAuthor
/// <inheritdoc cref="IUser.Id" /> /// <inheritdoc cref="IUser.Id" />
public Guid Id { get; private set; } = Guid.NewGuid(); public Guid Id { get; private set; } = Guid.NewGuid();
/// <inheritdoc />
public IReadOnlyList<Permission> Permissions { get; private set; } = ArraySegment<Permission>.Empty;
/// <inheritdoc /> /// <inheritdoc />
public DateTimeOffset Registered { get; private set; } = DateTimeOffset.UtcNow; 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}"); return new Uri($"https://www.gravatar.com/avatar/{builder}?size={size}");
} }
/// <inheritdoc />
public bool HasPermission(Permission permission)
{
return HasPermission(permission.Name);
}
/// <inheritdoc />
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);
}
/// <inheritdoc /> /// <inheritdoc />
public bool TestCredentials(string password) public bool TestCredentials(string password)
{ {

View File

@ -1,5 +1,4 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Blog.Configuration;
using OliverBooth.Data.Web.Configuration; using OliverBooth.Data.Web.Configuration;
namespace OliverBooth.Data.Web; namespace OliverBooth.Data.Web;