From 07728d1048f34e1d048da63684b341ab30bc1d06 Mon Sep 17 00:00:00 2001 From: Jake Mannens Date: Mon, 16 Oct 2023 02:01:27 +1100 Subject: FeedService and AclDialog --- .config/dotnet-tools.json | 2 +- Controllers/LoginController.cs | 1 - LocalPrincipal.cs | 1 + Pages/Component/AclDialog.razor | 93 +++++++++++++++++++-------- Pages/Component/MiniPrincipalSelect.razor | 59 ++++++++++++----- Pages/Component/MiniPrincipalSelect.razor.css | 19 +++++- Pages/Component/Titlebar.razor | 5 +- Pages/ViewMedia.razor | 1 - Pages/_Host.cshtml | 1 + PrincipalProviders/LocalPrincipalProvider.cs | 22 +++++++ SecurityIdentifier.cs | 2 +- Server.csproj | 8 +-- Services/FeedService.cs | 78 ++++++++++++++++++++++ Services/PrincipalProvider.cs | 7 +- Services/SecurityService.cs | 10 ++- wwwroot/js/shake.js | 9 +++ 16 files changed, 253 insertions(+), 65 deletions(-) create mode 100644 Services/FeedService.cs create mode 100644 wwwroot/js/shake.js diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 558293e..96be437 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.10", + "version": "7.0.11", "commands": [ "dotnet-ef" ] diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs index ebadf37..55842d9 100644 --- a/Controllers/LoginController.cs +++ b/Controllers/LoginController.cs @@ -1,7 +1,6 @@ using HyperBooru.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Cryptography.KeyDerivation; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; diff --git a/LocalPrincipal.cs b/LocalPrincipal.cs index f4b9981..6a12bad 100644 --- a/LocalPrincipal.cs +++ b/LocalPrincipal.cs @@ -18,6 +18,7 @@ public class LocalPrincipal : IPrincipal { // displaying the user's name consistently, but may lead to // unexpected behaviour in future when setting the DisplayName // property if that ever becomes possible. + [NotMapped] public string DisplayName { get => Name; set => Name = value; diff --git a/Pages/Component/AclDialog.razor b/Pages/Component/AclDialog.razor index 691e984..c924b98 100644 --- a/Pages/Component/AclDialog.razor +++ b/Pages/Component/AclDialog.razor @@ -1,20 +1,24 @@ @using System.Numerics; @inject HBContext db +@inject ISecurityService securityService @implements IDialog
- @if(obj?.Acl is not null) { - - - - - - - - @foreach(var rule in obj.Acl.Rules.OrderByDescending(r => r.Action)) { + obj.Owner = sid) + @ref=ownerSelect/> +
ActionSubjectPermissions
+ + + + + + @if(obj?.Acl is not null) { + @foreach(var rule in obj.Acl.Rules) { + - } -
ActionSubjectPermissions
@@ -24,24 +28,37 @@
- @rule.Principal.ToString() + @if(rule.Principal != WellKnownSid.NullSid) { + @(securityService.TranslateName(rule.Principal)) + } else { + Select a user/group! + } + + @if(rule.Permissions == 0) { + None + } else { + @GetActivePermissions(rule); + } @GetActivePermissions(rule) EditRule(rule))>🖉 RemoveRule(rule))>✖
-
-
Add new
- } + } + +
+
Add new
@if(ruleToEdit is not null && permissionCheckboxes is not null) { - - var permissions = Acl.GetPermissionDescriptions(obj) + ruleToEdit.Principal = sid) + @ref=subjectSelect/> + var permissions = Acl.GetPermissionDescriptions(obj!) .OrderByDescending(kv => BitOperations.PopCount(kv.Value)) .ThenBy(kv => kv.Value); foreach(var perm in permissionCheckboxes) { @@ -60,7 +77,7 @@ @if(obj?.Acl is not null) { - } @@ -91,7 +108,9 @@ private string? editOwner; private string? editSubject; - private Dialog dialog; + private Dialog dialog; + private MiniPrincipalSelect ownerSelect; + private MiniPrincipalSelect? subjectSelect; public void Show() => Visible = true; public void Hide() => Visible = false; @@ -107,6 +126,8 @@ editOwner = null; CancelEditRule(); + db.ChangeTracker.Clear(); + obj = db.Objects .Include(o => o.Acl) .First(o => o.ObjectId == value.ObjectId); @@ -121,22 +142,29 @@ addedAcl = true; } + ownerSelect.SecurityIdentifier = obj.Owner; lastHashCode = GetAclHashCode(obj.Acl); } } public bool ApplyDisabled => - #if DEBUG - false; - #else GetAclHashCode(obj.Acl!) == lastHashCode || obj.Acl!.Rules.Select(r => r.Principal).Contains(WellKnownSid.NullSid); - #endif + + protected override void OnAfterRender(bool firstRender) { + if(subjectSelect is null || ruleToEdit is null) + return; + if(subjectSelect.SecurityIdentifier is not null) + return; + + subjectSelect.SecurityIdentifier = ruleToEdit.Principal; + StateHasChanged(); + } private string GetActivePermissions(AclRule rule) { var perms = Acl.GetPermissionDescriptions(obj) - .Where(kv => (rule.Permissions & kv.Value) == kv.Value) - .ToList(); + .Where(kv => (rule.Permissions & kv.Value) == kv.Value) + .ToList(); // Filter the list of matching permissions to include the // most relevant encapsulation permissions only. E.g. if @@ -159,6 +187,12 @@ } private void ApplyAcl() { + if(obj.Acl!.Rules.Count() == 0) { + obj.Acl = null; + if(!addedAcl) + db.Remove(obj.Acl!); + } + db.SaveChanges(); Hide(); } @@ -198,14 +232,17 @@ // Special hash function to identify only the elements of // the ACL that may have been changed by the user via this // dialog. - private int GetAclHashCode(Acl acl) => - !acl.Rules.Any() ? 0 : acl.Rules + private int GetAclHashCode(Acl acl) { + var aclHash = !acl.Rules.Any() ? 0 : acl.Rules .Select(r => ( r.Action, r.Permissions, r.Principal.GetHashCode()).GetHashCode()) .Aggregate((a, v) => HashCode.Combine(a, v)); + return HashCode.Combine(aclHash, obj.Owner.GetHashCode()); + } + private class PermissionCheckbox { public string Description { get; private init; } @@ -228,4 +265,4 @@ } } } -} \ No newline at end of file +} diff --git a/Pages/Component/MiniPrincipalSelect.razor b/Pages/Component/MiniPrincipalSelect.razor index 2202b95..89409bd 100644 --- a/Pages/Component/MiniPrincipalSelect.razor +++ b/Pages/Component/MiniPrincipalSelect.razor @@ -1,18 +1,19 @@ @inject ISecurityService securityService +@inject IJSRuntime jsRuntime -
- @if(edit) { +
+ @if(newName is not null) { - - + + } else { - Edit(true))> + @if(SecurityIdentifier is null || SecurityIdentifier == WellKnownSid.NullSid) { Please select a user or group } else { - @securityService.TranslateName(SecurityIdentifier) + @name } } @@ -25,20 +26,44 @@ [Parameter] public EventCallback OnChange { get; set; } - private bool edit = false; - private string name; + private bool edit = false; + private string? name; + private string? newName; - public SecurityIdentifier? SecurityIdentifier { get; set; } + private ElementReference div; - private void Edit(bool enableEdit) { - edit = enableEdit; + private HyperBooru.SecurityIdentifier? securityIdentifier; - if(enableEdit) - name = SecurityIdentifier is null ? "" : - securityService.TranslateName(SecurityIdentifier); + public SecurityIdentifier? SecurityIdentifier { + get => securityIdentifier; + set { + securityIdentifier = value; + name = value is null ? null : securityService.TranslateName(value); + } } - private void Submit() { - Edit(false); + private void Edit() { + newName = SecurityIdentifier == WellKnownSid.NullSid ? "" : name; + StateHasChanged(); } -} \ No newline at end of file + + private async void Cancel() { + newName = null; + await jsRuntime.InvokeVoidAsync("removeClass", div, "bad-principal"); + StateHasChanged(); + } + + private async void Submit() { + var sid = securityService.TranslateName(newName!); + if(sid is null) { + await jsRuntime.InvokeVoidAsync("cycleClass", div, "bad-principal"); + return; + } + + await jsRuntime.InvokeVoidAsync("removeClass", div, "bad-principal"); + SecurityIdentifier = sid; + await OnChange.InvokeAsync(sid); + newName = null; + StateHasChanged(); + } +} diff --git a/Pages/Component/MiniPrincipalSelect.razor.css b/Pages/Component/MiniPrincipalSelect.razor.css index 4b7a217..7e410fc 100644 --- a/Pages/Component/MiniPrincipalSelect.razor.css +++ b/Pages/Component/MiniPrincipalSelect.razor.css @@ -5,14 +5,27 @@ margin-bottom: 16px; } -div * { +div > :not(div) { margin: 0; } -div :not(:last-child) { +div > :not(:last-child) { margin-right: 5px; } -div input[type="text"] { +div > input[type="text"] { flex-grow: 1; } + +div.bad-principal > :not(div) { + animation-iteration-count: 3; + animation-timing-function: linear; + animation: bad-principal 0.2s; +} + +@keyframes bad-principal { + 0% { transform: translateX(0); } + 33% { transform: translateX(-15px); } + 66% { transform: translateX(+15px); } + 100% { transform: translateX(0); } +} diff --git a/Pages/Component/Titlebar.razor b/Pages/Component/Titlebar.razor index 766787a..73700bc 100644 --- a/Pages/Component/Titlebar.razor +++ b/Pages/Component/Titlebar.razor @@ -15,10 +15,7 @@ if(resp.ok) { window.location.href = '/'; } else if(resp.status == 403) { - form.classList.remove('bad-login'); - @* TODO: improve this hacky method of triggering reflow *@ - form.offsetWidth; - form.classList.add('bad-login'); + cycleClass(form, 'bad-login'); inputs.forEach(e => e.value = null); inputs[0].focus(); } else { diff --git a/Pages/ViewMedia.razor b/Pages/ViewMedia.razor index f6a1382..05cf700 100644 --- a/Pages/ViewMedia.razor +++ b/Pages/ViewMedia.razor @@ -83,7 +83,6 @@
- diff --git a/Pages/_Host.cshtml b/Pages/_Host.cshtml index 69bced8..20901d2 100644 --- a/Pages/_Host.cshtml +++ b/Pages/_Host.cshtml @@ -13,6 +13,7 @@ + diff --git a/PrincipalProviders/LocalPrincipalProvider.cs b/PrincipalProviders/LocalPrincipalProvider.cs index 5c27518..723ff81 100644 --- a/PrincipalProviders/LocalPrincipalProvider.cs +++ b/PrincipalProviders/LocalPrincipalProvider.cs @@ -66,6 +66,28 @@ public class LocalPrincipalProvider : PrincipalProvider { return groups.ToArray(); } + public override SecurityIdentifier? TranslateName(string name) { + using var db = dbFactory.CreateDbContext(); + try { + return db.Principals + .First(p => p.Name.ToLower() == name.ToLower().Trim()) + .Sid; + } catch(InvalidOperationException) { + return null; + } + } + + public override string? TranslateName(SecurityIdentifier sid) { + using var db = dbFactory.CreateDbContext(); + try { + return db.Principals + .First(p => p.Sid == sid) + .Name; + } catch(InvalidOperationException) { + return null; + } + } + public override bool ValidatePassword(IUser user, string password) => ((LocalUser) user).PasswordHash == HashPassword(password); diff --git a/SecurityIdentifier.cs b/SecurityIdentifier.cs index 98818e0..608cb75 100644 --- a/SecurityIdentifier.cs +++ b/SecurityIdentifier.cs @@ -17,7 +17,7 @@ public static class WellKnownSid { private static readonly (string name, SecurityIdentifier sid)[] nameMap = new[] { ( "Everyone", WorldSid ), ( "LOCAL", LocalSid ), - ( "CREATOR OWNER", CreatorGroupSid ), + ( "CREATOR OWNER", CreatorOwnerSid ), ( "CREATOR GROUP", CreatorGroupSid ) }; diff --git a/Server.csproj b/Server.csproj index ae08913..f83ab22 100644 --- a/Server.csproj +++ b/Server.csproj @@ -21,16 +21,12 @@ - - - all + runtime; build; native; contentfiles; analyzers; buildtransitive + all - - - diff --git a/Services/FeedService.cs b/Services/FeedService.cs new file mode 100644 index 0000000..c66b9ee --- /dev/null +++ b/Services/FeedService.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore; +using System.Data.Common; + +namespace HyperBooru.Services; + +public enum FeedOrder { + Chronological, + Rating +} + +public interface IFeedService { + public IEnumerable Feed { get; } + + public void InitializeFeed( + FeedOrder order = FeedOrder.Chronological, + bool descending = true, + bool randomPosition = false); +} + +public class FeedService : IFeedService { + private FeedConfiguration? feedConfig; + + private IDbContextFactory dbFactory; + + public FeedService(IDbContextFactory dbFactory) => + this.dbFactory = dbFactory; + + public void InitializeFeed( + FeedOrder order, + bool descending, + bool randomPosition) { + + feedConfig = new() { + Order = order, + Descending = descending, + RandomPosition = randomPosition + }; + } + + public IEnumerable Feed { + get { + if(feedConfig is null) + throw new InvalidOperationException("Feed must be initialized first"); + + while(true) { + var db = dbFactory.CreateDbContext(); + + IOrderedQueryable media; + + switch(feedConfig.Order) { + default: + case FeedOrder.Chronological: + if(feedConfig.Descending) + media = db.Media.OrderByDescending(m => m.ObjectId); + else + media = db.Media.OrderBy(m => m.ObjectId); + break; + } + + Media[] mediaArray = media.Take(50).ToArray(); + + db.Dispose(); + + if(mediaArray.Count() == 0) + break; + + foreach(var m in mediaArray) + yield return m; + } + } + } + + private record FeedConfiguration { + public FeedOrder Order { get; set; } + public bool Descending { get; set; } + public bool RandomPosition { get; set; } + } +} diff --git a/Services/PrincipalProvider.cs b/Services/PrincipalProvider.cs index 4b2cf42..7039379 100644 --- a/Services/PrincipalProvider.cs +++ b/Services/PrincipalProvider.cs @@ -5,6 +5,9 @@ public interface IPrincipalProvider { public IUser? GetUser(string name); public IGroup? GetGroup(string name); + public SecurityIdentifier? TranslateName(string name); + public string? TranslateName(SecurityIdentifier sid); + /// /// Perform a search for any principals whose account name /// matches the search term specified by name. @@ -29,13 +32,15 @@ public abstract class PrincipalProvider : IPrincipalProvider { public abstract IUser? GetUser(string name); public abstract IGroup? GetGroup(string name); + public abstract SecurityIdentifier? TranslateName(string name); + public abstract string? TranslateName(SecurityIdentifier sid); + public abstract IPrincipal[]? SearchPrincipals(string name); public IGroup[] GetGroups(IPrincipal principal) => GetGroups(principal.Sid, false); public IGroup[] GetGroups(IPrincipal principal, bool recurse) => GetGroups(principal.Sid, recurse); - public IGroup[] GetGroups(SecurityIdentifier sid) => GetGroups(sid, false); public abstract IGroup[] GetGroups(SecurityIdentifier sid, bool recurse); diff --git a/Services/SecurityService.cs b/Services/SecurityService.cs index 2d23a58..85513ec 100644 --- a/Services/SecurityService.cs +++ b/Services/SecurityService.cs @@ -80,15 +80,21 @@ public class SecurityService : ISecurityService { if(wellKnownSid is not null) return wellKnownSid; - return sid.SddlForm; + return principalProvider.TranslateName(sid) ?? sid.SddlForm; } public SecurityIdentifier? TranslateName(string name) { + name = name.Trim().ToLower(); + + try { + return new SecurityIdentifier(name.ToUpper()); + } catch(ArgumentException) {} + var wellKnownSid = WellKnownSid.TranslateName(name); if(wellKnownSid is not null) return wellKnownSid; - throw new NotImplementedException(); + return principalProvider.TranslateName(name); } public IEnumerable Filter( diff --git a/wwwroot/js/shake.js b/wwwroot/js/shake.js new file mode 100644 index 0000000..be984ed --- /dev/null +++ b/wwwroot/js/shake.js @@ -0,0 +1,9 @@ +function cycleClass(element, className) { + element.classList.remove(className); + element.offsetWidth; + element.classList.add(className); +} + +function removeClass(element, className) { + element.classList.remove(className); +} -- cgit v1.3