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:
parent
d74cdb678d
commit
c3839d2363
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue