summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Mannens <jake@asger.xyz>2023-10-05 16:47:49 +1100
committerJake Mannens <jake@asger.xyz>2023-10-05 16:47:49 +1100
commit2c6e3aa4456811a3d6412fc10019012a900eb6a0 (patch)
treed0616bbe573a4abe5aaf9f80e7960a40352967b8
parent035d2e3858dd55580c294031573c3be9e1999449 (diff)
parent3d5f6e47bd74ce77d5ec253f51b7cef1b42099ef (diff)
Merged security
-rw-r--r--HBContext.cs2
-rw-r--r--HBObject.cs2
-rw-r--r--Pages/Component/AclActionSwitch.razor20
-rw-r--r--Pages/Component/AclActionSwitch.razor.css48
-rw-r--r--Pages/Component/AclDialog.razor97
-rw-r--r--Pages/Component/AclDialog.razor.css88
-rw-r--r--Pages/Component/Dialog.razor32
-rw-r--r--Pages/Component/Dialog.razor.css1
-rw-r--r--Pages/Component/Switch.razor.css8
-rw-r--r--Pages/ViewMedia.razor1
-rw-r--r--PrincipalProviders/LocalPrincipalProvider.cs8
-rw-r--r--SecurityIdentifier.cs79
-rw-r--r--Services/PrincipalProvider.cs17
-rw-r--r--wwwroot/styles/global.css9
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:;">&#x1F589</a>
+ <a title="Delete" href="javascript:;">&#x2716</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;
}