Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/Turnierplan.App.Test.Unit/Security/AccessValidatorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
}
}
<!-- TODO: Resolve ApiKey/User name -->
<span>{{ assignment.principal.objectId }}</span>
<span>{{ assignment.principal.principalId }}</span>
@if (canDeleteAssignment(assignment)) {
<span class="flex-grow-1"></span>
<tp-delete-button [reducedFootprint]="true" (confirmed)="removeRoleAssignment(assignment.id)" />
Expand Down
2 changes: 1 addition & 1 deletion src/Turnierplan.App/Endpoints/EndpointBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ private static async Task<IResult> 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);
}
Expand Down
16 changes: 12 additions & 4 deletions src/Turnierplan.App/Extensions/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PrincipalKind>(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.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Turnierplan.App/Models/PrincipalDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
4 changes: 3 additions & 1 deletion src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -79,7 +80,8 @@ protected override async Task<AuthenticateResult> 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 ]);
Expand Down
3 changes: 2 additions & 1 deletion src/Turnierplan.App/Security/ClaimTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
6 changes: 5 additions & 1 deletion src/Turnierplan.Core/ApiKey/ApiKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
organization._apiKeys.Add(this);

Id = 0;
PrincipalId = Guid.NewGuid();
PublicId = new PublicId.PublicId();
Organization = organization;
Name = name;
Expand All @@ -23,9 +24,10 @@
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)

Check warning on line 27 in src/Turnierplan.Core/ApiKey/ApiKey.cs

View workflow job for this annotation

GitHub Actions / Validate

Constructor has 9 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)
{
Id = id;
PrincipalId = principalId;
PublicId = publicId;
Name = name;
Description = description;
Expand All @@ -37,6 +39,8 @@

public override long Id { get; protected set; }

public Guid PrincipalId { get; }

public PublicId.PublicId PublicId { get; }

public Organization.Organization Organization { get; internal set; } = null!;
Expand Down
4 changes: 2 additions & 2 deletions src/Turnierplan.Core/Extensions/PrincipalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
8 changes: 4 additions & 4 deletions src/Turnierplan.Core/RoleAssignment/Principal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
6 changes: 5 additions & 1 deletion src/Turnierplan.Core/User/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
email = email.Trim();

Id = Guid.NewGuid();
PrincipalId = Guid.NewGuid();
CreatedAt = DateTime.UtcNow;
Name = name;
EMail = email;
Expand All @@ -19,9 +20,10 @@
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)

Check warning on line 23 in src/Turnierplan.Core/User/User.cs

View workflow job for this annotation

GitHub Actions / Validate

Constructor has 10 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)
{
Id = id;
PrincipalId = principalId;
CreatedAt = createdAt;
Name = name;
EMail = eMail;
Expand All @@ -34,6 +36,8 @@

public override Guid Id { get; protected set; }

public Guid PrincipalId { get; }

public DateTime CreatedAt { get; }

public string Name { get; set; }
Expand Down
18 changes: 10 additions & 8 deletions src/Turnierplan.Dal.Test.Unit/Converters/PrincipalConverterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,36 @@ 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);

result.Should().Be(expectedResult);
}

[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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PrincipalKind>(match.Groups["Kind"].Value, out var kind))
if (!match.Success
|| !Enum.TryParse<PrincipalKind>(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(@"^(?:(?<Kind>ApiKey):(?<ObjectId>\d+))$|^(?:(?<Kind>User):(?<ObjectId>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))$")]
[GeneratedRegex(@"^(?<Kind>ApiKey|User):(?<PrincipalId>[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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public void Configure(EntityTypeBuilder<ApiKey> 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<PublicIdConverter>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public void Configure(EntityTypeBuilder<User> 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();

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading