Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Gateway;
|
||||
|
||||
namespace StellaOps.Gateway.WebService.Authorization;
|
||||
|
||||
public sealed class AuthorizationMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IEffectiveClaimsStore _claimsStore;
|
||||
private readonly ILogger<AuthorizationMiddleware> _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public AuthorizationMiddleware(
|
||||
RequestDelegate next,
|
||||
IEffectiveClaimsStore claimsStore,
|
||||
ILogger<AuthorizationMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_claimsStore = claimsStore;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (!context.Items.TryGetValue(RouterHttpContextKeys.EndpointDescriptor, out var endpointObj) ||
|
||||
endpointObj is not EndpointDescriptor endpoint)
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var effectiveClaims = _claimsStore.GetEffectiveClaims(
|
||||
endpoint.ServiceName,
|
||||
endpoint.Method,
|
||||
endpoint.Path);
|
||||
|
||||
if (effectiveClaims.Count == 0)
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var required in effectiveClaims)
|
||||
{
|
||||
var userClaims = context.User.Claims;
|
||||
var hasClaim = required.Value == null
|
||||
? userClaims.Any(c => c.Type == required.Type)
|
||||
: userClaims.Any(c => c.Type == required.Type && c.Value == required.Value);
|
||||
|
||||
if (!hasClaim)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Authorization failed for {Method} {Path}: user lacks claim {ClaimType}={ClaimValue}",
|
||||
endpoint.Method,
|
||||
endpoint.Path,
|
||||
required.Type,
|
||||
required.Value ?? "(any)");
|
||||
|
||||
await WriteForbiddenAsync(context, endpoint, required);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private static Task WriteForbiddenAsync(
|
||||
HttpContext context,
|
||||
EndpointDescriptor endpoint,
|
||||
ClaimRequirement required)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
context.Response.ContentType = "application/json; charset=utf-8";
|
||||
|
||||
var payload = new AuthorizationFailureResponse(
|
||||
Error: "forbidden",
|
||||
Message: "Authorization failed: missing required claim",
|
||||
RequiredClaimType: required.Type,
|
||||
RequiredClaimValue: required.Value,
|
||||
Service: endpoint.ServiceName,
|
||||
Version: endpoint.Version);
|
||||
|
||||
return JsonSerializer.SerializeAsync(context.Response.Body, payload, JsonOptions, context.RequestAborted);
|
||||
}
|
||||
|
||||
private sealed record AuthorizationFailureResponse(
|
||||
string Error,
|
||||
string Message,
|
||||
string RequiredClaimType,
|
||||
string? RequiredClaimValue,
|
||||
string Service,
|
||||
string Version);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Gateway.Authorization;
|
||||
|
||||
namespace StellaOps.Gateway.WebService.Authorization;
|
||||
|
||||
public sealed class EffectiveClaimsStore : IEffectiveClaimsStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<EndpointKey, IReadOnlyList<ClaimRequirement>> _microserviceClaims = new();
|
||||
private readonly ConcurrentDictionary<EndpointKey, IReadOnlyList<ClaimRequirement>> _authorityClaims = new();
|
||||
private readonly ILogger<EffectiveClaimsStore> _logger;
|
||||
|
||||
public EffectiveClaimsStore(ILogger<EffectiveClaimsStore> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ClaimRequirement> GetEffectiveClaims(string serviceName, string method, string path)
|
||||
{
|
||||
var key = EndpointKey.Create(serviceName, method, path);
|
||||
|
||||
if (_authorityClaims.TryGetValue(key, out var authorityClaims))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Using Authority claims for {Endpoint}: {ClaimCount} claims",
|
||||
key,
|
||||
authorityClaims.Count);
|
||||
return authorityClaims;
|
||||
}
|
||||
|
||||
if (_microserviceClaims.TryGetValue(key, out var msClaims))
|
||||
{
|
||||
return msClaims;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public void UpdateFromMicroservice(string serviceName, IReadOnlyList<EndpointDescriptor> endpoints)
|
||||
{
|
||||
foreach (var endpoint in endpoints)
|
||||
{
|
||||
var key = EndpointKey.Create(serviceName, endpoint.Method, endpoint.Path);
|
||||
var claims = endpoint.RequiringClaims ?? [];
|
||||
|
||||
if (claims.Count > 0)
|
||||
{
|
||||
_microserviceClaims[key] = claims;
|
||||
_logger.LogDebug(
|
||||
"Registered {ClaimCount} claims from microservice for {Endpoint}",
|
||||
claims.Count,
|
||||
key);
|
||||
}
|
||||
else
|
||||
{
|
||||
_microserviceClaims.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateFromAuthority(IReadOnlyDictionary<EndpointKey, IReadOnlyList<ClaimRequirement>> overrides)
|
||||
{
|
||||
_authorityClaims.Clear();
|
||||
|
||||
foreach (var (key, claims) in overrides)
|
||||
{
|
||||
if (claims.Count > 0)
|
||||
{
|
||||
_authorityClaims[key] = claims;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Updated Authority claims: {EndpointCount} endpoints with overrides",
|
||||
overrides.Count);
|
||||
}
|
||||
|
||||
public void RemoveService(string serviceName)
|
||||
{
|
||||
var normalizedServiceName = serviceName.ToLowerInvariant();
|
||||
var keysToRemove = _microserviceClaims.Keys
|
||||
.Where(k => k.ServiceName == normalizedServiceName)
|
||||
.ToList();
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_microserviceClaims.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Removed {Count} endpoint claims for service {ServiceName}",
|
||||
keysToRemove.Count,
|
||||
serviceName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Gateway.Authorization;
|
||||
|
||||
namespace StellaOps.Gateway.WebService.Authorization;
|
||||
|
||||
public interface IEffectiveClaimsStore
|
||||
{
|
||||
IReadOnlyList<ClaimRequirement> GetEffectiveClaims(string serviceName, string method, string path);
|
||||
|
||||
void UpdateFromMicroservice(string serviceName, IReadOnlyList<EndpointDescriptor> endpoints);
|
||||
|
||||
void UpdateFromAuthority(IReadOnlyDictionary<EndpointKey, IReadOnlyList<ClaimRequirement>> overrides);
|
||||
|
||||
void RemoveService(string serviceName);
|
||||
}
|
||||
Reference in New Issue
Block a user