Fix live evidence and registry auth contracts
This commit is contained in:
@@ -668,6 +668,11 @@ public static class StellaOpsScopes
|
||||
/// </summary>
|
||||
public const string IntegrationOperate = "integration:operate";
|
||||
|
||||
/// <summary>
|
||||
/// Scope granting administrative access to registry plan and audit surfaces.
|
||||
/// </summary>
|
||||
public const string RegistryAdmin = "registry.admin";
|
||||
|
||||
private static readonly IReadOnlyList<string> AllScopes = BuildAllScopes();
|
||||
private static readonly HashSet<string> KnownScopes = new(AllScopes, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
||||
@@ -36,14 +36,24 @@ internal sealed class StandardPluginBootstrapper : IHostedService
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var optionsMonitor = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<StandardPluginOptions>>();
|
||||
var credentialStore = scope.ServiceProvider.GetRequiredService<StandardUserCredentialStore>();
|
||||
|
||||
var options = optionsMonitor.Get(pluginName);
|
||||
var tenantId = options.TenantId ?? DefaultTenantId;
|
||||
|
||||
try
|
||||
{
|
||||
await EnsureBootstrapClientsAsync(scope.ServiceProvider, tenantId, options.BootstrapClients, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Standard Authority plugin '{PluginName}' failed to ensure bootstrap clients.", pluginName);
|
||||
}
|
||||
|
||||
if (options.BootstrapUser is null || !options.BootstrapUser.IsConfigured)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var credentialStore = scope.ServiceProvider.GetRequiredService<StandardUserCredentialStore>();
|
||||
logger.LogInformation("Standard Authority plugin '{PluginName}' ensuring bootstrap user.", pluginName);
|
||||
try
|
||||
{
|
||||
@@ -54,7 +64,6 @@ internal sealed class StandardPluginBootstrapper : IHostedService
|
||||
logger.LogError(ex, "Standard Authority plugin '{PluginName}' failed to ensure bootstrap user.", pluginName);
|
||||
}
|
||||
|
||||
var tenantId = options.TenantId ?? DefaultTenantId;
|
||||
var bootstrapRoles = options.BootstrapUser.Roles ?? new[] { "admin" };
|
||||
|
||||
try
|
||||
@@ -171,5 +180,38 @@ internal sealed class StandardPluginBootstrapper : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureBootstrapClientsAsync(
|
||||
IServiceProvider services,
|
||||
string tenantId,
|
||||
IReadOnlyCollection<BootstrapClientOptions> bootstrapClients,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (bootstrapClients.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var clientProvisioningStore = services.GetRequiredService<StandardClientProvisioningStore>();
|
||||
foreach (var bootstrapClient in bootstrapClients)
|
||||
{
|
||||
var registration = bootstrapClient.ToRegistration(tenantId);
|
||||
var result = await clientProvisioningStore.CreateOrUpdateAsync(registration, cancellationToken).ConfigureAwait(false);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Standard Authority plugin '{PluginName}' failed to ensure bootstrap client '{ClientId}': {Message}",
|
||||
pluginName,
|
||||
registration.ClientId,
|
||||
result.Message ?? result.ErrorCode ?? "unknown_error");
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Standard Authority plugin '{PluginName}' ensured bootstrap client '{ClientId}'.",
|
||||
pluginName,
|
||||
registration.ClientId);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
using StellaOps.Cryptography;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Authority.Plugin.Standard;
|
||||
|
||||
@@ -11,6 +14,8 @@ internal sealed class StandardPluginOptions
|
||||
|
||||
public BootstrapUserOptions? BootstrapUser { get; set; }
|
||||
|
||||
public BootstrapClientOptions[] BootstrapClients { get; set; } = Array.Empty<BootstrapClientOptions>();
|
||||
|
||||
public PasswordPolicyOptions PasswordPolicy { get; set; } = new();
|
||||
|
||||
public LockoutOptions Lockout { get; set; } = new();
|
||||
@@ -23,12 +28,25 @@ internal sealed class StandardPluginOptions
|
||||
{
|
||||
TenantId = NormalizeTenantId(TenantId);
|
||||
BootstrapUser?.Normalize();
|
||||
BootstrapClients = BootstrapClients ?? Array.Empty<BootstrapClientOptions>();
|
||||
|
||||
foreach (var client in BootstrapClients)
|
||||
{
|
||||
client.Normalize();
|
||||
}
|
||||
|
||||
TokenSigning.Normalize(configPath);
|
||||
}
|
||||
|
||||
public void Validate(string pluginName)
|
||||
{
|
||||
BootstrapUser?.Validate(pluginName);
|
||||
|
||||
foreach (var client in BootstrapClients)
|
||||
{
|
||||
client.Validate(pluginName);
|
||||
}
|
||||
|
||||
PasswordPolicy.Validate(pluginName);
|
||||
Lockout.Validate(pluginName);
|
||||
PasswordHashing.Validate();
|
||||
@@ -44,6 +62,231 @@ internal sealed class StandardPluginOptions
|
||||
=> string.IsNullOrWhiteSpace(tenantId) ? null : tenantId.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
internal sealed class BootstrapClientOptions
|
||||
{
|
||||
public string? ClientId { get; set; }
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
public bool Confidential { get; set; }
|
||||
|
||||
public string? ClientSecret { get; set; }
|
||||
|
||||
public string? AllowedGrantTypes { get; set; }
|
||||
|
||||
public string? AllowedScopes { get; set; }
|
||||
|
||||
public string? AllowedAudiences { get; set; }
|
||||
|
||||
public string? RedirectUris { get; set; }
|
||||
|
||||
public string? PostLogoutRedirectUris { get; set; }
|
||||
|
||||
public string? TenantId { get; set; }
|
||||
|
||||
public string? Project { get; set; }
|
||||
|
||||
public string? SenderConstraint { get; set; }
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public bool RequirePkce { get; set; } = true;
|
||||
|
||||
public bool AllowPlainTextPkce { get; set; }
|
||||
|
||||
public void Normalize()
|
||||
{
|
||||
ClientId = NormalizeOptional(ClientId);
|
||||
DisplayName = NormalizeOptional(DisplayName);
|
||||
ClientSecret = NormalizeSecret(ClientSecret);
|
||||
AllowedGrantTypes = NormalizeJoinedValues(AllowedGrantTypes);
|
||||
AllowedScopes = NormalizeJoinedScopes(AllowedScopes);
|
||||
AllowedAudiences = NormalizeJoinedValues(AllowedAudiences);
|
||||
RedirectUris = NormalizeJoinedValues(RedirectUris);
|
||||
PostLogoutRedirectUris = NormalizeJoinedValues(PostLogoutRedirectUris);
|
||||
TenantId = NormalizeTenantId(TenantId);
|
||||
Project = NormalizeProject(Project);
|
||||
SenderConstraint = NormalizeSenderConstraint(SenderConstraint);
|
||||
}
|
||||
|
||||
public void Validate(string pluginName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ClientId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Standard plugin '{pluginName}' requires bootstrapClients.clientId.");
|
||||
}
|
||||
|
||||
if (Confidential && string.IsNullOrWhiteSpace(ClientSecret))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Standard plugin '{pluginName}' bootstrap client '{ClientId}' requires clientSecret when confidential=true.");
|
||||
}
|
||||
|
||||
var grantTypes = SplitValues(AllowedGrantTypes);
|
||||
if (grantTypes.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Standard plugin '{pluginName}' bootstrap client '{ClientId}' requires at least one allowed grant type.");
|
||||
}
|
||||
|
||||
var scopes = SplitValues(AllowedScopes);
|
||||
if (scopes.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Standard plugin '{pluginName}' bootstrap client '{ClientId}' requires at least one allowed scope.");
|
||||
}
|
||||
|
||||
var redirectUris = ParseUris(RedirectUris, pluginName, ClientId!, "redirectUris");
|
||||
_ = ParseUris(PostLogoutRedirectUris, pluginName, ClientId!, "postLogoutRedirectUris");
|
||||
|
||||
if (grantTypes.Contains("authorization_code", StringComparer.OrdinalIgnoreCase) && redirectUris.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Standard plugin '{pluginName}' bootstrap client '{ClientId}' requires at least one redirect URI for authorization_code.");
|
||||
}
|
||||
|
||||
if (AllowPlainTextPkce && !RequirePkce)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Standard plugin '{pluginName}' bootstrap client '{ClientId}' cannot allow plain-text PKCE when PKCE is disabled.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(SenderConstraint) &&
|
||||
!string.Equals(SenderConstraint, "dpop", StringComparison.Ordinal) &&
|
||||
!string.Equals(SenderConstraint, "mtls", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Standard plugin '{pluginName}' bootstrap client '{ClientId}' must use senderConstraint 'dpop' or 'mtls' when configured.");
|
||||
}
|
||||
}
|
||||
|
||||
public AuthorityClientRegistration ToRegistration(string defaultTenantId)
|
||||
{
|
||||
var properties = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[StandardClientMetadataKeys.Enabled] = Enabled ? "true" : "false",
|
||||
[StandardClientMetadataKeys.RequirePkce] = RequirePkce ? "true" : "false",
|
||||
[StandardClientMetadataKeys.AllowPlainTextPkce] = AllowPlainTextPkce ? "true" : "false",
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(SenderConstraint))
|
||||
{
|
||||
properties[AuthorityClientMetadataKeys.SenderConstraint] = SenderConstraint;
|
||||
}
|
||||
|
||||
return new AuthorityClientRegistration(
|
||||
clientId: ClientId!,
|
||||
confidential: Confidential,
|
||||
displayName: DisplayName ?? ClientId,
|
||||
clientSecret: ClientSecret,
|
||||
allowedGrantTypes: SplitValues(AllowedGrantTypes),
|
||||
allowedScopes: SplitValues(AllowedScopes),
|
||||
allowedAudiences: SplitValues(AllowedAudiences),
|
||||
redirectUris: ParseUris(RedirectUris, pluginName: null, ClientId!, "redirectUris"),
|
||||
postLogoutRedirectUris: ParseUris(PostLogoutRedirectUris, pluginName: null, ClientId!, "postLogoutRedirectUris"),
|
||||
tenant: TenantId ?? defaultTenantId,
|
||||
project: Project ?? StellaOpsTenancyDefaults.AnyProject,
|
||||
properties: properties);
|
||||
}
|
||||
|
||||
private static string? NormalizeOptional(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
private static string? NormalizeSecret(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
private static string? NormalizeTenantId(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
|
||||
|
||||
private static string? NormalizeProject(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
|
||||
|
||||
private static string? NormalizeSenderConstraint(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string? NormalizeJoinedValues(string? raw)
|
||||
{
|
||||
var values = SplitValues(raw);
|
||||
if (values.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return string.Join(" ", values.OrderBy(static value => value, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
private static string? NormalizeJoinedScopes(string? raw)
|
||||
{
|
||||
var values = SplitValues(raw)
|
||||
.Select(StellaOpsScopes.Normalize)
|
||||
.Where(static value => value is not null)
|
||||
.Select(static value => value!)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(static value => value, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
return values.Length == 0 ? null : string.Join(" ", values);
|
||||
}
|
||||
|
||||
private static string[] SplitValues(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return raw
|
||||
.Split([' ', ',', ';', '\t', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<Uri> ParseUris(string? raw, string? pluginName, string clientId, string propertyName)
|
||||
{
|
||||
var values = SplitValues(raw);
|
||||
if (values.Length == 0)
|
||||
{
|
||||
return Array.Empty<Uri>();
|
||||
}
|
||||
|
||||
var uris = new List<Uri>(values.Length);
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pluginName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Standard plugin '{pluginName}' bootstrap client '{clientId}' requires absolute URIs in {propertyName}.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Bootstrap client '{clientId}' requires absolute URIs in {propertyName}.");
|
||||
}
|
||||
|
||||
uris.Add(uri);
|
||||
}
|
||||
|
||||
return uris;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class StandardClientMetadataKeys
|
||||
{
|
||||
public const string Enabled = "enabled";
|
||||
public const string RequirePkce = "requirePkce";
|
||||
public const string AllowPlainTextPkce = "allowPlainTextPkce";
|
||||
}
|
||||
|
||||
internal sealed class BootstrapUserOptions
|
||||
{
|
||||
public string? Username { get; set; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Authority.Persistence.Documents;
|
||||
using StellaOps.Authority.Persistence.InMemory.Stores;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
@@ -40,78 +40,7 @@ internal sealed class StandardClientProvisioningStore : IClientProvisioningStore
|
||||
var document = await clientStore.FindByClientIdAsync(registration.ClientId, cancellationToken).ConfigureAwait(false)
|
||||
?? new AuthorityClientDocument { ClientId = registration.ClientId, CreatedAt = clock.GetUtcNow() };
|
||||
|
||||
document.Plugin = pluginName;
|
||||
document.ClientType = registration.Confidential ? "confidential" : "public";
|
||||
document.DisplayName = registration.DisplayName;
|
||||
document.SecretHash = registration.Confidential && registration.ClientSecret is not null
|
||||
? AuthoritySecretHasher.ComputeHash(registration.ClientSecret)
|
||||
: null;
|
||||
document.UpdatedAt = clock.GetUtcNow();
|
||||
|
||||
document.RedirectUris = registration.RedirectUris.Select(static uri => uri.ToString()).ToList();
|
||||
document.PostLogoutRedirectUris = registration.PostLogoutRedirectUris.Select(static uri => uri.ToString()).ToList();
|
||||
|
||||
document.Properties[AuthorityClientMetadataKeys.AllowedGrantTypes] = JoinValues(registration.AllowedGrantTypes);
|
||||
document.Properties[AuthorityClientMetadataKeys.AllowedScopes] = JoinValues(registration.AllowedScopes);
|
||||
document.Properties[AuthorityClientMetadataKeys.Audiences] = JoinValues(registration.AllowedAudiences);
|
||||
document.Properties[AuthorityClientMetadataKeys.RedirectUris] = string.Join(" ", document.RedirectUris);
|
||||
document.Properties[AuthorityClientMetadataKeys.PostLogoutRedirectUris] = string.Join(" ", document.PostLogoutRedirectUris);
|
||||
|
||||
if (registration.CertificateBindings is not null)
|
||||
{
|
||||
var now = clock.GetUtcNow();
|
||||
document.CertificateBindings = registration.CertificateBindings
|
||||
.Select(binding => MapCertificateBinding(binding, now))
|
||||
.OrderBy(binding => binding.Thumbprint, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
foreach (var (key, value) in registration.Properties)
|
||||
{
|
||||
document.Properties[key] = value;
|
||||
}
|
||||
|
||||
var normalizedTenant = NormalizeTenant(registration.Tenant);
|
||||
var normalizedTenants = NormalizeTenants(
|
||||
registration.Properties.TryGetValue(AuthorityClientMetadataKeys.Tenants, out var tenantAssignments) ? tenantAssignments : null,
|
||||
normalizedTenant);
|
||||
|
||||
if (normalizedTenants.Count > 0)
|
||||
{
|
||||
document.Properties[AuthorityClientMetadataKeys.Tenants] = string.Join(" ", normalizedTenants);
|
||||
}
|
||||
else
|
||||
{
|
||||
document.Properties.Remove(AuthorityClientMetadataKeys.Tenants);
|
||||
}
|
||||
|
||||
if (normalizedTenant is not null)
|
||||
{
|
||||
document.Properties[AuthorityClientMetadataKeys.Tenant] = normalizedTenant;
|
||||
}
|
||||
else if (normalizedTenants.Count == 1)
|
||||
{
|
||||
document.Properties[AuthorityClientMetadataKeys.Tenant] = normalizedTenants[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
document.Properties.Remove(AuthorityClientMetadataKeys.Tenant);
|
||||
}
|
||||
|
||||
if (registration.Properties.TryGetValue(AuthorityClientMetadataKeys.SenderConstraint, out var senderConstraintRaw))
|
||||
{
|
||||
var normalizedConstraint = NormalizeSenderConstraint(senderConstraintRaw);
|
||||
if (normalizedConstraint is not null)
|
||||
{
|
||||
document.SenderConstraint = normalizedConstraint;
|
||||
document.Properties[AuthorityClientMetadataKeys.SenderConstraint] = normalizedConstraint;
|
||||
}
|
||||
else
|
||||
{
|
||||
document.SenderConstraint = null;
|
||||
document.Properties.Remove(AuthorityClientMetadataKeys.SenderConstraint);
|
||||
}
|
||||
}
|
||||
ApplyRegistration(document, registration);
|
||||
|
||||
await clientStore.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
|
||||
await revocationStore.RemoveAsync("client", registration.ClientId, cancellationToken).ConfigureAwait(false);
|
||||
@@ -163,6 +92,86 @@ internal sealed class StandardClientProvisioningStore : IClientProvisioningStore
|
||||
return AuthorityPluginOperationResult.Success();
|
||||
}
|
||||
|
||||
private void ApplyRegistration(AuthorityClientDocument document, AuthorityClientRegistration registration)
|
||||
{
|
||||
document.Plugin = pluginName;
|
||||
document.ClientType = registration.Confidential ? "confidential" : "public";
|
||||
document.DisplayName = registration.DisplayName;
|
||||
document.ClientSecret = registration.Confidential ? registration.ClientSecret : null;
|
||||
document.SecretHash = registration.Confidential && registration.ClientSecret is not null
|
||||
? AuthoritySecretHasher.ComputeHash(registration.ClientSecret)
|
||||
: null;
|
||||
document.Enabled = ParseBoolean(registration.Properties, StandardClientMetadataKeys.Enabled) ?? true;
|
||||
document.Disabled = !document.Enabled;
|
||||
document.RequireClientSecret = registration.Confidential;
|
||||
document.RequirePkce = ParseBoolean(registration.Properties, StandardClientMetadataKeys.RequirePkce) ?? document.RequirePkce;
|
||||
document.AllowPlainTextPkce = ParseBoolean(registration.Properties, StandardClientMetadataKeys.AllowPlainTextPkce) ?? document.AllowPlainTextPkce;
|
||||
document.UpdatedAt = clock.GetUtcNow();
|
||||
|
||||
document.RedirectUris = registration.RedirectUris.Select(static uri => uri.ToString()).ToList();
|
||||
document.PostLogoutRedirectUris = registration.PostLogoutRedirectUris.Select(static uri => uri.ToString()).ToList();
|
||||
document.AllowedGrantTypes = registration.AllowedGrantTypes.OrderBy(static value => value, StringComparer.Ordinal).ToList();
|
||||
document.AllowedScopes = registration.AllowedScopes.OrderBy(static value => value, StringComparer.Ordinal).ToList();
|
||||
|
||||
document.Properties[AuthorityClientMetadataKeys.AllowedGrantTypes] = JoinValues(registration.AllowedGrantTypes);
|
||||
document.Properties[AuthorityClientMetadataKeys.AllowedScopes] = JoinValues(registration.AllowedScopes);
|
||||
document.Properties[AuthorityClientMetadataKeys.Audiences] = JoinValues(registration.AllowedAudiences);
|
||||
document.Properties[AuthorityClientMetadataKeys.RedirectUris] = string.Join(" ", document.RedirectUris);
|
||||
document.Properties[AuthorityClientMetadataKeys.PostLogoutRedirectUris] = string.Join(" ", document.PostLogoutRedirectUris);
|
||||
document.Properties[AuthorityClientMetadataKeys.Project] = registration.Project ?? StellaOpsTenancyDefaults.AnyProject;
|
||||
|
||||
foreach (var (key, value) in registration.Properties)
|
||||
{
|
||||
document.Properties[key] = value;
|
||||
}
|
||||
|
||||
var normalizedTenant = NormalizeTenant(registration.Tenant);
|
||||
var normalizedTenants = NormalizeTenants(
|
||||
registration.Properties.TryGetValue(AuthorityClientMetadataKeys.Tenants, out var tenantAssignments) ? tenantAssignments : null,
|
||||
normalizedTenant);
|
||||
|
||||
if (normalizedTenants.Count > 0)
|
||||
{
|
||||
document.Properties[AuthorityClientMetadataKeys.Tenants] = string.Join(" ", normalizedTenants);
|
||||
}
|
||||
else
|
||||
{
|
||||
document.Properties.Remove(AuthorityClientMetadataKeys.Tenants);
|
||||
}
|
||||
|
||||
if (normalizedTenant is not null)
|
||||
{
|
||||
document.Properties[AuthorityClientMetadataKeys.Tenant] = normalizedTenant;
|
||||
}
|
||||
else if (normalizedTenants.Count == 1)
|
||||
{
|
||||
document.Properties[AuthorityClientMetadataKeys.Tenant] = normalizedTenants[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
document.Properties.Remove(AuthorityClientMetadataKeys.Tenant);
|
||||
}
|
||||
|
||||
if (registration.Properties.TryGetValue(AuthorityClientMetadataKeys.SenderConstraint, out var senderConstraintRaw))
|
||||
{
|
||||
var normalizedConstraint = NormalizeSenderConstraint(senderConstraintRaw);
|
||||
if (normalizedConstraint is not null)
|
||||
{
|
||||
document.SenderConstraint = normalizedConstraint;
|
||||
document.Properties[AuthorityClientMetadataKeys.SenderConstraint] = normalizedConstraint;
|
||||
}
|
||||
else
|
||||
{
|
||||
document.SenderConstraint = null;
|
||||
document.Properties.Remove(AuthorityClientMetadataKeys.SenderConstraint);
|
||||
}
|
||||
}
|
||||
|
||||
document.CertificateBindings = registration.CertificateBindings.Count == 0
|
||||
? new List<AuthorityClientCertificateBinding>()
|
||||
: registration.CertificateBindings.Select(binding => MapCertificateBinding(binding, clock.GetUtcNow())).ToList();
|
||||
}
|
||||
|
||||
private static AuthorityClientDescriptor ToDescriptor(AuthorityClientDocument document)
|
||||
{
|
||||
var allowedGrantTypes = Split(document.Properties, AuthorityClientMetadataKeys.AllowedGrantTypes);
|
||||
@@ -250,6 +259,16 @@ internal sealed class StandardClientProvisioningStore : IClientProvisioningStore
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static bool? ParseBoolean(IReadOnlyDictionary<string, string?> properties, string key)
|
||||
{
|
||||
if (!properties.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return bool.TryParse(value, out var parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
private static AuthorityClientCertificateBinding MapCertificateBinding(
|
||||
AuthorityClientCertificateBindingRegistration registration,
|
||||
DateTimeOffset now)
|
||||
|
||||
@@ -100,7 +100,7 @@ VALUES
|
||||
'ui.preferences.read', 'ui.preferences.write',
|
||||
'doctor:run', 'doctor:admin',
|
||||
'ops.health',
|
||||
'integration:read', 'integration:write', 'integration:operate',
|
||||
'integration:read', 'integration:write', 'integration:operate', 'registry.admin',
|
||||
'advisory-ai:view', 'advisory-ai:operate',
|
||||
'timeline:read', 'timeline:write'],
|
||||
ARRAY['authorization_code', 'refresh_token'],
|
||||
|
||||
Reference in New Issue
Block a user