summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Mannens <jake@asger.xyz>2023-10-03 16:41:58 +1100
committerJake Mannens <jake@asger.xyz>2023-10-03 16:41:58 +1100
commit33438ac951430fa370965b42a3d98a54e704ab01 (patch)
treefbd835c25ac7566e8437cd0ef988fce0596a15e7
parent7170867a9a2650fa5a98b9e2664fb2114a0bf114 (diff)
AclDialog
-rw-r--r--Acl.cs37
-rw-r--r--AclEnum.cs39
-rw-r--r--HBObject.cs1
-rw-r--r--Media.cs1
-rw-r--r--Migrations/20231001212740_Security.Designer.cs492
-rw-r--r--Migrations/20231001212740_Security.cs312
-rw-r--r--Migrations/HBContextModelSnapshot.cs176
-rw-r--r--Pages/Component/AclActionSwitch.razor20
-rw-r--r--Pages/Component/AclActionSwitch.razor.css48
-rw-r--r--Pages/Component/AclDialog.razor95
-rw-r--r--Pages/Component/AclDialog.razor.css32
-rw-r--r--Pages/Component/Titlebar.razor2
-rw-r--r--Pages/ViewMedia.razor4
-rw-r--r--SecurityIdentifier.cs6
-rw-r--r--Tag.cs1
-rw-r--r--wwwroot/styles/global.css2
16 files changed, 1230 insertions, 38 deletions
diff --git a/Acl.cs b/Acl.cs
index 7bffc50..a16d08a 100644
--- a/Acl.cs
+++ b/Acl.cs
@@ -12,6 +12,24 @@ public class Acl {
public int AclId { get; set; }
public HBObject Subject { get; set; }
public List<AclRule> Rules { get; set; }
+
+ public static Type GetAclType(HBObject obj) {
+ var attrib = Attribute.GetCustomAttribute(obj.GetType(), typeof(AclTypeAttribute), true);
+
+ return (attrib as AclTypeAttribute)?.Type ?? typeof(ObjectPermissions);
+ }
+
+ public static IEnumerable<KeyValuePair<string, ulong>> GetPermissionDescriptions(HBObject obj) {
+ var aclType = GetAclType(obj);
+
+ foreach(var val in Enum.GetValues(aclType)) {
+ var attrib = (AclPermissionAttribute?) Attribute.GetCustomAttribute(
+ aclType.GetMember(val.ToString()!).First(),
+ typeof(AclPermissionAttribute));
+
+ yield return new(attrib?.Name ?? val.ToString()!, (ulong) val);
+ }
+ }
}
public class Acl<T> : Acl where T : Enum {
@@ -40,3 +58,22 @@ public class AclRule<T> : AclRule where T : Enum {
set => base.Permissions = (ulong) (object) value;
}
}
+
+public class AclTypeAttribute : Attribute {
+ public Type Type { get; private init; }
+
+ public AclTypeAttribute(Type aclType) {
+ if(!aclType.IsEnum)
+ throw new ArgumentException();
+ Type = aclType;
+ }
+}
+
+public class AclPermissionAttribute : Attribute {
+ public string? Name { get; set; }
+
+ public AclPermissionAttribute() {}
+
+ public AclPermissionAttribute(string name) =>
+ Name = name;
+}
diff --git a/AclEnum.cs b/AclEnum.cs
index e195444..ac07bf6 100644
--- a/AclEnum.cs
+++ b/AclEnum.cs
@@ -1,14 +1,37 @@
namespace HyperBooru;
[Flags]
-public enum HBMediaPermissions {
- [AclPermission] Read = 0x01,
- [AclPermission] Write = 0x02,
- [AclPermission] Delete = 0x04,
- [AclPermission] GetTags = 0x08,
- [AclPermission] SetTags = 0x10
+public enum ObjectPermissions : ulong {
+ [AclPermission] Read = 0x01,
+ [AclPermission] Write = 0x02,
+ [AclPermission] Delete = 0x04,
+ [AclPermission("View tags")] GetTags = 0x08,
+ [AclPermission("Set tags")] SetTags = 0x10,
+ [AclPermission("View permissions")] GetAcl = 0x20,
+ [AclPermission("Set permissions")] SetAcl = 0x40,
+ [AclPermission("Full control")] FullControl = 0x7F
}
-public class AclPermissionAttribute : Attribute {
- public string Name { get; set; }
+[Flags]
+public enum MediaPermissions : ulong {
+ [AclPermission("View media")] Read = 0x01,
+ [AclPermission("Edit info")] Write = 0x02,
+ [AclPermission] Delete = 0x04,
+ [AclPermission("View tags")] GetTags = 0x08,
+ [AclPermission("Set tags")] SetTags = 0x10,
+ [AclPermission("View permissions")] GetAcl = 0x20,
+ [AclPermission("Set permissions")] SetAcl = 0x40,
+ [AclPermission("Full control")] FullControl = 0x7F
+}
+
+[Flags]
+public enum TagPermissions : ulong {
+ [AclPermission("View info")] Read = 0x01,
+ [AclPermission("Edit info")] Write = 0x02,
+ [AclPermission] Delete = 0x04,
+ [AclPermission("View implicit tags")] GetImplicitTags = 0x08,
+ [AclPermission("Set implicit tags")] SetImplicitTags = 0x10,
+ [AclPermission("View permissions")] GetAcl = 0x20,
+ [AclPermission("Set permissions")] SetAcl = 0x40,
+ [AclPermission("Full control")] FullControl = 0x7F
}
diff --git a/HBObject.cs b/HBObject.cs
index 00ddfad..df60ed0 100644
--- a/HBObject.cs
+++ b/HBObject.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace HyperBooru;
+[AclType(typeof(ObjectPermissions))]
[Index(nameof(Guid))]
public class HBObject {
[Key]
diff --git a/Media.cs b/Media.cs
index 9664250..f8fc311 100644
--- a/Media.cs
+++ b/Media.cs
@@ -6,6 +6,7 @@ using System.Net.NetworkInformation;
namespace HyperBooru;
+[AclType(typeof(MediaPermissions))]
public class Media : HBObject {
public string? ShortDescription { get; set; }
public string? LongDescription { get; set; }
diff --git a/Migrations/20231001212740_Security.Designer.cs b/Migrations/20231001212740_Security.Designer.cs
new file mode 100644
index 0000000..0b3c8ee
--- /dev/null
+++ b/Migrations/20231001212740_Security.Designer.cs
@@ -0,0 +1,492 @@
+// <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("20231001212740_Security")]
+ partial class Security
+ {
+ /// <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>("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,
+ Name = "admin",
+ 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/20231001212740_Security.cs b/Migrations/20231001212740_Security.cs
new file mode 100644
index 0000000..0e38fde
--- /dev/null
+++ b/Migrations/20231001212740_Security.cs
@@ -0,0 +1,312 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace HyperBooru.Migrations
+{
+ /// <inheritdoc />
+ public partial class Security : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_TagDefinitionTagDefinition_TagDefinitions_ImplicitTagsObjec~",
+ table: "TagDefinitionTagDefinition");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_TagDefinitionTagDefinition_TagDefinitions_TagDefinitionObje~",
+ table: "TagDefinitionTagDefinition");
+
+ migrationBuilder.DropTable(
+ name: "Users");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_TagDefinitionTagDefinition",
+ table: "TagDefinitionTagDefinition");
+
+ migrationBuilder.DeleteData(
+ table: "Objects",
+ keyColumn: "ObjectId",
+ keyValue: -3);
+
+ migrationBuilder.RenameTable(
+ name: "TagDefinitionTagDefinition",
+ newName: "ImplicitTags");
+
+ migrationBuilder.RenameIndex(
+ name: "IX_TagDefinitionTagDefinition_TagDefinitionObjectId",
+ table: "ImplicitTags",
+ newName: "IX_ImplicitTags_TagDefinitionObjectId");
+
+ migrationBuilder.AddColumn<int>(
+ name: "AclId",
+ table: "Objects",
+ type: "integer",
+ nullable: true);
+
+ migrationBuilder.AddColumn<byte[]>(
+ name: "Owner",
+ table: "Objects",
+ type: "bytea",
+ nullable: false,
+ defaultValue: new byte[] { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 });
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_ImplicitTags",
+ table: "ImplicitTags",
+ columns: new[] { "ImplicitTagsObjectId", "TagDefinitionObjectId" });
+
+ migrationBuilder.CreateTable(
+ name: "Acls",
+ columns: table => new
+ {
+ AclId = table.Column<int>(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Acls", x => x.AclId);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "SecurityPrincipals",
+ columns: table => new
+ {
+ LocalPrincipalId = table.Column<int>(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Name = table.Column<string>(type: "text", nullable: false),
+ Sid = table.Column<byte[]>(type: "bytea", nullable: false),
+ Discriminator = table.Column<string>(type: "text", nullable: false),
+ PasswordHash = table.Column<string>(type: "text", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_SecurityPrincipals", x => x.LocalPrincipalId);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AclRules",
+ columns: table => new
+ {
+ AclRuleId = table.Column<int>(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Principal = table.Column<byte[]>(type: "bytea", nullable: false),
+ Action = table.Column<int>(type: "integer", nullable: false),
+ Permissions = table.Column<long>(type: "bigint", nullable: false),
+ AclId = table.Column<int>(type: "integer", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AclRules", x => x.AclRuleId);
+ table.ForeignKey(
+ name: "FK_AclRules_Acls_AclId",
+ column: x => x.AclId,
+ principalTable: "Acls",
+ principalColumn: "AclId");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "SecurityPrincipalMemberships",
+ columns: table => new
+ {
+ LocalPrincipalId = table.Column<int>(type: "integer", nullable: false),
+ MemberOfLocalPrincipalId = table.Column<int>(type: "integer", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_SecurityPrincipalMemberships", x => new { x.LocalPrincipalId, x.MemberOfLocalPrincipalId });
+ table.ForeignKey(
+ name: "FK_SecurityPrincipalMemberships_SecurityPrincipals_LocalPrinci~",
+ column: x => x.LocalPrincipalId,
+ principalTable: "SecurityPrincipals",
+ principalColumn: "LocalPrincipalId",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_SecurityPrincipalMemberships_SecurityPrincipals_MemberOfLoc~",
+ column: x => x.MemberOfLocalPrincipalId,
+ principalTable: "SecurityPrincipals",
+ principalColumn: "LocalPrincipalId",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.UpdateData(
+ table: "Objects",
+ keyColumn: "ObjectId",
+ keyValue: -2,
+ columns: new[] { "AclId", "Owner" },
+ values: new object[] { null, new byte[] { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } });
+
+ migrationBuilder.UpdateData(
+ table: "Objects",
+ keyColumn: "ObjectId",
+ keyValue: -1,
+ columns: new[] { "AclId", "Owner" },
+ values: new object[] { null, new byte[] { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } });
+
+ migrationBuilder.InsertData(
+ table: "SecurityPrincipals",
+ columns: new[] { "LocalPrincipalId", "Discriminator", "Name", "PasswordHash", "Sid" },
+ values: new object[] { -1, "LocalUser", "admin", "P4geAuE2yX/PDRHuJSq74FF5vO782rWz5c0LAQPR8m45DEYAONhu1wYnAn60PSNyjocqEBdnCeKCJfK3sKyuWw==", new byte[] { 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0 } });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Objects_AclId",
+ table: "Objects",
+ column: "AclId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AclRules_AclId",
+ table: "AclRules",
+ column: "AclId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_SecurityPrincipalMemberships_MemberOfLocalPrincipalId",
+ table: "SecurityPrincipalMemberships",
+ column: "MemberOfLocalPrincipalId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_SecurityPrincipals_Name",
+ table: "SecurityPrincipals",
+ column: "Name");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_SecurityPrincipals_Sid",
+ table: "SecurityPrincipals",
+ column: "Sid");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_ImplicitTags_TagDefinitions_ImplicitTagsObjectId",
+ table: "ImplicitTags",
+ column: "ImplicitTagsObjectId",
+ principalTable: "TagDefinitions",
+ principalColumn: "ObjectId",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_ImplicitTags_TagDefinitions_TagDefinitionObjectId",
+ table: "ImplicitTags",
+ column: "TagDefinitionObjectId",
+ principalTable: "TagDefinitions",
+ principalColumn: "ObjectId",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Objects_Acls_AclId",
+ table: "Objects",
+ column: "AclId",
+ principalTable: "Acls",
+ principalColumn: "AclId");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_ImplicitTags_TagDefinitions_ImplicitTagsObjectId",
+ table: "ImplicitTags");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_ImplicitTags_TagDefinitions_TagDefinitionObjectId",
+ table: "ImplicitTags");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Objects_Acls_AclId",
+ table: "Objects");
+
+ migrationBuilder.DropTable(
+ name: "AclRules");
+
+ migrationBuilder.DropTable(
+ name: "SecurityPrincipalMemberships");
+
+ migrationBuilder.DropTable(
+ name: "Acls");
+
+ migrationBuilder.DropTable(
+ name: "SecurityPrincipals");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Objects_AclId",
+ table: "Objects");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_ImplicitTags",
+ table: "ImplicitTags");
+
+ migrationBuilder.DropColumn(
+ name: "AclId",
+ table: "Objects");
+
+ migrationBuilder.DropColumn(
+ name: "Owner",
+ table: "Objects");
+
+ migrationBuilder.RenameTable(
+ name: "ImplicitTags",
+ newName: "TagDefinitionTagDefinition");
+
+ migrationBuilder.RenameIndex(
+ name: "IX_ImplicitTags_TagDefinitionObjectId",
+ table: "TagDefinitionTagDefinition",
+ newName: "IX_TagDefinitionTagDefinition_TagDefinitionObjectId");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_TagDefinitionTagDefinition",
+ table: "TagDefinitionTagDefinition",
+ columns: new[] { "ImplicitTagsObjectId", "TagDefinitionObjectId" });
+
+ migrationBuilder.CreateTable(
+ name: "Users",
+ columns: table => new
+ {
+ ObjectId = table.Column<int>(type: "integer", nullable: false),
+ PasswordHash = table.Column<string>(type: "text", nullable: false),
+ Username = table.Column<string>(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Users", x => x.ObjectId);
+ table.ForeignKey(
+ name: "FK_Users_Objects_ObjectId",
+ column: x => x.ObjectId,
+ principalTable: "Objects",
+ principalColumn: "ObjectId",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.InsertData(
+ table: "Objects",
+ columns: new[] { "ObjectId", "Guid" },
+ values: new object[] { -3, new Guid("4fa948f4-7c45-4f81-bb6b-e417491e6c96") });
+
+ migrationBuilder.InsertData(
+ table: "Users",
+ columns: new[] { "ObjectId", "PasswordHash", "Username" },
+ values: new object[] { -3, "P4geAuE2yX/PDRHuJSq74FF5vO782rWz5c0LAQPR8m45DEYAONhu1wYnAn60PSNyjocqEBdnCeKCJfK3sKyuWw==", "admin" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Users_Username",
+ table: "Users",
+ column: "Username");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_TagDefinitionTagDefinition_TagDefinitions_ImplicitTagsObjec~",
+ table: "TagDefinitionTagDefinition",
+ column: "ImplicitTagsObjectId",
+ principalTable: "TagDefinitions",
+ principalColumn: "ObjectId",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_TagDefinitionTagDefinition_TagDefinitions_TagDefinitionObje~",
+ table: "TagDefinitionTagDefinition",
+ column: "TagDefinitionObjectId",
+ principalTable: "TagDefinitions",
+ principalColumn: "ObjectId",
+ onDelete: ReferentialAction.Cascade);
+ }
+ }
+}
diff --git a/Migrations/HBContextModelSnapshot.cs b/Migrations/HBContextModelSnapshot.cs
index 16e6b48..3e49681 100644
--- a/Migrations/HBContextModelSnapshot.cs
+++ b/Migrations/HBContextModelSnapshot.cs
@@ -17,11 +17,52 @@ namespace HyperBooru.Migrations
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "7.0.10")
+ .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")
@@ -30,11 +71,23 @@ namespace HyperBooru.Migrations
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);
@@ -42,6 +95,39 @@ namespace HyperBooru.Migrations
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>("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")
@@ -69,7 +155,22 @@ namespace HyperBooru.Migrations
b.HasIndex("MediaId")
.IsUnique();
- b.ToTable("OcrData", (string)null);
+ 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 =>
@@ -84,7 +185,7 @@ namespace HyperBooru.Migrations
b.HasIndex("TagDefinitionObjectId");
- b.ToTable("TagDefinitionTagDefinition", (string)null);
+ b.ToTable("ImplicitTags", (string)null);
});
modelBuilder.Entity("HyperBooru.Media", b =>
@@ -150,6 +251,7 @@ namespace HyperBooru.Migrations
{
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
},
@@ -157,6 +259,7 @@ namespace HyperBooru.Migrations
{
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
});
@@ -209,32 +312,49 @@ namespace HyperBooru.Migrations
b.ToTable("UploadedFiles", (string)null);
});
- modelBuilder.Entity("HyperBooru.User", b =>
+ modelBuilder.Entity("HyperBooru.LocalGroup", b =>
{
- b.HasBaseType("HyperBooru.HBObject");
+ b.HasBaseType("HyperBooru.LocalPrincipal");
- b.Property<string>("PasswordHash")
- .IsRequired()
- .HasColumnType("text");
+ b.HasDiscriminator().HasValue("LocalGroup");
+ });
- b.Property<string>("Username")
+ modelBuilder.Entity("HyperBooru.LocalUser", b =>
+ {
+ b.HasBaseType("HyperBooru.LocalPrincipal");
+
+ b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
- b.HasIndex("Username");
-
- b.ToTable("Users", (string)null);
+ b.HasDiscriminator().HasValue("LocalUser");
b.HasData(
new
{
- ObjectId = -3,
- Guid = new Guid("4fa948f4-7c45-4f81-bb6b-e417491e6c96"),
- PasswordHash = "P4geAuE2yX/PDRHuJSq74FF5vO782rWz5c0LAQPR8m45DEYAONhu1wYnAn60PSNyjocqEBdnCeKCJfK3sKyuWw==",
- Username = "admin"
+ LocalPrincipalId = -1,
+ Name = "admin",
+ 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")
@@ -246,6 +366,21 @@ namespace HyperBooru.Migrations
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)
@@ -329,12 +464,11 @@ namespace HyperBooru.Migrations
b.Navigation("Media");
});
- modelBuilder.Entity("HyperBooru.User", b =>
+ modelBuilder.Entity("HyperBooru.Acl", b =>
{
- b.HasOne("HyperBooru.HBObject", null)
- .WithOne()
- .HasForeignKey("HyperBooru.User", "ObjectId")
- .OnDelete(DeleteBehavior.Cascade)
+ b.Navigation("Rules");
+
+ b.Navigation("Subject")
.IsRequired();
});
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 33d1f03..8116f04 100644
--- a/Pages/Component/AclDialog.razor
+++ b/Pages/Component/AclDialog.razor
@@ -1,19 +1,104 @@
-@implements IDialog
+@using System.Numerics;
+@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>
+ <th></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>@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>
+ <ButtonContainer>
+ <button class="secondary" @onclick=Hide>Cancel</button>
+ <button data-keyboard-shortcut="a" @onclick=Hide><u>A</u>pply</button>
+ </ButtonContainer>
+ } else {
+ <center><i>This item does not have any permissions set!</i></center>
+ <ButtonContainer>
+ <button class="secondary" @onclick=Hide>Cancel</button>
+ </ButtonContainer>
+ }
</Dialog>
@code {
- [Parameter]
- public HBObject Object { get; set; }
-
public bool Visible {
get => dialog.Visible;
- set => dialog.Visible = value;
+ set {
+ dialog.Visible = value;
+ if(value)
+ StateHasChanged();
+ }
}
+ private HBObject? obj;
+
private Dialog dialog;
public void Show() => Visible = true;
public void Hide() => Visible = false;
+
+ public void Show(HBObject obj) {
+ Object = obj;
+ Show();
+ }
+
+ public HBObject? Object {
+ get => obj;
+ set {
+ 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)
+ db.Entry(obj.Acl).Collection(a => a.Rules).Load();
+ }
+ }
+
+ public string GetActivePermissions(AclRule rule) {
+ 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
+ // 'Full access' includes 'Read' and 'Write', then only
+ // show 'Full access'.
+ List<int> toRemove = new();
+ for(int i = 0; i < perms.Count(); i++)
+ for(int j = 0; j < perms.Count(); j++)
+ if(i != j)
+ if((perms[i].Value & perms[j].Value) == perms[i].Value)
+ toRemove.Add(i);
+ for(int i = toRemove.Count() - 1; i >= 0; i--)
+ perms.RemoveAt(toRemove[i]);
+
+ return string.Join(", ", perms
+ .OrderByDescending(kv => BitOperations.PopCount(kv.Value))
+ .ThenByDescending(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
new file mode 100644
index 0000000..f1b7931
--- /dev/null
+++ b/Pages/Component/AclDialog.razor.css
@@ -0,0 +1,32 @@
+table p {
+ margin: 8px 0 8px 0;
+}
+
+table tr {
+ background: none !important;
+}
+
+table td {
+ font-family: 'Lucida Console';
+ font-size: 8pt;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+table td:last-child {
+ font-size: 12pt;
+}
+
+table tr:nth-child(2n+1) td:not(:first-child) {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+table td:nth-child(2n) {
+ white-space: nowrap;
+ width: 1px;
+}
+
+table td > div {
+ margin: auto;
+ width: min-content;
+} \ No newline at end of file
diff --git a/Pages/Component/Titlebar.razor b/Pages/Component/Titlebar.razor
index ad41532..766787a 100644
--- a/Pages/Component/Titlebar.razor
+++ b/Pages/Component/Titlebar.razor
@@ -74,5 +74,5 @@
private AboutDialog aboutDialog;
- private string username => AuthState.GetAwaiter().GetResult().User.Identity?.Name ?? "fugg";
+ private string username => AuthState.GetAwaiter().GetResult().User.Identity?.Name!;
}
diff --git a/Pages/ViewMedia.razor b/Pages/ViewMedia.razor
index 444fbc5..05cf700 100644
--- a/Pages/ViewMedia.razor
+++ b/Pages/ViewMedia.razor
@@ -84,6 +84,7 @@
<div id="button-container">
<ButtonContainer>
<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) {
@@ -125,6 +126,8 @@
OnSubmit=AddTags
@ref=tagDialog/>
+<AclDialog @ref=aclDialog/>
+
@code {
[Parameter]
[SupplyParameterFromQuery(Name = "m")]
@@ -142,6 +145,7 @@
private Dialog deleteDialog;
private Dialog ocrDialog;
private TagSelectDialog tagDialog;
+ private AclDialog aclDialog;
private ElementReference shortDescriptionInput;
diff --git a/SecurityIdentifier.cs b/SecurityIdentifier.cs
index 81d5ae7..c3f11ef 100644
--- a/SecurityIdentifier.cs
+++ b/SecurityIdentifier.cs
@@ -98,10 +98,10 @@ public class SecurityIdentifier {
return $"S-{SidStruct.Revision}-{identifierAuthority}{subAuthorities}";
}
- public static bool operator ==(SecurityIdentifier x, SecurityIdentifier y) =>
- x.SidStruct.Equals(y.SidStruct);
+ public static bool operator ==(SecurityIdentifier? x, SecurityIdentifier? y) =>
+ x?.SidStruct.Equals(y?.SidStruct) ?? false;
- public static bool operator !=(SecurityIdentifier x, SecurityIdentifier y) =>
+ public static bool operator !=(SecurityIdentifier? x, SecurityIdentifier? y) =>
!(x == y);
public override bool Equals(object? obj) =>
diff --git a/Tag.cs b/Tag.cs
index 0150b39..1560798 100644
--- a/Tag.cs
+++ b/Tag.cs
@@ -9,6 +9,7 @@ public enum TagSource {
UserTag
}
+[AclType(typeof(TagPermissions))]
public class TagDefinition : HBObject {
public TagSource Source { get; set; } = TagSource.Internal;
public string? Namespace { get; set; }
diff --git a/wwwroot/styles/global.css b/wwwroot/styles/global.css
index ebcda47..ec77442 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;
}