fix permissions;

Summary of all fixes

  3 files changed:

  1. src/Koogle.Application/Services/UserService.cs:140-171
    - GetByIdentityUserIdAsync now includes .Include(p => p.Clubs) and maps ClubMemberships
  2. src/Koogle.Web/Store/AuthState/AuthEffects.cs:53-73
    - Merges club-specific roles from ClubMemberships into AuthState roles
  3. src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs:17-114
    - Changed ClubRoleHandler to extend AuthorizationHandler<ClubRoleRequirement> (no resource)
    - Reads current_club_id from claims to determine club context
    - Added ClubRoleResourceHandler for resource-based auth (explicit clubId)
  4. src/Koogle.Infrastructure/DependencyInjection.cs:72
    - Registered ClubRoleResourceHandler

  The [Authorize(Policy = "ClubViewer")] attribute now uses current_club_id claim set during login to check club roles.
This commit is contained in:
beo3000 2025-12-24 15:44:52 +01:00
parent d74cdb678d
commit c3839d2363
4 changed files with 103 additions and 13 deletions

View File

@ -136,9 +136,13 @@ public class UserService : IUserService
/// <inheritdoc/>
public async Task<UserDto?> GetByIdentityUserIdAsync(Guid identityUserId, CancellationToken ct = default)
{
// Domain-Profil laden (SoftDelete Filter wirkt)
// Domain-Profil laden mit Club-Memberships (SoftDelete Filter wirkt)
var profile = await _appDb.UserProfiles
.AsNoTracking()
.Include(p => p.Clubs)
.ThenInclude(c => c.Club)
.Include(p => p.Clubs)
.ThenInclude(c => c.RoleAssignments)
.SingleOrDefaultAsync(p => p.IdentityUserId == identityUserId, ct);
if (profile == null)
@ -156,6 +160,16 @@ public class UserService : IUserService
var roles = await _userManager.GetRolesAsync(identity);
dto.Identity.Roles = roles.ToList();
// Map club memberships
dto.ClubMemberships = profile.Clubs.Select(c => new UserClubMembershipDto
{
ClubId = c.ClubId,
ClubName = c.Club?.Name ?? "",
IsDefault = c.IsDefault,
AssignedAt = c.AssignedAt,
Roles = c.RoleAssignments.Select(ra => ra.RoleName).ToList()
}).ToList();
return dto;
}

View File

@ -69,7 +69,7 @@ public static class DependencyInjection
options.AddPolicy("ClubAdmin", p => p.Requirements.Add(new ClubRoleRequirement("Admin")));
});
services.AddSingleton<IAuthorizationHandler, ClubRoleHandler>();
services.AddSingleton<IAuthorizationHandler, ClubRoleResourceHandler>();
// HTTP Context Accessor (für CurrentUserService)
services.AddHttpContextAccessor();

View File

@ -14,15 +14,31 @@ public sealed class ClubRoleRequirement : IAuthorizationRequirement
public string RequiredRole { get; }
}
public sealed class ClubRoleHandler : AuthorizationHandler<ClubRoleRequirement, Guid>
/// <summary>
/// Handles ClubRoleRequirement when no resource is passed (e.g., [Authorize(Policy = "ClubViewer")]).
/// Uses current_club_id claim to determine the club context.
/// </summary>
public sealed class ClubRoleHandler : AuthorizationHandler<ClubRoleRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ClubRoleRequirement requirement)
{
// Get current club from claims
var currentClubClaim = context.User.FindFirst("current_club_id");
if (currentClubClaim == null || !Guid.TryParse(currentClubClaim.Value, out var clubId))
{
return Task.CompletedTask; // No club context, fail
}
return CheckClubRole(context, requirement, clubId);
}
private static Task CheckClubRole(
AuthorizationHandlerContext context,
ClubRoleRequirement requirement,
Guid clubId)
{
// Hierarchie (optional): Admin >= Editor >= Viewer
// Du kannst das anpassen/erweitern.
static int Rank(string role) => role switch
{
"Admin" => 3,
@ -33,7 +49,7 @@ public sealed class ClubRoleHandler : AuthorizationHandler<ClubRoleRequirement,
var requiredRank = Rank(requirement.RequiredRole);
// Alle club_role Claims lesen
// Read all club_role claims
var claims = context.User.FindAll("club_role");
foreach (var c in claims)
{
@ -55,3 +71,44 @@ public sealed class ClubRoleHandler : AuthorizationHandler<ClubRoleRequirement,
return Task.CompletedTask;
}
}
/// <summary>
/// Handles ClubRoleRequirement when a specific clubId resource is passed.
/// </summary>
public sealed class ClubRoleResourceHandler : AuthorizationHandler<ClubRoleRequirement, Guid>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ClubRoleRequirement requirement,
Guid clubId)
{
static int Rank(string role) => role switch
{
"Admin" => 3,
"Editor" => 2,
"Viewer" => 1,
_ => 0
};
var requiredRank = Rank(requirement.RequiredRole);
var claims = context.User.FindAll("club_role");
foreach (var c in claims)
{
var parts = c.Value.Split(':', 2);
if (parts.Length != 2) continue;
if (Guid.TryParse(parts[0], out var claimClubId) && claimClubId == clubId)
{
var userRole = parts[1];
if (Rank(userRole) >= requiredRank)
{
context.Succeed(requirement);
break;
}
}
}
return Task.CompletedTask;
}
}

View File

@ -12,21 +12,21 @@ namespace Koogle.Web.Store.AuthState
{
private readonly IUserService _userService;
private readonly IAuthorizationService _authorizationService;
private readonly ICurrentClubContext _currentClubContext;
private readonly ILogger<AuthEffects> _logger;
/// <summary>
/// Initializes a new instance of the AuthEffects class.
/// </summary>
/// <param name="userService">The user service.</param>
/// <param name="authorizationService">The authorization service.</param>
/// <param name="logger">The logger.</param>
public AuthEffects(
IUserService userService,
IAuthorizationService authorizationService,
ICurrentClubContext currentClubContext,
ILogger<AuthEffects> logger)
{
_userService = userService;
_authorizationService = authorizationService;
_currentClubContext = currentClubContext;
_logger = logger;
}
@ -50,13 +50,32 @@ namespace Koogle.Web.Store.AuthState
return;
}
//var roles = await _authorizationService.GetCurrentUserRolesAsync();
//var companyIds = await _authorizationService.GetAccessibleCompanyIdsAsync();
// Merge Identity roles with club-specific roles for current club
var roles = currentUser.Identity.Roles.ToList();
var currentClubId = _currentClubContext.ClubId;
dispatcher.Dispatch(new AuthState.InitializeAuthSuccessAction(currentUser, currentUser.Identity.Roles));
if (currentClubId != Guid.Empty)
{
var clubMembership = currentUser.ClubMemberships
.FirstOrDefault(m => m.ClubId == currentClubId);
if (clubMembership != null)
{
// Add club roles that aren't already in the list
foreach (var clubRole in clubMembership.Roles)
{
if (!roles.Contains(clubRole))
{
roles.Add(clubRole);
}
}
}
}
dispatcher.Dispatch(new AuthState.InitializeAuthSuccessAction(currentUser, roles));
_logger.LogInformation("Auth initialized for user {DisplayName} with roles {Roles}",
currentUser.DisplayName, string.Join(", ", currentUser.Identity.Roles));
currentUser.DisplayName, string.Join(", ", roles));
}
catch (Exception ex)
{