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/>
|
/// <inheritdoc/>
|
||||||
public async Task<UserDto?> GetByIdentityUserIdAsync(Guid identityUserId, CancellationToken ct = default)
|
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
|
var profile = await _appDb.UserProfiles
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
.Include(p => p.Clubs)
|
||||||
|
.ThenInclude(c => c.Club)
|
||||||
|
.Include(p => p.Clubs)
|
||||||
|
.ThenInclude(c => c.RoleAssignments)
|
||||||
.SingleOrDefaultAsync(p => p.IdentityUserId == identityUserId, ct);
|
.SingleOrDefaultAsync(p => p.IdentityUserId == identityUserId, ct);
|
||||||
|
|
||||||
if (profile == null)
|
if (profile == null)
|
||||||
|
|
@ -156,6 +160,16 @@ public class UserService : IUserService
|
||||||
var roles = await _userManager.GetRolesAsync(identity);
|
var roles = await _userManager.GetRolesAsync(identity);
|
||||||
dto.Identity.Roles = roles.ToList();
|
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;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ public static class DependencyInjection
|
||||||
options.AddPolicy("ClubAdmin", p => p.Requirements.Add(new ClubRoleRequirement("Admin")));
|
options.AddPolicy("ClubAdmin", p => p.Requirements.Add(new ClubRoleRequirement("Admin")));
|
||||||
});
|
});
|
||||||
services.AddSingleton<IAuthorizationHandler, ClubRoleHandler>();
|
services.AddSingleton<IAuthorizationHandler, ClubRoleHandler>();
|
||||||
|
services.AddSingleton<IAuthorizationHandler, ClubRoleResourceHandler>();
|
||||||
|
|
||||||
// HTTP Context Accessor (für CurrentUserService)
|
// HTTP Context Accessor (für CurrentUserService)
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,31 @@ public sealed class ClubRoleRequirement : IAuthorizationRequirement
|
||||||
public string RequiredRole { get; }
|
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(
|
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,
|
AuthorizationHandlerContext context,
|
||||||
ClubRoleRequirement requirement,
|
ClubRoleRequirement requirement,
|
||||||
Guid clubId)
|
Guid clubId)
|
||||||
{
|
{
|
||||||
// Hierarchie (optional): Admin >= Editor >= Viewer
|
|
||||||
// Du kannst das anpassen/erweitern.
|
|
||||||
static int Rank(string role) => role switch
|
static int Rank(string role) => role switch
|
||||||
{
|
{
|
||||||
"Admin" => 3,
|
"Admin" => 3,
|
||||||
|
|
@ -33,7 +49,7 @@ public sealed class ClubRoleHandler : AuthorizationHandler<ClubRoleRequirement,
|
||||||
|
|
||||||
var requiredRank = Rank(requirement.RequiredRole);
|
var requiredRank = Rank(requirement.RequiredRole);
|
||||||
|
|
||||||
// Alle club_role Claims lesen
|
// Read all club_role claims
|
||||||
var claims = context.User.FindAll("club_role");
|
var claims = context.User.FindAll("club_role");
|
||||||
foreach (var c in claims)
|
foreach (var c in claims)
|
||||||
{
|
{
|
||||||
|
|
@ -55,3 +71,44 @@ public sealed class ClubRoleHandler : AuthorizationHandler<ClubRoleRequirement,
|
||||||
return Task.CompletedTask;
|
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 IUserService _userService;
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
|
private readonly ICurrentClubContext _currentClubContext;
|
||||||
private readonly ILogger<AuthEffects> _logger;
|
private readonly ILogger<AuthEffects> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the AuthEffects class.
|
/// Initializes a new instance of the AuthEffects class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userService">The user service.</param>
|
|
||||||
/// <param name="authorizationService">The authorization service.</param>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
public AuthEffects(
|
public AuthEffects(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
|
ICurrentClubContext currentClubContext,
|
||||||
ILogger<AuthEffects> logger)
|
ILogger<AuthEffects> logger)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
|
_currentClubContext = currentClubContext;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,13 +50,32 @@ namespace Koogle.Web.Store.AuthState
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//var roles = await _authorizationService.GetCurrentUserRolesAsync();
|
// Merge Identity roles with club-specific roles for current club
|
||||||
//var companyIds = await _authorizationService.GetAccessibleCompanyIdsAsync();
|
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}",
|
_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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue