Skip to main content
Authorization tamamen uygulama katmanına aittir. Keycloak rol ve permission claim’lerini token’a koyar; “bu endpoint bu izni gerektiriyor” kararını .NET tarafı verir. İki seviyede çalışır: HTTP authorization (ASP.NET policy/handler) ve MediatR pipeline (defense-in-depth).

DynamicPermissionPolicyProvider

[Authorize(Policy = "...")] için her permission’a tek tek named policy tanımlamak yerine, policy adı bir konvansiyonla çözülür (SeedWork/Authorization/DynamicPermissionPolicyProvider.cs):
  • Permission:{code}PermissionAuthorizationRequirement(code)
  • Scope:{name}ScopeAuthorizationRequirement(name)
public async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
    if (policyName.StartsWith(RequirePermissionAttribute.PolicyPrefix, ...)) // "Permission:"
    {
        var permission = policyName[RequirePermissionAttribute.PolicyPrefix.Length..];
        return new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddRequirements(new PermissionAuthorizationRequirement(permission))
            .AddRequirements(new ActiveAccountRequirement())
            .Build();
    }
    // "Scope:" → ScopeAuthorizationRequirement
    // bilinmeyen policy → default provider'a düşer (ama ActiveAccountRequirement eklenir)
}
Provider her policy’yi (named, fallback ve Permission:/Scope: dinamik policy’leri) ActiveAccountRequirement ile augment eder. Böylece [Authorize] taşıyan hiçbir endpoint’e Pending/Banned/Suspended hesap erişemez. Augment idempotenttir (zaten varsa tekrar eklemez).

[RequirePermission("...")]

RequirePermissionAttribute aslında bir AuthorizeAttribute’tur; policy adını Permission: + kod olarak üretir:
[RequirePermission("users:read")]   // → Policy = "Permission:users:read"
public async Task<IActionResult> List(...) => ...;

Handler’lar

Program.cs → RBAC bölümünde üç handler kayıtlıdır:
builder.Services.AddSingleton<IAuthorizationPolicyProvider, DynamicPermissionPolicyProvider>();
builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
builder.Services.AddScoped<IAuthorizationHandler, ScopeAuthorizationHandler>();
builder.Services.AddScoped<IAuthorizationHandler, ActiveAccountAuthorizationHandler>();

PermissionAuthorizationHandler

permissions claim’lerini kontrol eder. *:* wildcard her izni geçirir (SuperAdmin):
var permissions = context.User.Claims
    .Where(c => c.Type == DiyanetCleanArchitectureClaimTypes.Permissions)
    .Select(c => c.Value).ToHashSet();

bool granted = permissions.Contains("*:*") ||
               permissions.Contains(requirement.Permission);
if (granted) context.Succeed(requirement);

ScopeAuthorizationHandler

scope claim’ini kontrol eder; "shared" scope her iki uygulama için geçerlidir, *:* yine geçer:
bool granted = scope == requirement.Scope ||
               scope == "shared" ||
               context.User.Claims.Any(c => c.Type == Permissions && c.Value == "*:*");

ActiveAccountAuthorizationHandler

account_status claim’inin Active olmasını gerektirir; endpoint [AllowPendingAccount] taşıyorsa atlanır:
var endpoint = httpContext.GetEndpoint();
if (endpoint?.Metadata.GetMetadata<AllowPendingAccountAttribute>() is not null)
{
    context.Succeed(requirement);   // Pending/Banned/Suspended bypass
    return Task.CompletedTask;
}
var status = context.User.FindFirstValue(DiyanetCleanArchitectureClaimTypes.AccountStatus);
if (string.Equals(status, "Active", StringComparison.OrdinalIgnoreCase))
    context.Succeed(requirement);
