summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Controllers/LoginController.cs13
-rw-r--r--HBContext.cs2
-rw-r--r--LocalPrincipal.cs11
-rw-r--r--Migrations/20231009045945_SecurityRevision.Designer.cs497
-rw-r--r--Migrations/20231009045945_SecurityRevision.cs63
-rw-r--r--Migrations/HBContextModelSnapshot.cs7
-rw-r--r--Pages/Component/AclDialog.razor114
-rw-r--r--Pages/Component/AclDialog.razor.css19
-rw-r--r--Pages/Component/MiniPrincipalSelect.razor44
-rw-r--r--Pages/Component/MiniPrincipalSelect.razor.css18
-rw-r--r--Principal.cs5
-rw-r--r--SecurityIdentifier.cs6
-rw-r--r--Services/SecurityService.cs40
-rw-r--r--wwwroot/js/keyboard.js2
-rw-r--r--wwwroot/styles/global.css3
15 files changed, 755 insertions, 89 deletions
diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs
index 364bc5e..ebadf37 100644
--- a/Controllers/LoginController.cs
+++ b/Controllers/LoginController.cs
@@ -11,27 +11,26 @@ namespace HyperBooru.Controllers;
[Route("/")]
public class LoginController : Controller {
private IHttpContextAccessor httpContextAccessor;
- private IPrincipalProvider principalProvider;
+ private ISecurityService securityService;
public LoginController(
IHttpContextAccessor httpContextAccessor,
- IPrincipalProvider principalProvider) {
+ ISecurityService securityService) {
this.httpContextAccessor = httpContextAccessor;
- this.principalProvider = principalProvider;
+ this.securityService = securityService;
}
[HttpPost("Login")]
public async Task<IActionResult> Login(
[FromForm] string username,
- [FromForm] string password,
- HBContext db) {
+ [FromForm] string password) {
- var user = principalProvider.GetUser(username);
+ var user = securityService.GetUser(username);
if(user is null)
return StatusCode(403);
- if(!principalProvider.ValidatePassword(user, password))
+ if(!securityService.ValidatePassword(user, password))
return StatusCode(403);
var claims = new Claim[] {
diff --git a/HBContext.cs b/HBContext.cs
index dee100d..3b7849a 100644
--- a/HBContext.cs
+++ b/HBContext.cs
@@ -80,7 +80,7 @@ public class HBContext : DbContext {
modelBuilder.Entity<LocalUser>().HasData(new LocalUser[] {
new() {
LocalPrincipalId = (int) LocalPrincipalId.AdminUser,
- Name = "admin",
+ Name = "Administrator",
Sid = new SecurityIdentifier("S-1-5-18"),
PasswordHash = LocalPrincipalProvider.HashPassword("admin")
}
diff --git a/LocalPrincipal.cs b/LocalPrincipal.cs
index 35ff310..f4b9981 100644
--- a/LocalPrincipal.cs
+++ b/LocalPrincipal.cs
@@ -11,6 +11,17 @@ public class LocalPrincipal : IPrincipal {
public string Name { get; set; }
public SecurityIdentifier Sid { get; set; }
public List<LocalGroup> MemberOf { get; set; }
+
+ // At this time, DisplayName is bound to Name, as local
+ // users are currently expected to be identified by username
+ // only. This is a quick 'n' dirty hack that works for
+ // displaying the user's name consistently, but may lead to
+ // unexpected behaviour in future when setting the DisplayName
+ // property if that ever becomes possible.
+ public string DisplayName {
+ get => Name;
+ set => Name = value;
+ }
}
public class LocalUser : LocalPrincipal, IUser {
diff --git a/Migrations/20231009045945_SecurityRevision.Designer.cs b/Migrations/20231009045945_SecurityRevision.Designer.cs
new file mode 100644
index 0000000..a2c924d
--- /dev/null
+++ b/Migrations/20231009045945_SecurityRevision.Designer.cs
@@ -0,0 +1,497 @@
+// <auto-generated />
+using System;
+using HyperBooru;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace HyperBooru.Migrations
+{
+ [DbContext(typeof(HBContext))]
+ [Migration("20231009045945_SecurityRevision")]
+ partial class SecurityRevision
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("HyperBooru.Acl", b =>
+ {
+ b.Property<int>("AclId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("AclId"));
+
+ b.HasKey("AclId");
+
+ b.ToTable("Acls");
+ });
+
+ modelBuilder.Entity("HyperBooru.AclRule", b =>
+ {
+ b.Property<int>("AclRuleId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("AclRuleId"));
+
+ b.Property<int?>("AclId")
+ .HasColumnType("integer");
+
+ b.Property<int>("Action")
+ .HasColumnType("integer");
+
+ b.Property<long>("Permissions")
+ .HasColumnType("bigint");
+
+ b.Property<byte[]>("Principal")
+ .IsRequired()
+ .HasColumnType("bytea");
+
+ b.HasKey("AclRuleId");
+
+ b.HasIndex("AclId");
+
+ b.ToTable("AclRules");
+ });
+
+ modelBuilder.Entity("HyperBooru.HBObject", b =>
+ {
+ b.Property<int>("ObjectId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("ObjectId"));
+
+ b.Property<int?>("AclId")
+ .HasColumnType("integer");
+
+ b.Property<Guid>("Guid")
+ .HasColumnType("uuid");
+
+ b.Property<byte[]>("Owner")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bytea")
+ .HasDefaultValue(new byte[] { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 });
+
+ b.HasKey("ObjectId");
+
+ b.HasIndex("AclId")
+ .IsUnique();
+
+ b.HasIndex("Guid");
+
+ b.ToTable("Objects", (string)null);
+
+ b.UseTptMappingStrategy();
+ });
+
+ modelBuilder.Entity("HyperBooru.LocalPrincipal", b =>
+ {
+ b.Property<int>("LocalPrincipalId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("LocalPrincipalId"));
+
+ b.Property<string>("Discriminator")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property<string>("DisplayName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property<byte[]>("Sid")
+ .IsRequired()
+ .HasColumnType("bytea");
+
+ b.HasKey("LocalPrincipalId");
+
+ b.HasIndex("Name");
+
+ b.HasIndex("Sid");
+
+ b.ToTable("SecurityPrincipals", (string)null);
+
+ b.HasDiscriminator<string>("Discriminator").HasValue("LocalPrincipal");
+
+ b.UseTphMappingStrategy();
+ });
+
+ modelBuilder.Entity("HyperBooru.OcrData", b =>
+ {
+ b.Property<int>("OcrDataId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("OcrDataId"));
+
+ b.Property<int>("MediaId")
+ .HasColumnType("integer");
+
+ b.Property<string>("SearchableText")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property<string>("Text")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property<DateTime>("Timestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("OcrDataId");
+
+ b.HasIndex("MediaId")
+ .IsUnique();
+
+ b.ToTable("OcrData");
+ });
+
+ modelBuilder.Entity("LocalGroupLocalPrincipal", b =>
+ {
+ b.Property<int>("LocalPrincipalId")
+ .HasColumnType("integer");
+
+ b.Property<int>("MemberOfLocalPrincipalId")
+ .HasColumnType("integer");
+
+ b.HasKey("LocalPrincipalId", "MemberOfLocalPrincipalId");
+
+ b.HasIndex("MemberOfLocalPrincipalId");
+
+ b.ToTable("SecurityPrincipalMemberships", (string)null);
+ });
+
+ modelBuilder.Entity("TagDefinitionTagDefinition", b =>
+ {
+ b.Property<int>("ImplicitTagsObjectId")
+ .HasColumnType("integer");
+
+ b.Property<int>("TagDefinitionObjectId")
+ .HasColumnType("integer");
+
+ b.HasKey("ImplicitTagsObjectId", "TagDefinitionObjectId");
+
+ b.HasIndex("TagDefinitionObjectId");
+
+ b.ToTable("ImplicitTags", (string)null);
+ });
+
+ modelBuilder.Entity("HyperBooru.Media", b =>
+ {
+ b.HasBaseType("HyperBooru.HBObject");
+
+ b.Property<int>("CurrentUploadedFileId")
+ .HasColumnType("integer");
+
+ b.Property<string>("LongDescription")
+ .HasColumnType("text");
+
+ b.Property<string>("ShortDescription")
+ .HasColumnType("text");
+
+ b.HasIndex("CurrentUploadedFileId")
+ .IsUnique();
+
+ b.ToTable("Media", (string)null);
+ });
+
+ modelBuilder.Entity("HyperBooru.Tag", b =>
+ {
+ b.HasBaseType("HyperBooru.HBObject");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property<int>("TagDefinitionId")
+ .HasColumnType("integer");
+
+ b.Property<int>("TargetObjectId")
+ .HasColumnType("integer");
+
+ b.HasIndex("TagDefinitionId");
+
+ b.HasIndex("TargetObjectId");
+
+ b.ToTable("Tags", (string)null);
+ });
+
+ modelBuilder.Entity("HyperBooru.TagDefinition", b =>
+ {
+ b.HasBaseType("HyperBooru.HBObject");
+
+ b.Property<string>("Alias")
+ .HasColumnType("text");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property<string>("Namespace")
+ .HasColumnType("text");
+
+ b.Property<int>("Source")
+ .HasColumnType("integer");
+
+ b.ToTable("TagDefinitions", (string)null);
+
+ b.HasData(
+ new
+ {
+ ObjectId = -1,
+ Guid = new Guid("ebdad4f8-455a-4351-8017-1d4854d6fa38"),
+ Owner = new byte[] { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 },
+ Name = "nsfw",
+ Source = 0
+ },
+ new
+ {
+ ObjectId = -2,
+ Guid = new Guid("ea212801-5bcc-4c0e-814f-fb9d30db58bc"),
+ Owner = new byte[] { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 },
+ Name = "ingest",
+ Source = 0
+ });
+ });
+
+ modelBuilder.Entity("HyperBooru.UploadedFile", b =>
+ {
+ b.HasBaseType("HyperBooru.HBObject");
+
+ b.Property<string>("Checksum")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property<bool>("ChecksumVerified")
+ .HasColumnType("boolean");
+
+ b.Property<DateTime?>("CreateTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property<string>("Filename")
+ .HasColumnType("text");
+
+ b.Property<int?>("Height")
+ .HasColumnType("integer");
+
+ b.Property<DateTime?>("LastAccessTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property<DateTime?>("LastWriteTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property<long>("Length")
+ .HasColumnType("bigint");
+
+ b.Property<int>("MediaObjectId")
+ .HasColumnType("integer");
+
+ b.Property<string>("MimeType")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property<DateTime>("UploadTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property<int?>("Width")
+ .HasColumnType("integer");
+
+ b.HasIndex("MediaObjectId");
+
+ b.ToTable("UploadedFiles", (string)null);
+ });
+
+ modelBuilder.Entity("HyperBooru.LocalGroup", b =>
+ {
+ b.HasBaseType("HyperBooru.LocalPrincipal");
+
+ b.HasDiscriminator().HasValue("LocalGroup");
+ });
+
+ modelBuilder.Entity("HyperBooru.LocalUser", b =>
+ {
+ b.HasBaseType("HyperBooru.LocalPrincipal");
+
+ b.Property<string>("PasswordHash")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasDiscriminator().HasValue("LocalUser");
+
+ b.HasData(
+ new
+ {
+ LocalPrincipalId = -1,
+ DisplayName = "Administrator",
+ Name = "Administrator",
+ Sid = new byte[] { 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0 },
+ PasswordHash = "P4geAuE2yX/PDRHuJSq74FF5vO782rWz5c0LAQPR8m45DEYAONhu1wYnAn60PSNyjocqEBdnCeKCJfK3sKyuWw=="
+ });
+ });
+
+ modelBuilder.Entity("HyperBooru.AclRule", b =>
+ {
+ b.HasOne("HyperBooru.Acl", null)
+ .WithMany("Rules")
+ .HasForeignKey("AclId");
+ });
+
+ modelBuilder.Entity("HyperBooru.HBObject", b =>
+ {
+ b.HasOne("HyperBooru.Acl", "Acl")
+ .WithOne("Subject")
+ .HasForeignKey("HyperBooru.HBObject", "AclId");
+
+ b.Navigation("Acl");
+ });
+
+ modelBuilder.Entity("HyperBooru.OcrData", b =>
+ {
+ b.HasOne("HyperBooru.Media", "Media")
+ .WithOne("OcrData")
+ .HasForeignKey("HyperBooru.OcrData", "MediaId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Media");
+ });
+
+ modelBuilder.Entity("LocalGroupLocalPrincipal", b =>
+ {
+ b.HasOne("HyperBooru.LocalPrincipal", null)
+ .WithMany()
+ .HasForeignKey("LocalPrincipalId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("HyperBooru.LocalGroup", null)
+ .WithMany()
+ .HasForeignKey("MemberOfLocalPrincipalId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("TagDefinitionTagDefinition", b =>
+ {
+ b.HasOne("HyperBooru.TagDefinition", null)
+ .WithMany()
+ .HasForeignKey("ImplicitTagsObjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("HyperBooru.TagDefinition", null)
+ .WithMany()
+ .HasForeignKey("TagDefinitionObjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("HyperBooru.Media", b =>
+ {
+ b.HasOne("HyperBooru.UploadedFile", "CurrentUploadedFile")
+ .WithOne()
+ .HasForeignKey("HyperBooru.Media", "CurrentUploadedFileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("HyperBooru.HBObject", null)
+ .WithOne()
+ .HasForeignKey("HyperBooru.Media", "ObjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CurrentUploadedFile");
+ });
+
+ modelBuilder.Entity("HyperBooru.Tag", b =>
+ {
+ b.HasOne("HyperBooru.HBObject", null)
+ .WithOne()
+ .HasForeignKey("HyperBooru.Tag", "ObjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("HyperBooru.TagDefinition", "TagDefinition")
+ .WithMany()
+ .HasForeignKey("TagDefinitionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("HyperBooru.HBObject", "Target")
+ .WithMany("Tags")
+ .HasForeignKey("TargetObjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("TagDefinition");
+
+ b.Navigation("Target");
+ });
+
+ modelBuilder.Entity("HyperBooru.TagDefinition", b =>
+ {
+ b.HasOne("HyperBooru.HBObject", null)
+ .WithOne()
+ .HasForeignKey("HyperBooru.TagDefinition", "ObjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("HyperBooru.UploadedFile", b =>
+ {
+ b.HasOne("HyperBooru.Media", "Media")
+ .WithMany("UploadedFiles")
+ .HasForeignKey("MediaObjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("HyperBooru.HBObject", null)
+ .WithOne()
+ .HasForeignKey("HyperBooru.UploadedFile", "ObjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Media");
+ });
+
+ modelBuilder.Entity("HyperBooru.Acl", b =>
+ {
+ b.Navigation("Rules");
+
+ b.Navigation("Subject")
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("HyperBooru.HBObject", b =>
+ {
+ b.Navigation("Tags");
+ });
+
+ modelBuilder.Entity("HyperBooru.Media", b =>
+ {
+ b.Navigation("OcrData");
+
+ b.Navigation("UploadedFiles");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Migrations/20231009045945_SecurityRevision.cs b/Migrations/20231009045945_SecurityRevision.cs
new file mode 100644
index 0000000..03fbd60
--- /dev/null
+++ b/Migrations/20231009045945_SecurityRevision.cs
@@ -0,0 +1,63 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace HyperBooru.Migrations
+{
+ /// <inheritdoc />
+ public partial class SecurityRevision : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "DisplayName",
+ table: "SecurityPrincipals",
+ type: "text",
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AlterColumn<byte[]>(
+ name: "Owner",
+ table: "Objects",
+ type: "bytea",
+ nullable: false,
+ defaultValue: new byte[] { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 },
+ oldClrType: typeof(byte[]),
+ oldType: "bytea",
+ oldDefaultValue: new byte[] { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 });
+
+ migrationBuilder.UpdateData(
+ table: "SecurityPrincipals",
+ keyColumn: "LocalPrincipalId",
+ keyValue: -1,
+ columns: new[] { "DisplayName", "Name" },
+ values: new object[] { "Administrator", "Administrator" });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "DisplayName",
+ table: "SecurityPrincipals");
+
+ migrationBuilder.AlterColumn<byte[]>(
+ name: "Owner",
+ table: "Objects",
+ type: "bytea",
+ nullable: false,
+ defaultValue: new byte[] { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 },
+ oldClrType: typeof(byte[]),
+ oldType: "bytea",
+ oldDefaultValue: new byte[] { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 });
+
+ migrationBuilder.UpdateData(
+ table: "SecurityPrincipals",
+ keyColumn: "LocalPrincipalId",
+ keyValue: -1,
+ column: "Name",
+ value: "admin");
+ }
+ }
+}
diff --git a/Migrations/HBContextModelSnapshot.cs b/Migrations/HBContextModelSnapshot.cs
index 3e49681..b03b20c 100644
--- a/Migrations/HBContextModelSnapshot.cs
+++ b/Migrations/HBContextModelSnapshot.cs
@@ -107,6 +107,10 @@ namespace HyperBooru.Migrations
.IsRequired()
.HasColumnType("text");
+ b.Property<string>("DisplayName")
+ .IsRequired()
+ .HasColumnType("text");
+
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
@@ -333,7 +337,8 @@ namespace HyperBooru.Migrations
new
{
LocalPrincipalId = -1,
- Name = "admin",
+ DisplayName = "Administrator",
+ Name = "Administrator",
Sid = new byte[] { 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0 },
PasswordHash = "P4geAuE2yX/PDRHuJSq74FF5vO782rWz5c0LAQPR8m45DEYAONhu1wYnAn60PSNyjocqEBdnCeKCJfK3sKyuWw=="
});
diff --git a/Pages/Component/AclDialog.razor b/Pages/Component/AclDialog.razor
index 0d24530..691e984 100644
--- a/Pages/Component/AclDialog.razor
+++ b/Pages/Component/AclDialog.razor
@@ -1,5 +1,5 @@
@using System.Numerics;
-@inject IDbContextFactory<HBContext> dbFactory;
+@inject HBContext db
@implements IDialog
<Dialog HeightPixels=500 WidthPixels=900 Title="Edit permissions" @ref=dialog>
@@ -7,19 +7,7 @@
<div class="hcontainer">
<div>
@if(obj?.Acl is not null) {
- <div class="principal-select">
- @if(editOwner is not null) {
- <label>Owner</label>
- <input type="text" autocomplete="off" @bind=editOwner/>
- <button class="secondary" @onclick=@(() => editOwner = null)>Cancel</button>
- <button @onclick=@(() => editOwner = null)>OK</button>
- } else {
- <label>Owner:</label>
- <a href="javascript:;" @onclick=@(() => editOwner = obj.Owner.ToString())>
- @obj.Owner.ToString()
- </a>
- }
- </div>
+ <MiniPrincipalSelect Label="Owner"/>
<table class="data-table">
<tr>
<th>Action</th>
@@ -28,7 +16,13 @@
</tr>
@foreach(var rule in obj.Acl.Rules.OrderByDescending(r => r.Action)) {
<tr>
- <td><div><AclActionSwitch InitialValue=@(rule.Action == AclRuleAction.Allow)/></div></td>
+ <td>
+ <div>
+ <AclActionSwitch
+ InitialValue=@(rule.Action == AclRuleAction.Allow)
+ OnToggle=@((v) => rule.Action = v ? AclRuleAction.Allow : AclRuleAction.Deny)/>
+ </div>
+ </td>
<td>
@rule.Principal.ToString()
</td>
@@ -42,30 +36,12 @@
</table>
<br/>
<center><a href="javascript:;" @onclick=AddRule>Add new</a></center>
- } else {
- <p><i>This item does not have any permissions set!</i></p>
}
</div>
<div>
@if(ruleToEdit is not null && permissionCheckboxes is not null) {
- <div class="principal-select">
- @if(editSubject is not null) {
- <label>Subject</label>
- <input type="text" autocomplete="off" @bind=editSubject/>
- <button class="secondary" @onclick=@(() => editSubject = null)>Cancel</button>
- <button @onclick=@(() => editSubject = null)>OK</button>
- } else {
- <label>Subject:</label>
- <a href="javascript:;" @onclick=@(() => editSubject = ruleToEdit.Principal.ToString())>
- @if(ruleToEdit.Principal == WellKnownSid.NullSid) {
- <i>Select a user or group</i>
- } else {
- @ruleToEdit.Principal.ToString()
- }
- </a>
- }
- </div>
- var permissions = Acl.GetPermissionDescriptions(obj!)
+ <MiniPrincipalSelect Label="Subject"/>
+ var permissions = Acl.GetPermissionDescriptions(obj)
.OrderByDescending(kv => BitOperations.PopCount(kv.Value))
.ThenBy(kv => kv.Value);
foreach(var perm in permissionCheckboxes) {
@@ -84,7 +60,9 @@
<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>
+ <button data-keyboard-shortcut="a" @onclick=ApplyAcl disabled=@(ApplyDisabled)>
+ <u>A</u>pply
+ </button>
}
</ButtonContainer>
</div>
@@ -94,16 +72,22 @@
public bool Visible {
get => dialog.Visible;
set {
+ if(value && obj is null)
+ throw new ArgumentException("Object must not be null!", "Object");
+
dialog.Visible = value;
if(value)
StateHasChanged();
}
}
- private HBObject? obj;
+ private HBObject obj;
private AclRule? ruleToEdit;
private PermissionCheckbox[]? permissionCheckboxes;
+ private int lastHashCode;
+ private bool addedAcl;
+
private string? editOwner;
private string? editSubject;
@@ -117,32 +101,42 @@
Show();
}
- public HBObject? Object {
+ public HBObject Object {
get => obj;
set {
editOwner = null;
CancelEditRule();
- if(value is null) {
- obj = null;
- return;
- }
-
- using var db = dbFactory.CreateDbContext();
-
obj = db.Objects
.Include(o => o.Acl)
.First(o => o.ObjectId == value.ObjectId);
- if(obj.Acl is not null)
+ if (obj.Acl is not null) {
db.Entry(obj.Acl).Collection(a => a.Rules).Load();
+ addedAcl = false;
+ } else {
+ obj.Acl = new() {
+ Rules = new()
+ };
+ addedAcl = true;
+ }
+
+ 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
+
private string GetActivePermissions(AclRule rule) {
- var perms = Acl.GetPermissionDescriptions(obj!)
- .Where(kv => (rule.Permissions & kv.Value) == kv.Value)
- .ToList();
+ var perms = Acl.GetPermissionDescriptions(obj)
+ .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
@@ -164,6 +158,11 @@
.Select(kv => kv.Key));
}
+ private void ApplyAcl() {
+ db.SaveChanges();
+ Hide();
+ }
+
private void AddRule() {
var rule = new AclRule() {
Principal = WellKnownSid.NullSid,
@@ -171,20 +170,20 @@
Permissions = 0
};
- obj!.Acl!.Rules.Add(rule);
+ obj.Acl!.Rules.Add(rule);
EditRule(rule);
}
private void RemoveRule(AclRule rule) {
if(rule == ruleToEdit)
CancelEditRule();
- obj!.Acl!.Rules.Remove(rule);
+ obj.Acl!.Rules.Remove(rule);
}
private void EditRule(AclRule rule) {
ruleToEdit = rule;
editSubject = null;
- permissionCheckboxes = Acl.GetPermissionDescriptions(obj!)
+ permissionCheckboxes = Acl.GetPermissionDescriptions(obj)
.OrderByDescending(kv => BitOperations.PopCount(kv.Value))
.ThenBy(kv => kv.Value)
.Select(kv => new PermissionCheckbox(rule, kv))
@@ -196,6 +195,17 @@
permissionCheckboxes = null;
}
+ // 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
+ .Select(r => (
+ r.Action,
+ r.Permissions,
+ r.Principal.GetHashCode()).GetHashCode())
+ .Aggregate((a, v) => HashCode.Combine(a, v));
+
private class PermissionCheckbox {
public string Description { get; private init; }
diff --git a/Pages/Component/AclDialog.razor.css b/Pages/Component/AclDialog.razor.css
index c9ac518..3cab1c6 100644
--- a/Pages/Component/AclDialog.razor.css
+++ b/Pages/Component/AclDialog.razor.css
@@ -30,25 +30,6 @@ div.hcontainer > div > p {
text-align: center;
}
-div.principal-select {
- align-items: center;
- display: flex;
- flex-direction: row;
- margin-bottom: 16px;
-}
-
-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';
}
diff --git a/Pages/Component/MiniPrincipalSelect.razor b/Pages/Component/MiniPrincipalSelect.razor
new file mode 100644
index 0000000..2202b95
--- /dev/null
+++ b/Pages/Component/MiniPrincipalSelect.razor
@@ -0,0 +1,44 @@
+@inject ISecurityService securityService
+
+<div>
+ @if(edit) {
+ <label>@Label</label>
+ <input type="text" autocomplete="off" @bind=name/>
+ <button class="secondary" @onclick=@(() => Edit(false))>Cancel</button>
+ <button @onclick=Submit>OK</button>
+ } else {
+ <label>@(Label):</label>
+ <a href="javascript:;" @onclick=@(() => Edit(true))>
+ @if(SecurityIdentifier is null || SecurityIdentifier == WellKnownSid.NullSid) {
+ <i>Please select a user or group</i>
+ } else {
+ @securityService.TranslateName(SecurityIdentifier)
+ }
+ </a>
+ }
+</div>
+
+@code {
+ [Parameter]
+ public string Label { get; set; }
+
+ [Parameter]
+ public EventCallback<SecurityIdentifier> OnChange { get; set; }
+
+ private bool edit = false;
+ private string name;
+
+ public SecurityIdentifier? SecurityIdentifier { get; set; }
+
+ private void Edit(bool enableEdit) {
+ edit = enableEdit;
+
+ if(enableEdit)
+ name = SecurityIdentifier is null ? "" :
+ securityService.TranslateName(SecurityIdentifier);
+ }
+
+ private void Submit() {
+ Edit(false);
+ }
+} \ No newline at end of file
diff --git a/Pages/Component/MiniPrincipalSelect.razor.css b/Pages/Component/MiniPrincipalSelect.razor.css
new file mode 100644
index 0000000..4b7a217
--- /dev/null
+++ b/Pages/Component/MiniPrincipalSelect.razor.css
@@ -0,0 +1,18 @@
+div {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 16px;
+}
+
+div * {
+ margin: 0;
+}
+
+div :not(:last-child) {
+ margin-right: 5px;
+}
+
+div input[type="text"] {
+ flex-grow: 1;
+}
diff --git a/Principal.cs b/Principal.cs
index 3e1a607..1ac18c1 100644
--- a/Principal.cs
+++ b/Principal.cs
@@ -1,8 +1,9 @@
namespace HyperBooru;
public interface IPrincipal {
- public string Name { get; set; }
- public SecurityIdentifier Sid { get; set; }
+ public string Name { get; set; }
+ public string DisplayName { get; set; }
+ public SecurityIdentifier Sid { get; set; }
}
public interface IUser : IPrincipal {}
diff --git a/SecurityIdentifier.cs b/SecurityIdentifier.cs
index 8ffaa1b..98818e0 100644
--- a/SecurityIdentifier.cs
+++ b/SecurityIdentifier.cs
@@ -15,7 +15,6 @@ public static class WellKnownSid {
public static readonly SecurityIdentifier CreatorGroupSid = new("S-1-3-1");
private static readonly (string name, SecurityIdentifier sid)[] nameMap = new[] {
- ( "Nobody", NullSid ),
( "Everyone", WorldSid ),
( "LOCAL", LocalSid ),
( "CREATOR OWNER", CreatorGroupSid ),
@@ -32,7 +31,7 @@ public static class WellKnownSid {
}
}
- public static string? TranslateSid(SecurityIdentifier sid) {
+ public static string? TranslateName(SecurityIdentifier sid) {
try {
return nameMap.First(x => x.sid == sid).name;
} catch(InvalidOperationException) {
@@ -117,8 +116,7 @@ public class SecurityIdentifier {
}
}
- public override string ToString() =>
- WellKnownSid.TranslateSid(this) ?? SddlForm;
+ public override string ToString() => SddlForm;
public static bool operator ==(SecurityIdentifier? x, SecurityIdentifier? y) =>
x?.SidStruct.Equals(y?.SidStruct) ?? false;
diff --git a/Services/SecurityService.cs b/Services/SecurityService.cs
index e365266..2d23a58 100644
--- a/Services/SecurityService.cs
+++ b/Services/SecurityService.cs
@@ -5,6 +5,15 @@ using System.Data;
namespace HyperBooru.Services;
public interface ISecurityService {
+ public IPrincipal[]? SearchPrincipals(string name);
+ public IPrincipal? GetPrincipal(string name);
+ public IUser? GetUser(string name);
+ public IGroup? GetGroup(string name);
+ public bool ValidatePassword(IUser user, string password);
+
+ public SecurityIdentifier? TranslateName(string name);
+ public string TranslateName(SecurityIdentifier sid);
+
public IEnumerable<HBObject> Filter(
IEnumerable<HBObject> objects,
IPrincipal principal,
@@ -51,6 +60,37 @@ public class SecurityService : ISecurityService {
};
}
+ public IPrincipal[]? SearchPrincipals(string name) =>
+ principalProvider.SearchPrincipals(name);
+
+ public IPrincipal? GetPrincipal(string name) =>
+ principalProvider.GetPrincipal(name);
+
+ public IUser? GetUser(string name) =>
+ principalProvider.GetUser(name);
+
+ public IGroup? GetGroup(string name) =>
+ principalProvider.GetGroup(name);
+
+ public bool ValidatePassword(IUser user, string password) =>
+ principalProvider.ValidatePassword(user, password);
+
+ public string TranslateName(SecurityIdentifier sid) {
+ var wellKnownSid = WellKnownSid.TranslateName(sid);
+ if(wellKnownSid is not null)
+ return wellKnownSid;
+
+ return sid.SddlForm;
+ }
+
+ public SecurityIdentifier? TranslateName(string name) {
+ var wellKnownSid = WellKnownSid.TranslateName(name);
+ if(wellKnownSid is not null)
+ return wellKnownSid;
+
+ throw new NotImplementedException();
+ }
+
public IEnumerable<HBObject> Filter(
IEnumerable<HBObject> objects,
IPrincipal principal,
diff --git a/wwwroot/js/keyboard.js b/wwwroot/js/keyboard.js
index 6ba9f9f..8086418 100644
--- a/wwwroot/js/keyboard.js
+++ b/wwwroot/js/keyboard.js
@@ -20,7 +20,6 @@ async function keyDownHandler(e) {
.find(d => d.element == element)
.dialogObject
.invokeMethodAsync('KeyHandler', e.key);
- console.log('lmao');
e.preventDefault();
return;
}
@@ -32,7 +31,6 @@ async function keyDownHandler(e) {
if(button) {
button.click();
- console.log('lmao2');
e.preventDefault();
return;
}
diff --git a/wwwroot/styles/global.css b/wwwroot/styles/global.css
index 5e46d37..09bbcbf 100644
--- a/wwwroot/styles/global.css
+++ b/wwwroot/styles/global.css
@@ -95,8 +95,9 @@ input[type="checkbox"] {
}
button:disabled {
- color: var(--col-button-disabled) !important;
background: var(--col-button-disabled-bg) !important;
+ color: var(--col-button-disabled) !important;
+ cursor: not-allowed;
}
button.warning {