diff options
| -rw-r--r-- | HBContext.cs | 2 | ||||
| -rw-r--r-- | HBObject.cs | 2 | ||||
| -rw-r--r-- | Pages/Component/AclActionSwitch.razor | 20 | ||||
| -rw-r--r-- | Pages/Component/AclActionSwitch.razor.css | 48 | ||||
| -rw-r--r-- | Pages/Component/AclDialog.razor | 97 | ||||
| -rw-r--r-- | Pages/Component/AclDialog.razor.css | 88 | ||||
| -rw-r--r-- | Pages/Component/Dialog.razor | 32 | ||||
| -rw-r--r-- | Pages/Component/Dialog.razor.css | 1 | ||||
| -rw-r--r-- | Pages/Component/Switch.razor.css | 8 | ||||
| -rw-r--r-- | Pages/ViewMedia.razor | 1 | ||||
| -rw-r--r-- | PrincipalProviders/LocalPrincipalProvider.cs | 8 | ||||
| -rw-r--r-- | SecurityIdentifier.cs | 79 | ||||
| -rw-r--r-- | Services/PrincipalProvider.cs | 17 | ||||
| -rw-r--r-- | wwwroot/styles/global.css | 9 |
14 files changed, 359 insertions, 53 deletions
diff --git a/HBContext.cs b/HBContext.cs index 2e78b5a..dee100d 100644 --- a/HBContext.cs +++ b/HBContext.cs @@ -57,7 +57,7 @@ public class HBContext : DbContext { modelBuilder.Entity<HBObject>() .Property(o => o.Owner) - .HasDefaultValue(new SecurityIdentifier(WellKnownSidType.WorldSid)); + .HasDefaultValue(WellKnownSid.WorldSid); // Seed internal tag definitions // These should NEVER change diff --git a/HBObject.cs b/HBObject.cs index df60ed0..eecb3f5 100644 --- a/HBObject.cs +++ b/HBObject.cs @@ -15,5 +15,5 @@ public class HBObject { [ForeignKey("AclId")] public Acl? Acl { get; set; } public SecurityIdentifier Owner { get; set; } = - new SecurityIdentifier(WellKnownSidType.WorldSid); + WellKnownSid.WorldSid; }
\ No newline at end of file diff --git a/Pages/Component/AclActionSwitch.razor b/Pages/Component/AclActionSwitch.razor new file mode 100644 index 0000000..8bc61d2 --- /dev/null +++ b/Pages/Component/AclActionSwitch.razor @@ -0,0 +1,20 @@ +<label> + <input + type="checkbox" + checked=@InitialValue + @onchange=@(e => OnToggle.InvokeAsync((e.Value as bool?) ?? false)) + hidden> + <div class="outer"> + <p>Deny</p> + <p>Allow</p> + <div class="inner"/> + </div> +</label> + +@code { + [Parameter] + public bool InitialValue { get; set; } = false; + + [Parameter] + public EventCallback<bool> OnToggle { get; set; } +}
\ No newline at end of file diff --git a/Pages/Component/AclActionSwitch.razor.css b/Pages/Component/AclActionSwitch.razor.css new file mode 100644 index 0000000..e9de5a2 --- /dev/null +++ b/Pages/Component/AclActionSwitch.razor.css @@ -0,0 +1,48 @@ +div.outer { + align-items: center; + border-radius: 10px; + border: 1px solid var(--color-aclaction-deny); + cursor: pointer; + display: flex; + flex-direction: row; + height: 20px; + position: relative; + transition: border-color 0.1s linear; + user-select: none; + width: min-content; +} + +div.inner { + background: var(--color-aclaction-deny); + border-radius: 8px; + height: calc(100% - 2px); + left: 1px; + position: absolute; + top: 1px; + transition: left 0.1s linear, background 0.1s linear; + width: calc(50% - 2px); + z-index: 1; +} + +div.outer p { + color: white; + font-family: 'Trebuchet MS'; + font-size: 8pt; + text-align: center; + transition: color 0.1s linear; + width: 50px; + z-index: 2; +} + +input:checked + div.outer { + border-color: var(--color-aclaction-allow); +} + +input:checked + div.outer > div.inner { + background: var(--color-aclaction-allow); + left: calc(50% + 1px); +} + +input:checked + div.outer p:nth-child(2) { + color: black; +}
\ No newline at end of file diff --git a/Pages/Component/AclDialog.razor b/Pages/Component/AclDialog.razor index 7c3267d..6df9320 100644 --- a/Pages/Component/AclDialog.razor +++ b/Pages/Component/AclDialog.razor @@ -2,37 +2,81 @@ @inject IDbContextFactory<HBContext> dbFactory; @implements IDialog -<Dialog Title="Edit permissions" @ref=dialog> - @if(obj?.Acl is not null) { - <table class="data-table"> - <tr> - <th>Action</th> - <th>Subject</th> - <th>Permissions</th> - </tr> - @foreach(var rule in Object.Acl.Rules) { - <tr> - <td style="font-family:'lucida console';font-size:10pt;">@rule.Action.ToString()</td> - <td style="font-family:'lucida console';font-size:10pt;">@rule.Principal.ToString()</td> - <td style="font-family:'lucida console';font-size:10pt;">@GetActivePermissions(rule)</td> - </tr> - } - </table> - <ButtonContainer> - <button class="secondary" @onclick=Hide>Cancel</button> - </ButtonContainer> - } else { - <center><i>This item does not have any permissions set!</i></center> +<Dialog HeightPixels=500 WidthPixels=900 Title="Edit permissions" @ref=dialog> + <div class="vcontainer"> + <div class="hcontainer"> + <div> + @if(obj?.Acl is not null) { + <label> + Owner + <input type="text" autocomplete="off"/> + </label> + <table class="data-table"> + <tr> + <th>Action</th> + <th>Subject</th> + <th colspan="2">Permissions</th> + </tr> + @foreach(var rule in obj.Acl.Rules.OrderByDescending(r => r.Action)) { + <tr> + <td><div><AclActionSwitch InitialValue=@(rule.Action == AclRuleAction.Allow)/></div></td> + <td> + @(WellKnownSid.TranslateSid(rule.Principal) ?? rule.Principal.ToString()) + </td> + <td>@GetActivePermissions(rule)</td> + <td> + <a title="Edit" href="javascript:;">🖉</a> + <a title="Delete" href="javascript:;">✖</a> + </td> + </tr> + } + </table> + <br/> + <center><a href="javascript:;">Add new</a></center> + } else { + <p><i>This item does not have any permissions set!</i></p> + } + </div> + <div> + @if(obj?.Acl is not null) { + <div class="principal-select"> + <label> + Subject + <input type="text" autocomplete="off"/> + </label> + <button>Submit</button> + </div> + var permissions = Acl.GetPermissionDescriptions(obj) + .OrderByDescending(kv => BitOperations.PopCount(kv.Value)) + .ThenBy(kv => kv.Value); + foreach(var kv in permissions) { + <label> + <input type="checkbox"/> + @kv.Key + </label> + } + } else { + <p><i>Click Edit next to an ACL to edit it's permissions</i></p> + } + </div> + </div> <ButtonContainer> <button class="secondary" @onclick=Hide>Cancel</button> + @if(obj?.Acl is not null) { + <button data-keyboard-shortcut="a" @onclick=Hide><u>A</u>pply</button> + } </ButtonContainer> - } + </div> </Dialog> @code { public bool Visible { get => dialog.Visible; - set => dialog.Visible = value; + set { + dialog.Visible = value; + if(value) + StateHasChanged(); + } } private HBObject? obj; @@ -42,6 +86,11 @@ public void Show() => Visible = true; public void Hide() => Visible = false; + public void Show(HBObject obj) { + Object = obj; + Show(); + } + public HBObject? Object { get => obj; set { @@ -81,7 +130,7 @@ return string.Join(", ", perms .OrderByDescending(kv => BitOperations.PopCount(kv.Value)) - .ThenByDescending(kv => kv.Value) + .ThenBy(kv => kv.Value) .Select(kv => kv.Key)); } }
\ No newline at end of file diff --git a/Pages/Component/AclDialog.razor.css b/Pages/Component/AclDialog.razor.css index 7a7e545..74e405f 100644 --- a/Pages/Component/AclDialog.razor.css +++ b/Pages/Component/AclDialog.razor.css @@ -1,8 +1,90 @@ -table td:nth-child(2n) { - white-space: nowrap; - width: 1px; +div.vcontainer{ + display: flex; + flex-direction: column; + height: 100%; +} + +div.hcontainer { + display: flex; + flex-direction: row; + flex-grow: 1; +} + +div.hcontainer > div { + width: 50%; + flex-direction: column; + display: flex; +} + +div.hcontainer > div:first-child { + border-right: 1px solid white; + padding-right: 15px; +} + +div.hcontainer > div:last-child { + padding-left: 15px; +} + +div.hcontainer > div > p { + margin: auto 0 auto 0; + text-align: center; +} + +div.principal-select { + align-items: center; + display: flex; + flex-direction: row; +} + +div.principal-select * { + margin: 0; +} + +div.principal-select :not(:last-child) { + margin-right: 5px; +} + +div.principal-select input[type="text"] { + flex-grow: 1; +} + +div.hcontainer > div:last-child > label { + font-family: 'Lucida Console'; } table p { margin: 8px 0 8px 0; +} + +table tr { + background: none !important; +} + +table td { + white-space: nowrap; + text-overflow: ellipsis; + font-size: 8pt; + font-family: 'Lucida Console'; +} + +table td:last-child { + font-size: 12pt; +} + +table td:nth-last-child(2) { + border-right: none !important; +} + +table tr:nth-child(2n+1) td:not(:first-child) { + background: rgba(255, 255, 255, 0.1); +} + +table td:nth-child(2n) { + width: 1px; + white-space: nowrap; +} + +table td > div { + width: min-content; + margin: auto; }
\ No newline at end of file diff --git a/Pages/Component/Dialog.razor b/Pages/Component/Dialog.razor index 673ec2f..ac05515 100644 --- a/Pages/Component/Dialog.razor +++ b/Pages/Component/Dialog.razor @@ -4,7 +4,7 @@ <div class="dialog" onmousedown="dialogMouseDown(event)" - style="opacity:0;visibility:hidden;@(heightStyle)" + style="opacity:0;visibility:hidden;@(sizeStyle)" @ref=dialogDiv> @if(Title is not null) { <div class="titlebar" onmousedown="dialogTitleMouseDown(event)"> @@ -25,9 +25,19 @@ public RenderFragment ChildContent { get; set; } [Parameter] + public int WidthPixels { set => width = $"{value}px"; } + [Parameter] public int HeightPixels { set => height = $"{value}px"; } [Parameter] public int HeightPercent { set => height = $"{value}%"; } + [Parameter] + public int MinHeightPixels { set => minHeight = $"{value}px"; } + [Parameter] + public int MinHeightPercent { set => minHeight = $"{value}%"; } + [Parameter] + public int MaxHeightPixels { set => maxHeight = $"{value}px"; } + [Parameter] + public int MaxHeightPercent { set => maxHeight = $"{value}%"; } public bool Visible { get => visible; @@ -41,7 +51,10 @@ private bool visible = false; + private string? width; private string? height; + private string? minHeight; + private string? maxHeight; private ElementReference dialogDiv; @@ -53,7 +66,7 @@ await jsRuntime.InvokeVoidAsync("dialogAddObjectReference", new object[] { dialogDiv, DotNetObjectReference.Create(this) - }); + }); } } @@ -65,6 +78,19 @@ } } + private string sizeStyle => string.Join("", new[] { + widthStyle, heightStyle, minHeightStyle, maxHeightStyle + }); + + private string widthStyle => + $"{(width is null ? "" : $"width:{width};")}"; + private string heightStyle => - $"{(height is null ? "" : $"max-height:{height};")}"; + $"{(height is null ? "" : $"height:{height};")}"; + + private string minHeightStyle => + $"{(minHeight is null ? "" : $"min-height:{minHeight};")}"; + + private string maxHeightStyle => + $"{(maxHeight is null ? "" : $"max-height:{maxHeight};")}"; } diff --git a/Pages/Component/Dialog.razor.css b/Pages/Component/Dialog.razor.css index 1447407..5e983a2 100644 --- a/Pages/Component/Dialog.razor.css +++ b/Pages/Component/Dialog.razor.css @@ -31,5 +31,6 @@ div.titlebar p.title { div.content { display: flex; flex-direction: column; + flex-grow: 1; padding: 20px; } diff --git a/Pages/Component/Switch.razor.css b/Pages/Component/Switch.razor.css index 6b1f5d5..4c2d3a5 100644 --- a/Pages/Component/Switch.razor.css +++ b/Pages/Component/Switch.razor.css @@ -11,9 +11,11 @@ div.switch-inner { background: var(--col-switch-fg); border-radius: 20px; - height: 20px; + height: 18px; transition: margin-left 0.1s linear; - width: 20px; + width: 18px; + margin-top: 1px; + margin-left: 1px; } input:checked + div.switch-outer { @@ -21,5 +23,5 @@ input:checked + div.switch-outer { } input:checked + div.switch-outer > div.switch-inner { - margin-left: 20px; + margin-left: 21px; } diff --git a/Pages/ViewMedia.razor b/Pages/ViewMedia.razor index 607e59f..f6a1382 100644 --- a/Pages/ViewMedia.razor +++ b/Pages/ViewMedia.razor @@ -85,6 +85,7 @@ <ButtonContainer> <button @onclick=@(() => { aclDialog.Object = media; aclDialog.Show(); }) class="secondary" data-keyboard-shortcut="p">Edit <u>P</u>ermissions</button> <button @onclick=@(() => deleteDialog.Show()) class="warning" data-keyboard-shortcut="d"><u>D</u>elete</button> + <button @onclick=@(() => aclDialog.Show(media)) class="secondary" data-keyboard-shortcut="p">Edit <u>P</u>ermissions</button> <button @onclick=@(() => tagDialog.Show()) class="secondary" data-keyboard-shortcut="t">Add <u>T</u>ag</button> <button @onclick=@(() => ocrDialog.Show()) class="secondary" data-keyboard-shortcut="o">View <u>O</u>CR</button> @if(infoEditMode) { diff --git a/PrincipalProviders/LocalPrincipalProvider.cs b/PrincipalProviders/LocalPrincipalProvider.cs index d480633..5c27518 100644 --- a/PrincipalProviders/LocalPrincipalProvider.cs +++ b/PrincipalProviders/LocalPrincipalProvider.cs @@ -15,6 +15,14 @@ public class LocalPrincipalProvider : PrincipalProvider { return db.Principals.FirstOrDefault(p => p.Name == name); } + public override IPrincipal[]? SearchPrincipals(string name) { + using var db = dbFactory.CreateDbContext(); + return db.Principals + .Where(p => p.Name.ToLower().Contains(name)) + .Cast<IPrincipal>() + .ToArray(); + } + public override IUser? GetUser(string name) { using var db = dbFactory.CreateDbContext(); return db.Users.FirstOrDefault(p => p.Name == name); diff --git a/SecurityIdentifier.cs b/SecurityIdentifier.cs index c3f11ef..075788d 100644 --- a/SecurityIdentifier.cs +++ b/SecurityIdentifier.cs @@ -1,16 +1,43 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; namespace HyperBooru; -public enum WellKnownSidType { - NullSid, - WorldSid, - LocalSid, - CreatorOwnerSid, - CreatorGroupSid +public static class WellKnownSid { + public static readonly SecurityIdentifier NullSid = new("S-1-0-0"); + public static readonly SecurityIdentifier WorldSid = new("S-1-1-0"); + public static readonly SecurityIdentifier LocalSid = new("S-1-2-0"); + public static readonly SecurityIdentifier CreatorOwnerSid = new("S-1-3-0"); + public static readonly SecurityIdentifier CreatorGroupSid = new("S-1-3-1"); + + private static readonly (string name, SecurityIdentifier sid)[] nameMap = new[] { + ( "Everyone", WorldSid ), + ( "LOCAL", LocalSid ), + ( "CREATOR OWNER", CreatorGroupSid ), + ( "CREATOR GROUP", CreatorGroupSid ) + }; + + public static SecurityIdentifier? TranslateName(string name) { + try { + return nameMap + .First(x => x.name.ToLower() == name.ToLower().Trim()) + .sid; + } catch(InvalidOperationException) { + return null; + } + } + + public static string? TranslateSid(SecurityIdentifier sid) { + try { + return nameMap.First(x => x.sid == sid).name; + } catch(InvalidOperationException) { + return null; + } + } } public class SecurityIdentifier { @@ -19,17 +46,6 @@ public class SecurityIdentifier { private static readonly Regex SddlRegex = new(@"^S(-[0-9]+){2,}$", RegexOptions.Compiled); - private static readonly Dictionary<WellKnownSidType, string> wellKnownSidTypes = new() { - { HyperBooru.WellKnownSidType.NullSid, "S-1-0-0" }, - { HyperBooru.WellKnownSidType.WorldSid, "S-1-1-0" }, - { HyperBooru.WellKnownSidType.LocalSid, "S-1-2-0" }, - { HyperBooru.WellKnownSidType.CreatorOwnerSid, "S-1-3-0" }, - { HyperBooru.WellKnownSidType.CreatorGroupSid, "S-1-3-1" } - }; - - public SecurityIdentifier(WellKnownSidType wellKnownSidType) - : this(wellKnownSidTypes[wellKnownSidType]) {} - public SecurityIdentifier(string sddlForm) { var match = SddlRegex.Match(sddlForm); if(!match.Success) @@ -119,6 +135,35 @@ public struct SidStruct { public byte[] IdentifierAuthority; [MarshalAs(UnmanagedType.ByValArray)] public uint[] SubAuthorities; + + public static bool operator ==(SidStruct? x, SidStruct? y) => + x?.Equals(y) ?? false; + + public static bool operator !=(SidStruct? x, SidStruct? y) => + !(x == y); + + public override bool Equals([NotNullWhen(true)] object? obj) { + if(obj is null || obj is not SidStruct) + return false; + + var sid = (SidStruct) obj; + + return + Revision == sid.Revision && + SubAuthorityCount == sid.SubAuthorityCount && + Enumerable.SequenceEqual(IdentifierAuthority, sid.IdentifierAuthority) && + Enumerable.SequenceEqual(SubAuthorities, sid.SubAuthorities); + } + + public override int GetHashCode() => ( + Revision, + SubAuthorityCount, + IdentifierAuthority + .Select(v => (uint) v) + .Aggregate(0, (a, v) => HashCode.Combine(a, v)), + SubAuthorities + .Aggregate(0, (a, v) => HashCode.Combine(a, v))) + .GetHashCode(); } public class SecurityIdentifierConverter : ValueConverter<SecurityIdentifier, byte[]> { diff --git a/Services/PrincipalProvider.cs b/Services/PrincipalProvider.cs index d37e8c0..4b2cf42 100644 --- a/Services/PrincipalProvider.cs +++ b/Services/PrincipalProvider.cs @@ -5,6 +5,16 @@ public interface IPrincipalProvider { public IUser? GetUser(string name); public IGroup? GetGroup(string name); + /// <summary> + /// Perform a search for any principals whose account name + /// matches the search term specified by <c>name</c>. + /// </summary> + /// <returns> + /// A list of matching principals or <c>null</c> if the + /// provider does not support search functionality. + /// </returns> + public IPrincipal[]? SearchPrincipals(string name); + public IGroup[] GetGroups(IPrincipal principal); public IGroup[] GetGroups(IPrincipal principal, bool recurse); @@ -19,6 +29,8 @@ public abstract class PrincipalProvider : IPrincipalProvider { public abstract IUser? GetUser(string name); public abstract IGroup? GetGroup(string name); + public abstract IPrincipal[]? SearchPrincipals(string name); + public IGroup[] GetGroups(IPrincipal principal) => GetGroups(principal.Sid, false); public IGroup[] GetGroups(IPrincipal principal, bool recurse) => @@ -28,4 +40,9 @@ public abstract class PrincipalProvider : IPrincipalProvider { public abstract IGroup[] GetGroups(SecurityIdentifier sid, bool recurse); public abstract bool ValidatePassword(IUser user, string password); + + public void Test() { + var ret = SearchPrincipals("lol"); + Console.WriteLine(ret); + } } diff --git a/wwwroot/styles/global.css b/wwwroot/styles/global.css index ebcda47..5e46d37 100644 --- a/wwwroot/styles/global.css +++ b/wwwroot/styles/global.css @@ -23,6 +23,8 @@ --col-switch-bg: var(--col-bg); --col-switch-fg: #fff; --col-switch-bg-hl: var(--col-accent-pri); + --color-aclaction-allow: #8dff76; + --color-aclaction-deny: #ff4747; --size-default-gap: 30px; } @@ -87,6 +89,11 @@ button, input[type=submit] { user-select: none; } +input[type="checkbox"] { + accent-color: var(--col-accent-pri); + background: #222; +} + button:disabled { color: var(--col-button-disabled) !important; background: var(--col-button-disabled-bg) !important; @@ -141,7 +148,7 @@ input, textarea { margin-bottom: 10px; } -input { +input:not(input[type="checkbox"]) { height: 25px !important; } |
