Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -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));
}
}

View File

@@ -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());
}
}

View File

@@ -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}'.");
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Http;
namespace StellaOps.Scheduler.WebService.Auth;
public interface IScopeAuthorizer
{
void EnsureScope(HttpContext context, string requiredScope);
}

View File

@@ -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);

View File

@@ -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;
}
}