elsa-core/src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/ConnectionManagementModels.cs

211 lines
12 KiB
C#

using System.Text.Json;
using Elsa.ExternalAuthentication.Contracts;
using Elsa.ExternalAuthentication.Models;
using Elsa.ExternalAuthentication.Services;
namespace Elsa.ExternalAuthentication.Endpoints.Connections;
internal sealed class ConnectionScopeRequest
{
public string? Kind { get; set; }
public string? TenantId { get; set; }
}
internal sealed class ConnectionRequest
{
public string? Key { get; set; }
public ConnectionScopeRequest? Scope { get; set; }
public string? AdapterType { get; set; }
public int AdapterSettingsVersion { get; set; }
public JsonElement AdapterSettings { get; set; }
// Accepted only to return a precise error for clients attempting to mutate
// secret references through the general connection document.
public Dictionary<string, SecretBinding>? SecretBindings { get; set; }
public string? DisplayName { get; set; }
public string? IconId { get; set; }
public int Order { get; set; }
public bool IsPreferred { get; set; }
public bool OverridesConfigurationConnection { get; set; }
public PolicySelection? UnlinkedPolicy { get; set; }
public List<GrantSourceSelection>? PermissionGrantSources { get; set; }
public ClaimProjectionRequest? ClaimProjection { get; set; }
public string? UpstreamLogoutMode { get; set; }
public bool ConfirmUnsafeSettings { get; set; }
public bool ConfirmFinalLoginPathOverride { get; set; }
public bool HasOnlyHostScope() => Scope is null ||
(string.IsNullOrWhiteSpace(Scope.TenantId) && (string.IsNullOrWhiteSpace(Scope.Kind) || string.Equals(Scope.Kind, "host", StringComparison.OrdinalIgnoreCase)));
public IdentityProviderConnection ToConnection() => new()
{
TenantId = ConnectionScope.HostTenantId,
Key = Key ?? string.Empty,
AdapterType = AdapterType ?? string.Empty,
AdapterSettingsVersion = AdapterSettingsVersion,
AdapterSettings = AdapterSettings.ValueKind == JsonValueKind.Undefined ? default : AdapterSettings.Clone(),
SecretBindings = new Dictionary<string, SecretBinding>(StringComparer.Ordinal),
DisplayName = DisplayName ?? string.Empty,
IconId = IconId,
DisplayOrder = Order,
IsPreferred = IsPreferred,
OverridesConfigurationConnection = OverridesConfigurationConnection,
UnlinkedPolicy = UnlinkedPolicy,
PermissionGrantSources = PermissionGrantSources?.Select(x => new GrantSourceSelection(x.Type, x.SettingsVersion, x.Settings.ValueKind == JsonValueKind.Undefined ? default : x.Settings.Clone(), x.Order)).ToArray() ?? [],
ClaimProjection = ClaimProjection?.ToProjection() ?? Elsa.ExternalAuthentication.Models.ClaimProjection.Empty,
UpstreamLogoutMode = ParseUpstreamLogoutMode(UpstreamLogoutMode)
};
private static UpstreamLogoutMode ParseUpstreamLogoutMode(string? value) => value?.ToLowerInvariant() switch
{
"disabled" or null => Elsa.ExternalAuthentication.Models.UpstreamLogoutMode.Disabled,
"userchoice" or "user-choice" or "user_choice" => Elsa.ExternalAuthentication.Models.UpstreamLogoutMode.UserChoice,
"always" => Elsa.ExternalAuthentication.Models.UpstreamLogoutMode.Always,
_ => (Elsa.ExternalAuthentication.Models.UpstreamLogoutMode)(-1)
};
}
internal sealed class ClaimProjectionRequest
{
public ICollection<string>? AllowedClaimTypes { get; set; }
public ICollection<string>? RedactedClaimTypes { get; set; }
public int MaximumClaimCount { get; set; }
public int MaximumValueLength { get; set; }
public int MaximumTotalBytes { get; set; }
public ClaimProjection ToProjection() => new(
new HashSet<string>(AllowedClaimTypes ?? [], StringComparer.Ordinal),
new HashSet<string>(RedactedClaimTypes ?? [], StringComparer.Ordinal),
MaximumClaimCount,
MaximumValueLength,
MaximumTotalBytes);
}
internal sealed record ConnectionSecretBindingResponse(string Ownership, string? ResolverType, string? Reference, bool IsConfigured, bool IsResolvable);
internal sealed class ManagedSecretBindingRequest
{
public string? ResolverType { get; set; }
public string? Value { get; set; }
}
internal sealed record ConnectionScopeResponse(string Kind, string TenantId);
internal sealed class ConnectionResponse
{
public string Id { get; init; } = null!;
public string Key { get; init; } = null!;
public string Source { get; init; } = null!;
public ConnectionScopeResponse Scope { get; init; } = null!;
public string AdapterType { get; init; } = null!;
public Uri? CallbackUri { get; init; }
public Uri? PreviewCallbackUri { get; init; }
public int AdapterSettingsVersion { get; init; }
public JsonElement AdapterSettings { get; init; }
public IReadOnlyDictionary<string, ConnectionSecretBindingResponse> SecretBindings { get; init; } = null!;
public string DisplayName { get; init; } = null!;
public string? IconId { get; init; }
public int Order { get; init; }
public bool IsPreferred { get; init; }
public bool OverridesConfigurationConnection { get; init; }
public bool CanCreateOverride { get; init; }
public bool CanPromoteToConfigurationOverride { get; init; }
public bool EnabledIntent { get; init; }
public bool EffectivelyEnabled { get; init; }
public string Validity { get; init; } = null!;
public bool Shadowed { get; init; }
public ConnectionReferenceResponse? ShadowedBy { get; init; }
public IReadOnlyCollection<ConnectionReferenceResponse> Shadows { get; init; } = [];
public bool Archived { get; init; }
public PolicySelection? UnlinkedPolicy { get; init; }
public IReadOnlyCollection<GrantSourceSelection> PermissionGrantSources { get; init; } = [];
public ClaimProjection ClaimProjection { get; init; } = ClaimProjection.Empty;
public string UpstreamLogoutMode { get; init; } = null!;
public long Revision { get; init; }
public string MaterialRevision { get; init; } = null!;
public ConnectionObservationResponse? LatestObservation { get; init; }
public static async ValueTask<ConnectionResponse> FromAsync(EffectiveIdentityProviderConnection effective, Services.IdentityProviderConnectionManagementService management, IExternalAuthenticationAdapterRegistry adapters, ConnectionObservation? observation, CancellationToken cancellationToken)
{
var states = await management.GetSecretBindingStatesAsync(effective.Connection, cancellationToken);
var adapterSettings = effective.Connection.AdapterSettings.ValueKind == JsonValueKind.Undefined
? default
: adapters.TryGet(effective.Connection.AdapterType, out var adapter)
? AdapterSettingsSecretFieldGuard.RedactDeclaredSecrets(effective.Connection.AdapterSettings, adapter.Describe())
: JsonSerializer.SerializeToElement(new Dictionary<string, object?>());
return new ConnectionResponse
{
Id = effective.Connection.Id,
Key = effective.Connection.Key,
Source = effective.Ownership == ConnectionSourceOwnership.Configuration ? "configuration" : "database",
Scope = new ConnectionScopeResponse(effective.Scope.Kind switch { ConnectionScopeKind.Host => "host", ConnectionScopeKind.DefaultTenant => "defaultTenant", _ => "tenant" }, effective.Scope.TenantId),
AdapterType = effective.Connection.AdapterType,
CallbackUri = management.GetProviderCallbackUri(effective.Connection),
PreviewCallbackUri = management.GetProviderPreviewCallbackUri(effective.Connection),
AdapterSettingsVersion = effective.Connection.AdapterSettingsVersion,
AdapterSettings = adapterSettings,
SecretBindings = effective.Connection.SecretBindings.ToDictionary(x => x.Key, x =>
{
states.TryGetValue(x.Key, out var state);
var presentation = management.PresentSecretBinding(x.Value, state);
return new ConnectionSecretBindingResponse(
presentation.Ownership,
x.Value.Ownership == SecretBindingOwnership.External ? x.Value.ResolverType : null,
x.Value.Ownership == SecretBindingOwnership.External ? x.Value.Reference : null,
presentation.IsConfigured,
presentation.IsResolvable);
}, StringComparer.Ordinal),
DisplayName = effective.Connection.DisplayName,
IconId = effective.Connection.IconId,
Order = effective.Connection.DisplayOrder,
IsPreferred = effective.Connection.IsPreferred,
OverridesConfigurationConnection = effective.Connection.OverridesConfigurationConnection,
CanCreateOverride = effective.Ownership == ConnectionSourceOwnership.Configuration && management.CanCreateConfigurationOverride(),
CanPromoteToConfigurationOverride = management.CanPromoteToConfigurationOverride(effective),
EnabledIntent = effective.Connection.IsEnabled,
EffectivelyEnabled = effective.Connection.IsEnabled && !effective.Connection.ArchivedAt.HasValue && !effective.IsShadowed && effective.Validity != ConnectionValidity.Invalid,
Validity = effective.Validity.ToString().ToLowerInvariant(),
Shadowed = effective.IsShadowed,
ShadowedBy = effective.ShadowedBy is null ? null : ConnectionReferenceResponse.From(effective.ShadowedBy),
Shadows = effective.Shadows.Select(ConnectionReferenceResponse.From).ToArray(),
Archived = effective.Connection.ArchivedAt.HasValue,
UnlinkedPolicy = effective.Connection.UnlinkedPolicy,
PermissionGrantSources = effective.Connection.PermissionGrantSources.ToArray(),
ClaimProjection = effective.Connection.ClaimProjection,
UpstreamLogoutMode = FormatUpstreamLogoutMode(effective.Connection.UpstreamLogoutMode),
Revision = effective.Connection.Revision,
MaterialRevision = effective.Connection.MaterialRevision,
LatestObservation = observation is null
? null
: new ConnectionObservationResponse(
observation.Status.ToString().ToLowerInvariant(),
observation.ObservedAt,
observation.TestedMaterialRevision,
!string.Equals(observation.TestedMaterialRevision, effective.Connection.MaterialRevision, StringComparison.Ordinal),
observation.Category,
observation.Summary)
};
}
private static string FormatUpstreamLogoutMode(UpstreamLogoutMode mode) => mode switch
{
Elsa.ExternalAuthentication.Models.UpstreamLogoutMode.Disabled => "disabled",
Elsa.ExternalAuthentication.Models.UpstreamLogoutMode.UserChoice => "user-choice",
Elsa.ExternalAuthentication.Models.UpstreamLogoutMode.Always => "always",
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "The upstream logout mode is not supported.")
};
}
internal sealed record ConnectionReferenceResponse(string Id, string DisplayName, string Source)
{
public static ConnectionReferenceResponse From(IdentityProviderConnectionReference reference) =>
new(
reference.Id,
reference.DisplayName,
reference.Ownership == ConnectionSourceOwnership.Configuration ? "configuration" : "database");
}
internal sealed record ConnectionObservationResponse(string Status, DateTimeOffset ObservedAt, string TestedMaterialRevision, bool IsStale, string Category, string Summary);
internal sealed record ConnectionValidationResponse(bool Valid, IReadOnlyCollection<ConnectionValidationError> Errors, IReadOnlyCollection<string> Warnings);
internal sealed record ConnectionListResponse(IReadOnlyCollection<ConnectionResponse> Items, string? NextCursor);
internal sealed record ManagementErrorResponse(string Error, string Message, object? Details, string CorrelationId);