| | | 1 | | using System.Text.Json; |
| | | 2 | | using System.Text.Json.Serialization; |
| | | 3 | | using Elsa.ExternalAuthentication.Contracts; |
| | | 4 | | using Elsa.ExternalAuthentication.Models; |
| | | 5 | | using Elsa.ExternalAuthentication.Services; |
| | | 6 | | |
| | | 7 | | namespace Elsa.ExternalAuthentication.Endpoints.Connections; |
| | | 8 | | |
| | | 9 | | internal sealed class ConnectionScopeRequest |
| | | 10 | | { |
| | | 11 | | public string? Kind { get; set; } |
| | | 12 | | public string? TenantId { get; set; } |
| | | 13 | | } |
| | | 14 | | |
| | | 15 | | internal sealed class ConnectionRequest |
| | | 16 | | { |
| | | 17 | | public string? Key { get; set; } |
| | | 18 | | public ConnectionScopeRequest? Scope { get; set; } |
| | | 19 | | public string? AdapterType { get; set; } |
| | | 20 | | public int AdapterSettingsVersion { get; set; } |
| | | 21 | | public JsonElement AdapterSettings { get; set; } |
| | | 22 | | // Accepted only to return a precise error for clients attempting to mutate |
| | | 23 | | // secret references through the general connection document. |
| | | 24 | | public Dictionary<string, SecretBinding>? SecretBindings { get; set; } |
| | | 25 | | public string? DisplayName { get; set; } |
| | | 26 | | public string? IconId { get; set; } |
| | | 27 | | public int Order { get; set; } |
| | | 28 | | public bool IsPreferred { get; set; } |
| | | 29 | | public bool OverridesConfigurationConnection { get; set; } |
| | | 30 | | public PolicySelection? UnlinkedPolicy { get; set; } |
| | | 31 | | public List<GrantSourceSelection>? PermissionGrantSources { get; set; } |
| | | 32 | | public ClaimProjectionRequest? ClaimProjection { get; set; } |
| | | 33 | | [JsonConverter(typeof(UpstreamLogoutModeJsonConverter))] |
| | | 34 | | public UpstreamLogoutMode UpstreamLogoutMode { get; set; } |
| | | 35 | | public bool ConfirmUnsafeSettings { get; set; } |
| | | 36 | | public bool ConfirmFinalLoginPathOverride { get; set; } |
| | | 37 | | |
| | | 38 | | public bool HasOnlyHostScope() => Scope is null || |
| | | 39 | | (string.IsNullOrWhiteSpace(Scope.TenantId) && (string.IsNullOrWhiteSpace(Scope.Kind) || string.Equals(Scope.Kind |
| | | 40 | | |
| | | 41 | | public IdentityProviderConnection ToConnection() => new() |
| | | 42 | | { |
| | | 43 | | TenantId = ConnectionScope.HostTenantId, |
| | | 44 | | Key = Key ?? string.Empty, |
| | | 45 | | AdapterType = AdapterType ?? string.Empty, |
| | | 46 | | AdapterSettingsVersion = AdapterSettingsVersion, |
| | | 47 | | AdapterSettings = AdapterSettings.ValueKind == JsonValueKind.Undefined ? default : AdapterSettings.Clone(), |
| | | 48 | | SecretBindings = new Dictionary<string, SecretBinding>(StringComparer.Ordinal), |
| | | 49 | | DisplayName = DisplayName ?? string.Empty, |
| | | 50 | | IconId = IconId, |
| | | 51 | | DisplayOrder = Order, |
| | | 52 | | IsPreferred = IsPreferred, |
| | | 53 | | OverridesConfigurationConnection = OverridesConfigurationConnection, |
| | | 54 | | UnlinkedPolicy = UnlinkedPolicy, |
| | | 55 | | PermissionGrantSources = PermissionGrantSources?.Select(x => new GrantSourceSelection(x.Type, x.SettingsVersion, |
| | | 56 | | ClaimProjection = ClaimProjection?.ToProjection() ?? Elsa.ExternalAuthentication.Models.ClaimProjection.Empty, |
| | | 57 | | UpstreamLogoutMode = UpstreamLogoutMode |
| | | 58 | | }; |
| | | 59 | | } |
| | | 60 | | |
| | | 61 | | internal sealed class ClaimProjectionRequest |
| | | 62 | | { |
| | | 63 | | public ICollection<string>? AllowedClaimTypes { get; set; } |
| | | 64 | | public ICollection<string>? RedactedClaimTypes { get; set; } |
| | | 65 | | public int MaximumClaimCount { get; set; } |
| | | 66 | | public int MaximumValueLength { get; set; } |
| | | 67 | | public int MaximumTotalBytes { get; set; } |
| | | 68 | | |
| | | 69 | | public ClaimProjection ToProjection() => new( |
| | | 70 | | new HashSet<string>(AllowedClaimTypes ?? [], StringComparer.Ordinal), |
| | | 71 | | new HashSet<string>(RedactedClaimTypes ?? [], StringComparer.Ordinal), |
| | | 72 | | MaximumClaimCount, |
| | | 73 | | MaximumValueLength, |
| | | 74 | | MaximumTotalBytes); |
| | | 75 | | } |
| | | 76 | | |
| | | 77 | | internal sealed record ConnectionSecretBindingResponse(string Ownership, string? ResolverType, string? Reference, bool I |
| | | 78 | | |
| | | 79 | | internal sealed class ManagedSecretBindingRequest |
| | | 80 | | { |
| | | 81 | | public string? ResolverType { get; set; } |
| | | 82 | | public string? Value { get; set; } |
| | | 83 | | } |
| | | 84 | | |
| | | 85 | | internal sealed record ConnectionScopeResponse(string Kind, string TenantId); |
| | | 86 | | |
| | | 87 | | internal sealed class ConnectionResponse |
| | | 88 | | { |
| | | 89 | | public string Id { get; init; } = null!; |
| | | 90 | | public string Key { get; init; } = null!; |
| | | 91 | | public string Source { get; init; } = null!; |
| | | 92 | | public ConnectionScopeResponse Scope { get; init; } = null!; |
| | | 93 | | public string AdapterType { get; init; } = null!; |
| | | 94 | | public Uri? CallbackUri { get; init; } |
| | | 95 | | public Uri? PreviewCallbackUri { get; init; } |
| | | 96 | | public int AdapterSettingsVersion { get; init; } |
| | | 97 | | public JsonElement AdapterSettings { get; init; } |
| | | 98 | | public IReadOnlyDictionary<string, ConnectionSecretBindingResponse> SecretBindings { get; init; } = null!; |
| | | 99 | | public string DisplayName { get; init; } = null!; |
| | | 100 | | public string? IconId { get; init; } |
| | | 101 | | public int Order { get; init; } |
| | | 102 | | public bool IsPreferred { get; init; } |
| | | 103 | | public bool OverridesConfigurationConnection { get; init; } |
| | | 104 | | public bool CanCreateOverride { get; init; } |
| | | 105 | | public bool CanPromoteToConfigurationOverride { get; init; } |
| | | 106 | | public bool EnabledIntent { get; init; } |
| | | 107 | | public bool EffectivelyEnabled { get; init; } |
| | | 108 | | public string Validity { get; init; } = null!; |
| | | 109 | | public bool Shadowed { get; init; } |
| | | 110 | | public ConnectionReferenceResponse? ShadowedBy { get; init; } |
| | | 111 | | public IReadOnlyCollection<ConnectionReferenceResponse> Shadows { get; init; } = []; |
| | | 112 | | public bool Archived { get; init; } |
| | | 113 | | public PolicySelection? UnlinkedPolicy { get; init; } |
| | | 114 | | public IReadOnlyCollection<GrantSourceSelection> PermissionGrantSources { get; init; } = []; |
| | | 115 | | public ClaimProjection ClaimProjection { get; init; } = ClaimProjection.Empty; |
| | | 116 | | [JsonConverter(typeof(UpstreamLogoutModeJsonConverter))] |
| | | 117 | | public UpstreamLogoutMode UpstreamLogoutMode { get; init; } |
| | | 118 | | public long Revision { get; init; } |
| | | 119 | | public string MaterialRevision { get; init; } = null!; |
| | | 120 | | public ConnectionObservationResponse? LatestObservation { get; init; } |
| | | 121 | | |
| | | 122 | | public static async ValueTask<ConnectionResponse> FromAsync(EffectiveIdentityProviderConnection effective, Services. |
| | | 123 | | { |
| | | 124 | | var states = await management.GetSecretBindingStatesAsync(effective.Connection, cancellationToken); |
| | | 125 | | var adapterSettings = effective.Connection.AdapterSettings.ValueKind == JsonValueKind.Undefined |
| | | 126 | | ? default |
| | | 127 | | : adapters.TryGet(effective.Connection.AdapterType, out var adapter) |
| | | 128 | | ? AdapterSettingsSecretFieldGuard.RedactDeclaredSecrets(effective.Connection.AdapterSettings, adapter.De |
| | | 129 | | : JsonSerializer.SerializeToElement(new Dictionary<string, object?>()); |
| | | 130 | | return new ConnectionResponse |
| | | 131 | | { |
| | | 132 | | Id = effective.Connection.Id, |
| | | 133 | | Key = effective.Connection.Key, |
| | | 134 | | Source = effective.Ownership == ConnectionSourceOwnership.Configuration ? "configuration" : "database", |
| | | 135 | | Scope = new ConnectionScopeResponse(effective.Scope.Kind switch { ConnectionScopeKind.Host => "host", Connec |
| | | 136 | | AdapterType = effective.Connection.AdapterType, |
| | | 137 | | CallbackUri = management.GetProviderCallbackUri(effective.Connection), |
| | | 138 | | PreviewCallbackUri = management.GetProviderPreviewCallbackUri(effective.Connection), |
| | | 139 | | AdapterSettingsVersion = effective.Connection.AdapterSettingsVersion, |
| | | 140 | | AdapterSettings = adapterSettings, |
| | | 141 | | SecretBindings = effective.Connection.SecretBindings.ToDictionary(x => x.Key, x => |
| | | 142 | | { |
| | | 143 | | states.TryGetValue(x.Key, out var state); |
| | | 144 | | var presentation = management.PresentSecretBinding(x.Value, state); |
| | | 145 | | return new ConnectionSecretBindingResponse( |
| | | 146 | | presentation.Ownership, |
| | | 147 | | x.Value.Ownership == SecretBindingOwnership.External ? x.Value.ResolverType : null, |
| | | 148 | | x.Value.Ownership == SecretBindingOwnership.External ? x.Value.Reference : null, |
| | | 149 | | presentation.IsConfigured, |
| | | 150 | | presentation.IsResolvable); |
| | | 151 | | }, StringComparer.Ordinal), |
| | | 152 | | DisplayName = effective.Connection.DisplayName, |
| | | 153 | | IconId = effective.Connection.IconId, |
| | | 154 | | Order = effective.Connection.DisplayOrder, |
| | | 155 | | IsPreferred = effective.Connection.IsPreferred, |
| | | 156 | | OverridesConfigurationConnection = effective.Connection.OverridesConfigurationConnection, |
| | | 157 | | CanCreateOverride = effective.Ownership == ConnectionSourceOwnership.Configuration && management.CanCreateCo |
| | | 158 | | CanPromoteToConfigurationOverride = management.CanPromoteToConfigurationOverride(effective), |
| | | 159 | | EnabledIntent = effective.Connection.IsEnabled, |
| | | 160 | | EffectivelyEnabled = effective.Connection.IsEnabled && !effective.Connection.ArchivedAt.HasValue && !effecti |
| | | 161 | | Validity = effective.Validity.ToString().ToLowerInvariant(), |
| | | 162 | | Shadowed = effective.IsShadowed, |
| | | 163 | | ShadowedBy = effective.ShadowedBy is null ? null : ConnectionReferenceResponse.From(effective.ShadowedBy), |
| | | 164 | | Shadows = effective.Shadows.Select(ConnectionReferenceResponse.From).ToArray(), |
| | | 165 | | Archived = effective.Connection.ArchivedAt.HasValue, |
| | | 166 | | UnlinkedPolicy = effective.Connection.UnlinkedPolicy, |
| | | 167 | | PermissionGrantSources = effective.Connection.PermissionGrantSources.ToArray(), |
| | | 168 | | ClaimProjection = effective.Connection.ClaimProjection, |
| | | 169 | | UpstreamLogoutMode = effective.Connection.UpstreamLogoutMode, |
| | | 170 | | Revision = effective.Connection.Revision, |
| | | 171 | | MaterialRevision = effective.Connection.MaterialRevision, |
| | | 172 | | LatestObservation = observation is null |
| | | 173 | | ? null |
| | | 174 | | : new ConnectionObservationResponse( |
| | | 175 | | observation.Status.ToString().ToLowerInvariant(), |
| | | 176 | | observation.ObservedAt, |
| | | 177 | | observation.TestedMaterialRevision, |
| | | 178 | | !string.Equals(observation.TestedMaterialRevision, effective.Connection.MaterialRevision, StringComp |
| | | 179 | | observation.Category, |
| | | 180 | | observation.Summary) |
| | | 181 | | }; |
| | | 182 | | } |
| | | 183 | | |
| | | 184 | | } |
| | | 185 | | |
| | | 186 | | internal sealed class UpstreamLogoutModeJsonConverter : JsonConverter<UpstreamLogoutMode> |
| | | 187 | | { |
| | | 188 | | public override UpstreamLogoutMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options |
| | | 189 | | { |
| | | 190 | | if (reader.TokenType != JsonTokenType.String) |
| | | 191 | | throw new JsonException("The upstream logout mode must be a string."); |
| | | 192 | | |
| | | 193 | | return reader.GetString()?.ToLowerInvariant() switch |
| | | 194 | | { |
| | | 195 | | "disabled" => UpstreamLogoutMode.Disabled, |
| | | 196 | | "userchoice" or "user-choice" or "user_choice" => UpstreamLogoutMode.UserChoice, |
| | | 197 | | "always" => UpstreamLogoutMode.Always, |
| | | 198 | | _ => throw new JsonException("The upstream logout mode is not supported.") |
| | | 199 | | }; |
| | | 200 | | } |
| | | 201 | | |
| | | 202 | | public override void Write(Utf8JsonWriter writer, UpstreamLogoutMode value, JsonSerializerOptions options) => |
| | | 203 | | writer.WriteStringValue(value switch |
| | | 204 | | { |
| | | 205 | | UpstreamLogoutMode.Disabled => "disabled", |
| | | 206 | | UpstreamLogoutMode.UserChoice => "user-choice", |
| | | 207 | | UpstreamLogoutMode.Always => "always", |
| | | 208 | | _ => throw new JsonException("The upstream logout mode is not supported.") |
| | | 209 | | }); |
| | | 210 | | } |
| | | 211 | | |
| | | 212 | | internal sealed record ConnectionReferenceResponse(string Id, string DisplayName, string Source) |
| | | 213 | | { |
| | | 214 | | public static ConnectionReferenceResponse From(IdentityProviderConnectionReference reference) => |
| | | 215 | | new( |
| | | 216 | | reference.Id, |
| | | 217 | | reference.DisplayName, |
| | | 218 | | reference.Ownership == ConnectionSourceOwnership.Configuration ? "configuration" : "database"); |
| | | 219 | | } |
| | | 220 | | |
| | | 221 | | internal sealed record ConnectionObservationResponse(string Status, DateTimeOffset ObservedAt, string TestedMaterialRevi |
| | | 222 | | internal sealed record ConnectionValidationResponse(bool Valid, IReadOnlyCollection<ConnectionValidationError> Errors, I |
| | | 223 | | internal sealed record ConnectionListResponse(IReadOnlyCollection<ConnectionResponse> Items, string? NextCursor); |
| | 115 | 224 | | internal sealed record ManagementErrorResponse(string Error, string Message, object? Details, string CorrelationId); |