account_status her istekte UserContextClaimsTransformation / CitizenContextClaimsTransformation tarafından DB’den güncel olarak yazılır. Olası değerler: Active, Pending, Banned, Suspended, Draft, Unknown.
/me/status, logout, profil okuma gibi kendi durumunu öğrenmesi gereken endpoint’ler [AllowPendingAccount] ile işaretlenir — aksi halde yeni kayıt olmuş (Pending) kullanıcı durumunu hiç öğrenemez.

AuthPolicies — named scheme policy’leri

Bazı endpoint’ler için “sadece doğru realm’den authenticated olsun yeter” gerekir. Program.cs üç named policy tanımlar (SeedWork/Authorization/AuthPolicies.cs):
options.AddPolicy(AuthPolicies.PersonelAuthenticated, p => p
    .AddAuthenticationSchemes(KeycloakSchemeNames.Personel).RequireAuthenticatedUser());
options.AddPolicy(AuthPolicies.VatandasAuthenticated, p => p
    .AddAuthenticationSchemes(KeycloakSchemeNames.Vatandas).RequireAuthenticatedUser());
options.AddPolicy(AuthPolicies.AnyKeycloakAuthenticated, p => p
    .AddAuthenticationSchemes(KeycloakSchemeNames.Vatandas, KeycloakSchemeNames.Personel)
    .RequireAuthenticatedUser());
Bare [Authorize(AuthenticationSchemes = X)] policy’ye hiçbir requirement eklemez; boş-requirement policy bazı senaryolarda yanlışlıkla 403 dönebilir. Named policy RequireAuthenticatedUser() çağrısıyla DenyAnonymousAuthorizationRequirement ekler → policy düzgün değerlendirilir.
[Authorize(Policy = AuthPolicies.PersonelAuthenticated)]
public async Task<IActionResult> Me(...) => ...;

Pipeline-level defense-in-depth (MediatR)

HTTP authorization ilk savunma hattıdır. İkinci hat, MediatR AuthorizationPipelineBehavior’dır (Application/SeedWork/PipelineBehaviors/AuthorizationPipelineBehavior.cs). Command/query şu marker’ları implement ederse handler çalışmadan önce kontrol edilir:
  • IRequirePermissionsRequiredPermissions dizisindeki tüm izinler olmalı (AND).
  • IRequireTenantAccess → token tenant’ı ile command’daki TenantId örtüşmeli.
public class UpdateStaffCommand : IRequest<Result>, IRequirePermissions
{
    public string[] RequiredPermissions => [ "users:write" ];
}
Eksik izin / tenant uyuşmazlığı → ForbiddenExceptionExceptionPipelineBehavior bunu HTTP 403’e çevirir. Bu sayede bir endpoint controller attribute’unu unutsa bile use-case seviyesinde korunur.

Roller ve RolePermission seed

Sabit dört sistem rolü vardır (Domain/AggregatesModel/RoleAggregate/Role.cs, Enumeration):
IdRolAçıklama
1SuperAdminSistem genelinde tam yetki
2AdminKurum/tenant yöneticisi
3StaffStandart personel — okuma + sınırlı işlem
4ReadOnlySalt okunur (raporlama, denetim)
Rol → izin haritası DB’de role_permission tablosunda seed edilir (RolePermissionEntityTypeConfiguration). Varsayılan matrisin bir kesiti:
İzinSuperAdminAdminStaffReadOnly
users:read
users:write
users:delete
roles:manage
tenants:switch
reports:read
faqs:write
bagisBasvuru:onayla
Seed ID’leri deterministiktir (a{roleId}-...-{permissionId}), böylece migration tekrarlarında duplicate oluşmaz. Aktif (role_id, permission_id) çifti partial unique index ile korunur (deleted_at IS NULL), soft-delete sonrası yeniden atama mümkündür.

İlgili

Authentication

Token doğrulama ve claim üretimi.

Multi-Tenancy

IRequireTenantAccess ve tenant izolasyonu.

Realm Yapısı

Rollerin Keycloak’taki karşılığı, permissions mapper.

Application Authorization

MediatR pipeline ve marker arayüzleri.