diff --git a/src/Turnierplan.App.Test.Unit/Security/AccessValidatorTest.cs b/src/Turnierplan.App.Test.Unit/Security/AccessValidatorTest.cs
index 52bb9ae1..6c0f743c 100644
--- a/src/Turnierplan.App.Test.Unit/Security/AccessValidatorTest.cs
+++ b/src/Turnierplan.App.Test.Unit/Security/AccessValidatorTest.cs
@@ -17,8 +17,8 @@ public void IsActionAllowed___When_Called_With_Basic_Target___Returns_Expected_R
{
var target = new Organization("Test");
- var principal = new Principal(PrincipalKind.User, "faa6d5d3-93ad-410e-bc81-171a04cf0130");
- var otherPrincipal = new Principal(PrincipalKind.User, "98f8cb8c-606f-47fc-805f-244210e1df51");
+ var principal = new Principal(PrincipalKind.User, Guid.Parse("faa6d5d3-93ad-410e-bc81-171a04cf0130"));
+ var otherPrincipal = new Principal(PrincipalKind.User, Guid.Parse("98f8cb8c-606f-47fc-805f-244210e1df51"));
target.AddRoleAssignment(Role.Reader, principal);
target.AddRoleAssignment(Role.Contributor, otherPrincipal);
@@ -35,8 +35,8 @@ public void IsActionAllowed___When_Called_With_Indirect_Target___Returns_Expecte
{
var organization = new Organization("Test");
- var principal = new Principal(PrincipalKind.User, "faa6d5d3-93ad-410e-bc81-171a04cf0130");
- var otherPrincipal = new Principal(PrincipalKind.User, "98f8cb8c-606f-47fc-805f-244210e1df51");
+ var principal = new Principal(PrincipalKind.User, Guid.Parse("faa6d5d3-93ad-410e-bc81-171a04cf0130"));
+ var otherPrincipal = new Principal(PrincipalKind.User, Guid.Parse("98f8cb8c-606f-47fc-805f-244210e1df51"));
organization.AddRoleAssignment(Role.Reader, principal);
organization.AddRoleAssignment(Role.Contributor, otherPrincipal);
@@ -66,8 +66,8 @@ public void IsActionAllowed___When_Called_With_Tournament_Target_And_Role_Assign
var organization = new Organization("Test");
var folder = new Folder(organization, "Test");
- var principal = new Principal(PrincipalKind.User, "faa6d5d3-93ad-410e-bc81-171a04cf0130");
- var otherPrincipal = new Principal(PrincipalKind.User, "98f8cb8c-606f-47fc-805f-244210e1df51");
+ var principal = new Principal(PrincipalKind.User, Guid.Parse("faa6d5d3-93ad-410e-bc81-171a04cf0130"));
+ var otherPrincipal = new Principal(PrincipalKind.User, Guid.Parse("98f8cb8c-606f-47fc-805f-244210e1df51"));
folder.AddRoleAssignment(Role.Reader, principal);
folder.AddRoleAssignment(Role.Contributor, otherPrincipal);
diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html
index 3ae767c7..d28ed5a3 100644
--- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html
+++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html
@@ -39,7 +39,7 @@
}
}
- {{ assignment.principal.objectId }}
+ {{ assignment.principal.principalId }}
@if (canDeleteAssignment(assignment)) {
diff --git a/src/Turnierplan.App/Endpoints/EndpointBase.cs b/src/Turnierplan.App/Endpoints/EndpointBase.cs
index b01201da..c2b83693 100644
--- a/src/Turnierplan.App/Endpoints/EndpointBase.cs
+++ b/src/Turnierplan.App/Endpoints/EndpointBase.cs
@@ -69,7 +69,7 @@ private void ConfigureAuthorization(RouteHandlerBuilder builder)
? [AuthenticationSchemes.AuthenticationSchemeApiKey, AuthenticationSchemes.AuthenticationSchemeSession]
: [AuthenticationSchemes.AuthenticationSchemeSession];
- policy.RequireAssertion(context => context.User.Claims.Any(x => x.Type.Equals(ClaimTypes.ApiKeyId) || x.Type.Equals(ClaimTypes.UserId)));
+ policy.RequireClaim(ClaimTypes.PrincipalId);
if (RequireAdministrator == true)
{
diff --git a/src/Turnierplan.App/Endpoints/Identity/IdentityEndpointBase.cs b/src/Turnierplan.App/Endpoints/Identity/IdentityEndpointBase.cs
index 4d839bd5..e858c4eb 100644
--- a/src/Turnierplan.App/Endpoints/Identity/IdentityEndpointBase.cs
+++ b/src/Turnierplan.App/Endpoints/Identity/IdentityEndpointBase.cs
@@ -4,6 +4,7 @@
using Microsoft.IdentityModel.Tokens;
using Turnierplan.App.Options;
using Turnierplan.App.Security;
+using Turnierplan.Core.RoleAssignment;
using Turnierplan.Core.User;
using ClaimTypes = Turnierplan.App.Security.ClaimTypes;
@@ -36,6 +37,8 @@ protected string CreateTokenForUser(User user, bool isRefreshToken)
claims.Add(new Claim(ClaimTypes.DisplayName, user.Name));
claims.Add(new Claim(ClaimTypes.EMailAddress, user.EMail));
claims.Add(new Claim(ClaimTypes.UserId, user.Id.ToString()));
+ claims.Add(new Claim(ClaimTypes.PrincipalId, user.PrincipalId.ToString()));
+ claims.Add(new Claim(ClaimTypes.PrincipalKind, nameof(PrincipalKind.User)));
if (user.IsAdministrator)
{
diff --git a/src/Turnierplan.App/Endpoints/Organizations/GetOrganizationsEndpoint.cs b/src/Turnierplan.App/Endpoints/Organizations/GetOrganizationsEndpoint.cs
index 4f0c0dc0..79de1cc4 100644
--- a/src/Turnierplan.App/Endpoints/Organizations/GetOrganizationsEndpoint.cs
+++ b/src/Turnierplan.App/Endpoints/Organizations/GetOrganizationsEndpoint.cs
@@ -27,8 +27,7 @@ private static async Task Handle(
}
else
{
- var userId = context.GetCurrentUserIdOrThrow();
- var principal = new Principal(PrincipalKind.User, userId.ToString());
+ var principal = context.GetActivePrincipal();
organizations = await repository.GetByPrincipalAsync(principal).ConfigureAwait(false);
}
diff --git a/src/Turnierplan.App/Extensions/HttpContextExtensions.cs b/src/Turnierplan.App/Extensions/HttpContextExtensions.cs
index b73f452a..f161d116 100644
--- a/src/Turnierplan.App/Extensions/HttpContextExtensions.cs
+++ b/src/Turnierplan.App/Extensions/HttpContextExtensions.cs
@@ -26,19 +26,27 @@ public static bool IsCurrentUserAdministrator(this HttpContext context)
public static Principal GetActivePrincipal(this HttpContext context)
{
+ PrincipalKind? kind = null;
+ Guid? principalId = null;
+
foreach (var claim in context.User.Claims)
{
- if (claim.Type.Equals(ClaimTypes.ApiKeyId))
+ if (claim.Type.Equals(ClaimTypes.PrincipalKind))
{
- return new Principal(PrincipalKind.ApiKey, claim.Value);
+ kind = Enum.Parse(claim.Value);
}
- if (claim.Type.Equals(ClaimTypes.UserId))
+ if (claim.Type.Equals(ClaimTypes.PrincipalId))
{
- return new Principal(PrincipalKind.User, claim.Value);
+ principalId = Guid.Parse(claim.Value);
}
}
+ if (kind.HasValue && principalId.HasValue)
+ {
+ return new Principal(kind.Value, principalId.Value);
+ }
+
throw new InvalidOperationException("Could not determine active principal.");
}
diff --git a/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs
index dcc03da9..a868b2e8 100644
--- a/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs
+++ b/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs
@@ -26,7 +26,7 @@ protected override RoleAssignmentDto Map(IMapper mapper, MappingContext context,
Principal = new PrincipalDto
{
Kind = source.Principal.Kind,
- ObjectId = source.Principal.ObjectId
+ PrincipalId = source.Principal.PrincipalId
},
Description = source.Description,
IsInherited = false
diff --git a/src/Turnierplan.App/Models/PrincipalDto.cs b/src/Turnierplan.App/Models/PrincipalDto.cs
index 0cdc2f42..cee5ca10 100644
--- a/src/Turnierplan.App/Models/PrincipalDto.cs
+++ b/src/Turnierplan.App/Models/PrincipalDto.cs
@@ -6,5 +6,5 @@ public sealed record PrincipalDto
{
public required PrincipalKind Kind { get; init; }
- public required string ObjectId { get; init; }
+ public required Guid PrincipalId { get; init; }
}
diff --git a/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs b/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs
index 6da34734..96c26a76 100644
--- a/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs
+++ b/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs
@@ -5,6 +5,7 @@
using Microsoft.Extensions.Options;
using Turnierplan.Core.ApiKey;
using Turnierplan.Core.PublicId;
+using Turnierplan.Core.RoleAssignment;
using Turnierplan.Dal;
namespace Turnierplan.App.Security;
@@ -79,7 +80,8 @@ protected override async Task HandleAuthenticateAsync()
await _apiKeyRepository.UnitOfWork.SaveChangesAsync().ConfigureAwait(false);
var identity = new ClaimsIdentity(claims: [
- new Claim(ClaimTypes.ApiKeyId, apiKey.Id.ToString())
+ new Claim(ClaimTypes.PrincipalId, apiKey.PrincipalId.ToString()),
+ new Claim(ClaimTypes.PrincipalKind, nameof(PrincipalKind.ApiKey))
]);
var principal = new ClaimsPrincipal([ identity ]);
diff --git a/src/Turnierplan.App/Security/ClaimTypes.cs b/src/Turnierplan.App/Security/ClaimTypes.cs
index 2712d03f..64ad9898 100644
--- a/src/Turnierplan.App/Security/ClaimTypes.cs
+++ b/src/Turnierplan.App/Security/ClaimTypes.cs
@@ -3,9 +3,10 @@ namespace Turnierplan.App.Security;
internal static class ClaimTypes
{
public const string Administrator = "adm";
- public const string ApiKeyId = "turnierplan_api_key_id";
public const string DisplayName = "name";
public const string EMailAddress = "mail";
+ public const string PrincipalKind = "principalkind";
+ public const string PrincipalId = "principalid";
public const string SecurityStamp = "sst";
public const string TokenType = "typ";
public const string UserId = "uid";
diff --git a/src/Turnierplan.Core/ApiKey/ApiKey.cs b/src/Turnierplan.Core/ApiKey/ApiKey.cs
index 76e50c67..4db31f11 100644
--- a/src/Turnierplan.Core/ApiKey/ApiKey.cs
+++ b/src/Turnierplan.Core/ApiKey/ApiKey.cs
@@ -13,6 +13,7 @@ public ApiKey(Organization.Organization organization, string name, string? descr
organization._apiKeys.Add(this);
Id = 0;
+ PrincipalId = Guid.NewGuid();
PublicId = new PublicId.PublicId();
Organization = organization;
Name = name;
@@ -23,9 +24,10 @@ public ApiKey(Organization.Organization organization, string name, string? descr
IsActive = true;
}
- internal ApiKey(long id, PublicId.PublicId publicId, string name, string description, string secretHash, DateTime createdAt, DateTime expiryDate, bool isActive)
+ internal ApiKey(long id, Guid principalId, PublicId.PublicId publicId, string name, string description, string secretHash, DateTime createdAt, DateTime expiryDate, bool isActive)
{
Id = id;
+ PrincipalId = principalId;
PublicId = publicId;
Name = name;
Description = description;
@@ -37,6 +39,8 @@ internal ApiKey(long id, PublicId.PublicId publicId, string name, string descrip
public override long Id { get; protected set; }
+ public Guid PrincipalId { get; }
+
public PublicId.PublicId PublicId { get; }
public Organization.Organization Organization { get; internal set; } = null!;
diff --git a/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs b/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs
index 9e74bf4c..d2e78f70 100644
--- a/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs
+++ b/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs
@@ -6,11 +6,11 @@ public static class PrincipalExtensions
{
public static Principal AsPrincipal(this ApiKey.ApiKey apiKey)
{
- return new Principal(PrincipalKind.ApiKey, apiKey.Id.ToString());
+ return new Principal(PrincipalKind.ApiKey, apiKey.PrincipalId);
}
public static Principal AsPrincipal(this User.User user)
{
- return new Principal(PrincipalKind.User, user.Id.ToString());
+ return new Principal(PrincipalKind.User, user.PrincipalId);
}
}
diff --git a/src/Turnierplan.Core/RoleAssignment/Principal.cs b/src/Turnierplan.Core/RoleAssignment/Principal.cs
index 536bd2e4..713bd033 100644
--- a/src/Turnierplan.Core/RoleAssignment/Principal.cs
+++ b/src/Turnierplan.Core/RoleAssignment/Principal.cs
@@ -4,15 +4,15 @@ namespace Turnierplan.Core.RoleAssignment;
public sealed record Principal
{
- public Principal(PrincipalKind kind, string objectId)
+ public Principal(PrincipalKind kind, Guid principalId)
{
Kind = kind;
- ObjectId = objectId;
+ PrincipalId = principalId;
}
[JsonPropertyName("k")]
public PrincipalKind Kind { get; }
- [JsonPropertyName("oid")]
- public string ObjectId { get; }
+ [JsonPropertyName("pid")]
+ public Guid PrincipalId { get; }
}
diff --git a/src/Turnierplan.Core/User/User.cs b/src/Turnierplan.Core/User/User.cs
index ab8d908f..2a94a13d 100644
--- a/src/Turnierplan.Core/User/User.cs
+++ b/src/Turnierplan.Core/User/User.cs
@@ -9,6 +9,7 @@ public User(string name, string email)
email = email.Trim();
Id = Guid.NewGuid();
+ PrincipalId = Guid.NewGuid();
CreatedAt = DateTime.UtcNow;
Name = name;
EMail = email;
@@ -19,9 +20,10 @@ public User(string name, string email)
SecurityStamp = Guid.Empty;
}
- internal User(Guid id, DateTime createdAt, string name, string eMail, string normalizedEMail, string passwordHash, bool isAdministrator, DateTime lastPasswordChange, Guid securityStamp)
+ internal User(Guid id, Guid principalId, DateTime createdAt, string name, string eMail, string normalizedEMail, string passwordHash, bool isAdministrator, DateTime lastPasswordChange, Guid securityStamp)
{
Id = id;
+ PrincipalId = principalId;
CreatedAt = createdAt;
Name = name;
EMail = eMail;
@@ -34,6 +36,8 @@ internal User(Guid id, DateTime createdAt, string name, string eMail, string nor
public override Guid Id { get; protected set; }
+ public Guid PrincipalId { get; }
+
public DateTime CreatedAt { get; }
public string Name { get; set; }
diff --git a/src/Turnierplan.Dal.Test.Unit/Converters/PrincipalConverterTest.cs b/src/Turnierplan.Dal.Test.Unit/Converters/PrincipalConverterTest.cs
index f9792f9f..5ef6a5de 100644
--- a/src/Turnierplan.Dal.Test.Unit/Converters/PrincipalConverterTest.cs
+++ b/src/Turnierplan.Dal.Test.Unit/Converters/PrincipalConverterTest.cs
@@ -7,11 +7,11 @@ namespace Turnierplan.Dal.Test.Unit.Converters;
public sealed class PrincipalConverterTest
{
[Theory]
- [InlineData(PrincipalKind.ApiKey, "123", "ApiKey:123")]
+ [InlineData(PrincipalKind.ApiKey, "81c42278-5046-4602-9e2b-e16dc7ad9b03", "ApiKey:81c42278-5046-4602-9e2b-e16dc7ad9b03")]
[InlineData(PrincipalKind.User, "2e839c1b-04ea-43c9-9bd1-614bf9586859", "User:2e839c1b-04ea-43c9-9bd1-614bf9586859")]
- public void FormatPrincipal___When_Called___Produces_Expected_Result(PrincipalKind kind, string objectId, string expectedResult)
+ public void FormatPrincipal___When_Called___Produces_Expected_Result(PrincipalKind kind, string principalId, string expectedResult)
{
- var principal = new Principal(kind, objectId);
+ var principal = new Principal(kind, Guid.Parse(principalId));
var result = PrincipalConverter.FormatPrincipal(principal);
@@ -19,22 +19,24 @@ public void FormatPrincipal___When_Called___Produces_Expected_Result(PrincipalKi
}
[Theory]
- [InlineData("ApiKey:123", PrincipalKind.ApiKey, "123")]
+ [InlineData("ApiKey:81c42278-5046-4602-9e2b-e16dc7ad9b03", PrincipalKind.ApiKey, "81c42278-5046-4602-9e2b-e16dc7ad9b03")]
[InlineData("User:2e839c1b-04ea-43c9-9bd1-614bf9586859", PrincipalKind.User, "2e839c1b-04ea-43c9-9bd1-614bf9586859")]
- public void ParsePrincipal___When_Called_With_Valid_String___Returns_Expected_Result(string representation, PrincipalKind expectedKind, string expectedObjectId)
+ public void ParsePrincipal___When_Called_With_Valid_String___Returns_Expected_Result(string representation, PrincipalKind expectedKind, string expectedPrincipalId)
{
var result = PrincipalConverter.ParsePrincipal(representation);
result.Kind.Should().Be(expectedKind);
- result.ObjectId.Should().Be(expectedObjectId);
+ result.PrincipalId.Should().Be(Guid.Parse(expectedPrincipalId));
}
[Theory]
[InlineData("ApiKey:")]
- [InlineData("ApiKey:123 ")]
+ [InlineData("ApiKey:123")]
[InlineData(" ApiKey:123")]
- [InlineData("ApiKey:4285231c-cd63-4eb5-adb5-1604d00b2e8e")]
+ [InlineData("ApiKey:4285231c-cd63-4eb5-adb5-1604d00b2e8")]
+ [InlineData("User:")]
[InlineData("User:123")]
+ [InlineData(" User:123")]
[InlineData("User:4285231c-cd63-4eb5-adb5-1604d00b2e8")]
public void ParsePrincipal___When_Called_With_Invalid_String___Throws_Exception(string representation)
{
diff --git a/src/Turnierplan.Dal/Converters/RoleAssignmentConverter.cs b/src/Turnierplan.Dal/Converters/PrincipalConverter.cs
similarity index 64%
rename from src/Turnierplan.Dal/Converters/RoleAssignmentConverter.cs
rename to src/Turnierplan.Dal/Converters/PrincipalConverter.cs
index a73302d8..9e4621c4 100644
--- a/src/Turnierplan.Dal/Converters/RoleAssignmentConverter.cs
+++ b/src/Turnierplan.Dal/Converters/PrincipalConverter.cs
@@ -14,21 +14,23 @@ public PrincipalConverter()
internal static string FormatPrincipal(Principal principal)
{
- return $"{principal.Kind}:{principal.ObjectId}";
+ return $"{principal.Kind}:{principal.PrincipalId}";
}
internal static Principal ParsePrincipal(string input)
{
var match = PrincipalRegex().Match(input);
- if (!match.Success || !Enum.TryParse(match.Groups["Kind"].Value, out var kind))
+ if (!match.Success
+ || !Enum.TryParse(match.Groups["Kind"].Value, out var kind)
+ || !Guid.TryParse(match.Groups["PrincipalId"].Value, out var principalId))
{
throw new TurnierplanException("Invalid principal string.");
}
- return new Principal(kind, match.Groups["ObjectId"].Value);
+ return new Principal(kind, principalId);
}
- [GeneratedRegex(@"^(?:(?ApiKey):(?\d+))$|^(?:(?User):(?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))$")]
+ [GeneratedRegex(@"^(?ApiKey|User):(?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$")]
private static partial Regex PrincipalRegex();
}
diff --git a/src/Turnierplan.Dal/EntityConfigurations/ApiKeyEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/ApiKeyEntityTypeConfiguration.cs
index 6720878e..6dbde546 100644
--- a/src/Turnierplan.Dal/EntityConfigurations/ApiKeyEntityTypeConfiguration.cs
+++ b/src/Turnierplan.Dal/EntityConfigurations/ApiKeyEntityTypeConfiguration.cs
@@ -16,6 +16,12 @@ public void Configure(EntityTypeBuilder builder)
builder.Property(x => x.Id)
.IsRequired();
+ builder.Property(x => x.PrincipalId)
+ .IsRequired();
+
+ builder.HasIndex(x => x.PrincipalId)
+ .IsUnique();
+
builder.Property(x => x.PublicId)
.HasConversion();
diff --git a/src/Turnierplan.Dal/EntityConfigurations/UserEntityTypeConfiguration.cs b/src/Turnierplan.Dal/EntityConfigurations/UserEntityTypeConfiguration.cs
index 56a15510..42d81879 100644
--- a/src/Turnierplan.Dal/EntityConfigurations/UserEntityTypeConfiguration.cs
+++ b/src/Turnierplan.Dal/EntityConfigurations/UserEntityTypeConfiguration.cs
@@ -15,6 +15,12 @@ public void Configure(EntityTypeBuilder builder)
builder.Property(x => x.Id)
.IsRequired();
+ builder.Property(x => x.PrincipalId)
+ .IsRequired();
+
+ builder.HasIndex(x => x.PrincipalId)
+ .IsUnique();
+
builder.Property(x => x.CreatedAt)
.IsRequired();
diff --git a/src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.Designer.cs b/src/Turnierplan.Dal/Migrations/20250621085230_Add_RBAC.Designer.cs
similarity index 99%
rename from src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.Designer.cs
rename to src/Turnierplan.Dal/Migrations/20250621085230_Add_RBAC.Designer.cs
index 24a40f37..5af528c7 100644
--- a/src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.Designer.cs
+++ b/src/Turnierplan.Dal/Migrations/20250621085230_Add_RBAC.Designer.cs
@@ -13,7 +13,7 @@
namespace Turnierplan.Dal.Migrations
{
[DbContext(typeof(TurnierplanContext))]
- [Migration("20250615154535_Add_RBAC")]
+ [Migration("20250621085230_Add_RBAC")]
partial class Add_RBAC
{
///
@@ -56,6 +56,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("OrganizationId")
.HasColumnType("bigint");
+ b.Property("PrincipalId")
+ .HasColumnType("uuid");
+
b.Property("PublicId")
.HasColumnType("bigint");
@@ -67,6 +70,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.HasIndex("OrganizationId");
+ b.HasIndex("PrincipalId")
+ .IsUnique();
+
b.HasIndex("PublicId")
.IsUnique();
@@ -676,6 +682,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.IsRequired()
.HasColumnType("text");
+ b.Property("PrincipalId")
+ .HasColumnType("uuid");
+
b.Property("SecurityStamp")
.HasColumnType("uuid");
@@ -684,6 +693,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.HasIndex("NormalizedEMail")
.IsUnique();
+ b.HasIndex("PrincipalId")
+ .IsUnique();
+
b.ToTable("Users", "turnierplan");
});
diff --git a/src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.cs b/src/Turnierplan.Dal/Migrations/20250621085230_Add_RBAC.cs
similarity index 88%
rename from src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.cs
rename to src/Turnierplan.Dal/Migrations/20250621085230_Add_RBAC.cs
index 99d20e83..f86f03e4 100644
--- a/src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.cs
+++ b/src/Turnierplan.Dal/Migrations/20250621085230_Add_RBAC.cs
@@ -40,6 +40,22 @@ protected override void Up(MigrationBuilder migrationBuilder)
nullable: false,
defaultValue: false);
+ migrationBuilder.AddColumn(
+ name: "PrincipalId",
+ schema: "turnierplan",
+ table: "Users",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn(
+ name: "PrincipalId",
+ schema: "turnierplan",
+ table: "ApiKeys",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
migrationBuilder.CreateTable(
name: "IAM_ApiKey",
schema: "turnierplan",
@@ -184,6 +200,25 @@ protected override void Up(MigrationBuilder migrationBuilder)
onDelete: ReferentialAction.Cascade);
});
+ migrationBuilder.Sql("""
+UPDATE turnierplan."ApiKeys" SET "PrincipalId" = gen_random_uuid();
+UPDATE turnierplan."Users" SET "PrincipalId" = gen_random_uuid();
+""");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Users_PrincipalId",
+ schema: "turnierplan",
+ table: "Users",
+ column: "PrincipalId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ApiKeys_PrincipalId",
+ schema: "turnierplan",
+ table: "ApiKeys",
+ column: "PrincipalId",
+ unique: true);
+
migrationBuilder.CreateIndex(
name: "IX_IAM_ApiKey_ApiKeyId",
schema: "turnierplan",
@@ -227,8 +262,9 @@ protected override void Up(MigrationBuilder migrationBuilder)
migrationBuilder.Sql("""
INSERT INTO turnierplan."IAM_Organization" ("Id", "OrganizationId", "CreatedAt", "Role", "Principal", "Description")
-SELECT gen_random_uuid(), "Organizations"."Id", NOW(), 1000, ('User:' || "Organizations"."OwnerId"), ''
-FROM turnierplan."Organizations";
+SELECT gen_random_uuid(), "Organizations"."Id", NOW(), 1000, ('User:' || "Users"."PrincipalId"), ''
+FROM turnierplan."Organizations"
+INNER JOIN turnierplan."Users" ON "Organizations"."OwnerId" = "Users"."Id";
""");
migrationBuilder.DropColumn(
@@ -264,11 +300,31 @@ protected override void Down(MigrationBuilder migrationBuilder)
name: "IAM_Venue",
schema: "turnierplan");
+ migrationBuilder.DropIndex(
+ name: "IX_Users_PrincipalId",
+ schema: "turnierplan",
+ table: "Users");
+
+ migrationBuilder.DropIndex(
+ name: "IX_ApiKeys_PrincipalId",
+ schema: "turnierplan",
+ table: "ApiKeys");
+
migrationBuilder.DropColumn(
name: "IsAdministrator",
schema: "turnierplan",
table: "Users");
+ migrationBuilder.DropColumn(
+ name: "PrincipalId",
+ schema: "turnierplan",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "PrincipalId",
+ schema: "turnierplan",
+ table: "ApiKeys");
+
migrationBuilder.AddColumn(
name: "OwnerId",
schema: "turnierplan",
diff --git a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs
index 0b302773..ee029614 100644
--- a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs
+++ b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs
@@ -53,6 +53,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("OrganizationId")
.HasColumnType("bigint");
+ b.Property("PrincipalId")
+ .HasColumnType("uuid");
+
b.Property("PublicId")
.HasColumnType("bigint");
@@ -64,6 +67,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("OrganizationId");
+ b.HasIndex("PrincipalId")
+ .IsUnique();
+
b.HasIndex("PublicId")
.IsUnique();
@@ -673,6 +679,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.IsRequired()
.HasColumnType("text");
+ b.Property("PrincipalId")
+ .HasColumnType("uuid");
+
b.Property("SecurityStamp")
.HasColumnType("uuid");
@@ -681,6 +690,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("NormalizedEMail")
.IsUnique();
+ b.HasIndex("PrincipalId")
+ .IsUnique();
+
b.ToTable("Users", "turnierplan");
});