feature: RBAC + Clinical Audit Logging

This commit is contained in:
voltsrage
2026-06-21 15:46:55 +08:00
parent a43db52813
commit 5af6ab490e
83 changed files with 4281 additions and 70 deletions
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Authorization;
public class AuthorizePermissionAttribute : AuthorizeAttribute
{
public AuthorizePermissionAttribute(string permission)
{
Policy = $"perm:{permission}";
}
}
@@ -0,0 +1,19 @@
public static class ClinicalPermissions
{
public const string PatientsRead = "patients:read";
public const string PatientsWrite = "patients:write";
public const string EncountersRead = "encounters:read";
public const string EncountersWrite = "encounters:write";
public const string ObservationsIngest = "observations:ingest";
public const string AlertsRead = "alerts:read";
public const string AlertsAcknowledge = "alerts:acknowledge";
public const string AlertsResolve = "alerts:resolve";
public const string ThresholdsRead = "thresholds:read";
public const string ThresholdsWrite = "thresholds:write";
public const string AnalyticsRead = "analytics:read";
public const string OrdersWrite = "orders:write";
public const string MedicationsWrite = "medications:write";
public const string FhirIngest = "fhir:ingest";
public const string AuditRead = "audit:read";
public const string UsersAdmin = "users:admin";
}
@@ -0,0 +1,63 @@
public static class ClinicalRolePermissionMap
{
private static readonly Dictionary<ClinicalRole, HashSet<string>> _map = new()
{
[ClinicalRole.Nurse] = new(StringComparer.Ordinal)
{
ClinicalPermissions.PatientsRead,
ClinicalPermissions.EncountersRead,
ClinicalPermissions.ObservationsIngest,
ClinicalPermissions.AlertsRead,
ClinicalPermissions.AlertsAcknowledge,
ClinicalPermissions.AlertsResolve,
ClinicalPermissions.ThresholdsRead,
ClinicalPermissions.AnalyticsRead,
ClinicalPermissions.OrdersWrite,
ClinicalPermissions.MedicationsWrite,
},
[ClinicalRole.Physician] = new(StringComparer.Ordinal)
{
ClinicalPermissions.PatientsRead,
ClinicalPermissions.EncountersRead,
ClinicalPermissions.EncountersWrite,
ClinicalPermissions.ObservationsIngest,
ClinicalPermissions.AlertsRead,
ClinicalPermissions.AlertsAcknowledge,
ClinicalPermissions.AlertsResolve,
ClinicalPermissions.ThresholdsRead,
ClinicalPermissions.AnalyticsRead,
ClinicalPermissions.OrdersWrite,
ClinicalPermissions.MedicationsWrite,
},
[ClinicalRole.Admin] = new(StringComparer.Ordinal)
{
ClinicalPermissions.PatientsRead,
ClinicalPermissions.PatientsWrite,
ClinicalPermissions.EncountersRead,
ClinicalPermissions.EncountersWrite,
ClinicalPermissions.ObservationsIngest,
ClinicalPermissions.AlertsRead,
ClinicalPermissions.AlertsAcknowledge,
ClinicalPermissions.AlertsResolve,
ClinicalPermissions.ThresholdsRead,
ClinicalPermissions.ThresholdsWrite,
ClinicalPermissions.AnalyticsRead,
ClinicalPermissions.OrdersWrite,
ClinicalPermissions.MedicationsWrite,
ClinicalPermissions.FhirIngest,
ClinicalPermissions.AuditRead,
ClinicalPermissions.UsersAdmin,
},
[ClinicalRole.Integration] = new(StringComparer.Ordinal)
{
ClinicalPermissions.PatientsWrite,
ClinicalPermissions.EncountersWrite,
ClinicalPermissions.ObservationsIngest,
ClinicalPermissions.MedicationsWrite,
ClinicalPermissions.FhirIngest,
},
};
public static bool HasPermission(ClinicalRole role, string permission) =>
_map.TryGetValue(role, out var perms) && perms.Contains(permission);
}
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Authorization;
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
var roleClaim = context.User.FindFirst("clinical_role")?.Value;
if (roleClaim is null)
return Task.CompletedTask;
var role = ClinicalRoleExtensions.FromDbString(roleClaim);
if (ClinicalRolePermissionMap.HasPermission(role, requirement.Permission))
context.Succeed(requirement);
return Task.CompletedTask;
}
}
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
public class PermissionPolicyProvider : IAuthorizationPolicyProvider
{
private readonly DefaultAuthorizationPolicyProvider _fallback;
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options)
{
_fallback = new DefaultAuthorizationPolicyProvider(options);
}
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith("perm:", StringComparison.Ordinal))
{
var permission = policyName["perm:".Length..];
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new PermissionRequirement(permission))
.Build();
return Task.FromResult<AuthorizationPolicy?>(policy);
}
return _fallback.GetPolicyAsync(policyName);
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() =>
_fallback.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() =>
_fallback.GetFallbackPolicyAsync();
}
@@ -0,0 +1,7 @@
using Microsoft.AspNetCore.Authorization;
public class PermissionRequirement : IAuthorizationRequirement
{
public string Permission { get; }
public PermissionRequirement(string permission) => Permission = permission;
}