Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Auth;
|
||||
|
||||
internal sealed class AnonymousAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
public AnonymousAuthenticationHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var identity = new ClaimsIdentity(Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Auth;
|
||||
|
||||
internal sealed class ClaimsTenantContextAccessor : ITenantContextAccessor
|
||||
{
|
||||
public TenantContext GetTenant(HttpContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var principal = context.User ?? throw new UnauthorizedAccessException("Authentication required.");
|
||||
if (principal.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Authentication required.");
|
||||
}
|
||||
|
||||
var tenant = principal.FindFirstValue(StellaOpsClaimTypes.Tenant);
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
throw new InvalidOperationException("Authenticated principal is missing required tenant claim.");
|
||||
}
|
||||
|
||||
return new TenantContext(tenant.Trim());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Auth;
|
||||
|
||||
internal sealed class HeaderScopeAuthorizer : IScopeAuthorizer
|
||||
{
|
||||
private const string ScopeHeader = "X-Scopes";
|
||||
|
||||
public void EnsureScope(HttpContext context, string requiredScope)
|
||||
{
|
||||
if (!context.Request.Headers.TryGetValue(ScopeHeader, out var values))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Missing required header '{ScopeHeader}'.");
|
||||
}
|
||||
|
||||
var scopeBuffer = string.Join(' ', values.ToArray());
|
||||
if (string.IsNullOrWhiteSpace(scopeBuffer))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Header '{ScopeHeader}' cannot be empty.");
|
||||
}
|
||||
|
||||
var scopes = scopeBuffer
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!scopes.Contains(requiredScope))
|
||||
{
|
||||
throw new InvalidOperationException($"Missing required scope '{requiredScope}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Auth;
|
||||
|
||||
internal sealed class HeaderTenantContextAccessor : ITenantContextAccessor
|
||||
{
|
||||
private const string TenantHeader = "X-Tenant-Id";
|
||||
|
||||
public TenantContext GetTenant(HttpContext context)
|
||||
{
|
||||
if (!context.Request.Headers.TryGetValue(TenantHeader, out var values))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Missing required header '{TenantHeader}'.");
|
||||
}
|
||||
|
||||
var tenantId = values.ToString().Trim();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Header '{TenantHeader}' cannot be empty.");
|
||||
}
|
||||
|
||||
return new TenantContext(tenantId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Auth;
|
||||
|
||||
public interface IScopeAuthorizer
|
||||
{
|
||||
void EnsureScope(HttpContext context, string requiredScope);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Auth;
|
||||
|
||||
public interface ITenantContextAccessor
|
||||
{
|
||||
TenantContext GetTenant(HttpContext context);
|
||||
}
|
||||
|
||||
public sealed record TenantContext(string TenantId);
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Auth;
|
||||
|
||||
internal sealed class TokenScopeAuthorizer : IScopeAuthorizer
|
||||
{
|
||||
public void EnsureScope(HttpContext context, string requiredScope)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(requiredScope))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var principal = context.User ?? throw new UnauthorizedAccessException("Authentication required.");
|
||||
if (principal.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Authentication required.");
|
||||
}
|
||||
|
||||
var normalizedRequired = StellaOpsScopes.Normalize(requiredScope) ?? requiredScope.Trim().ToLowerInvariant();
|
||||
if (!HasScope(principal, normalizedRequired))
|
||||
{
|
||||
throw new InvalidOperationException($"Missing required scope '{normalizedRequired}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasScope(ClaimsPrincipal principal, string requiredScope)
|
||||
{
|
||||
foreach (var claim in principal.FindAll(StellaOpsClaimTypes.ScopeItem))
|
||||
{
|
||||
if (string.Equals(claim.Value, requiredScope, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var claim in principal.FindAll(StellaOpsClaimTypes.Scope))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(claim.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var parts = claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var normalized = StellaOpsScopes.Normalize(part);
|
||||
if (normalized is not null && string.Equals(normalized, requiredScope, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user