diff options
| -rw-r--r-- | Controllers/LoginController.cs | 13 | ||||
| -rw-r--r-- | HBContext.cs | 2 | ||||
| -rw-r--r-- | LocalPrincipal.cs | 11 | ||||
| -rw-r--r-- | Migrations/20231009045945_SecurityRevision.Designer.cs | 497 | ||||
| -rw-r--r-- | Migrations/20231009045945_SecurityRevision.cs | 63 | ||||
| -rw-r--r-- | Migrations/HBContextModelSnapshot.cs | 7 | ||||
| -rw-r--r-- | Pages/Component/AclDialog.razor | 114 | ||||
| -rw-r--r-- | Pages/Component/AclDialog.razor.css | 19 | ||||
| -rw-r--r-- | Pages/Component/MiniPrincipalSelect.razor | 44 | ||||
| -rw-r--r-- | Pages/Component/MiniPrincipalSelect.razor.css | 18 | ||||
| -rw-r--r-- | Principal.cs | 5 | ||||
| -rw-r--r-- | SecurityIdentifier.cs | 6 | ||||
| -rw-r--r-- | Services/SecurityService.cs | 40 | ||||
| -rw-r--r-- | wwwroot/js/keyboard.js | 2 | ||||
| -rw-r--r-- | wwwroot/styles/global.css | 3 |
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 { |
