< Summary

Line coverage
84%
Covered lines: 364
Uncovered lines: 68
Coverable lines: 432
Total lines: 711
Line coverage: 84.2%
Branch coverage
62%
Covered branches: 277
Total branches: 445
Branch coverage: 62.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: ConnectionKeyPattern()100%11100%
File 2: .ctor(...)100%11100%
File 2: FindAsync()87.5%8887.5%
File 2: GetProviderCallbackUri(...)50%22100%
File 2: GetProviderPreviewCallbackUri(...)50%22100%
File 2: ListAsync()100%11100%
File 2: CreateAsync()83.33%6691.66%
File 2: UpdateAsync()68.75%161688%
File 2: ChangeLifecycleAsync()58.06%463175%
File 2: ValidateAsync()71.87%423278.37%
File 2: GetSecretBindingStatesAsync(...)100%11100%
File 2: PresentSecretBinding(...)66.66%66100%
File 2: CanCreateConfigurationOverride()100%11100%
File 2: CanPromoteToConfigurationOverride(...)100%1010100%
File 2: IsBlockedByFinalLoginPathGuardAsync()87.5%8892.3%
File 2: ProcessMutationAsync()70%141066.66%
File 2: RunPostCommitActionsAsync()100%1172.72%
File 2: PublishBulkSessionRevocationAsync()66.66%7675%
File 2: PublishAsync()70.83%242496.15%
File 2: CollidesWithConfigurationOrHostAsync()66.66%6690.9%
File 2: GetSecretStatesAsync()75%4485.71%
File 2: ApplySettingsMigrationAsync()100%11100%
File 2: NormalizeForCreate(...)50%66100%
File 2: NormalizeForUpdate(...)50%1010100%
File 2: ValidateEnvelope(...)57.89%1073863.63%
File 2: ValidatePolicyAsync()87.5%333290.47%
File 2: DefaultRolesAreUnchangedAsync()91.66%1212100%
File 2: UsesCreateUserFallback(...)50%44100%
File 2: TryGetMatcherSelection(...)50%201883.33%
File 2: ReadString(...)50%66100%
File 2: ValidateGrantSources(...)20%291042.85%
File 2: ValidateSecretBindingFields(...)90%101090%
File 2: ValidateSecretBindingsAreNotAdapterSettings(...)83.33%6683.33%
File 2: UsesUnsafeSettings(...)75%4475%
File 2: UnsafeSettingsChanged(...)71.42%241462.5%
File 2: IsUnsafeFieldValue(...)50%1010100%
File 2: JsonValueEquals(...)50%22100%
File 2: ToEffective(...)100%11100%
File 2: ToScope(...)25%44100%
File 2: CanMutate(...)50%22100%
File 2: IsValidScope(...)100%11100%
File 2: NormalizeScopeTenantId(...)0%620%
File 2: IsAllowed(...)100%22100%
File 2: GetLifecycle(...)100%44100%
File 2: Matches(...)58.33%2424100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/IdentityProviderConnectionManagementService.cs

#LineLine coverage
 1using System.Security.Claims;
 2using System.Text.Json;
 3using Elsa.Common;
 4using Elsa.Authorization;
 5using Elsa.ExternalAuthentication.Contracts;
 6using Elsa.ExternalAuthentication.Models;
 7using Elsa.ExternalAuthentication.Notifications;
 8using Elsa.ExternalAuthentication.Options;
 9using Elsa.ExternalAuthentication.Permissions;
 10using Elsa.ExternalAuthentication.Providers;
 11using Elsa.Mediator.Contracts;
 12using Microsoft.Extensions.DependencyInjection;
 13using Microsoft.Extensions.Logging;
 14using Microsoft.Extensions.Options;
 15
 16namespace Elsa.ExternalAuthentication.Services;
 17
 18/// <summary>
 19/// Applies management-only invariants before mutating database-owned connections.
 20/// Connection stores remain responsible for durable compare-and-swap and unique-key enforcement.
 21/// </summary>
 10922public sealed partial class IdentityProviderConnectionManagementService(
 10923    IIdentityProviderConnectionStore store,
 10924    IIdentityProviderConnectionRegistry registry,
 10925    IIdentityProviderConnectionValidityAssessor validityAssessor,
 10926    IConnectionRegistryVersionStore registryVersions,
 10927    IExternalAuthenticationAdapterRegistry adapters,
 10928    IAdapterSettingsMigrationService settingsMigrations,
 10929    IUnlinkedIdentityPolicyRegistry policies,
 10930    IExternalUserMatcherRegistry matchers,
 10931    IPermissionGrantSourceRegistry grantSources,
 10932    IEnumerable<ISecretBindingResolver> secretBindingResolvers,
 10933    IEnumerable<IManagedSecretBindingWriter> managedSecretBindingWriters,
 10934    IPermissionDelegationAuthorizer delegationAuthorizer,
 10935    IPermissionEvaluator permissionEvaluator,
 10936    ConnectionRevisionCalculator revisionCalculator,
 10937    ISystemClock clock,
 10938    IOptions<ExternalAuthenticationOptions> options,
 10939    Elsa.Identity.Contracts.IRoleAuthorizationService roleAuthorizationService,
 10940    IExternalAuthenticationSessionStore sessions,
 10941    IServiceProvider services,
 10942    ILogger<IdentityProviderConnectionManagementService> logger)
 43{
 21644    private readonly IReadOnlyDictionary<string, ISecretBindingResolver> _secretBindingResolvers = secretBindingResolver
 21645    private readonly IReadOnlyDictionary<string, IManagedSecretBindingWriter> _managedSecretBindingWriters = managedSecr
 46
 47    public async ValueTask<ManagementConnectionLookupResult> FindAsync(string id, string targetTenantId, CancellationTok
 48    {
 4449        var effective = await registry.FindByIdAsync(targetTenantId, id, cancellationToken);
 4450        if (effective is not null && effective.Scope == ConnectionScope.Host)
 4351            return new ManagementConnectionLookupResult.Found(await validityAssessor.AssessAsync(effective, cancellation
 52
 153        var connection = await store.FindByIdAsync(id, cancellationToken);
 154        if (connection is null || connection.TenantId != ConnectionScope.HostTenantId)
 155            return new ManagementConnectionLookupResult.NotFound();
 56
 057        return new ManagementConnectionLookupResult.Found(await validityAssessor.AssessAsync(ToEffective(connection), ca
 4458    }
 59
 60    /// <summary>Returns the deployment-derived read-only upstream callback URI for management display.</summary>
 4261    public Uri? GetProviderCallbackUri(IdentityProviderConnection connection) => options.Value.Redirects.ExternalCallbac
 4262        ? ExternalAuthenticationCallbackUris.GetAuthorizationCallbackUri(baseUri, connection, BrokerTransactionPurpose.E
 4263        : null;
 64
 65    /// <summary>Returns the deployment-derived read-only callback URI used by provider preview sign-ins.</summary>
 4266    public Uri? GetProviderPreviewCallbackUri(IdentityProviderConnection connection) => options.Value.Redirects.External
 4267        ? ExternalAuthenticationCallbackUris.GetAuthorizationCallbackUri(baseUri, connection, BrokerTransactionPurpose.P
 4268        : null;
 69
 70    public async ValueTask<IReadOnlyCollection<EffectiveIdentityProviderConnection>> ListAsync(string targetTenantId, Co
 71    {
 272        var effective = await registry.GetAsync(targetTenantId, cancellationToken);
 273        var matches = effective.Connections
 674            .Where(x => x.Scope == ConnectionScope.Host)
 675            .Where(x => Matches(x, filter))
 676            .OrderBy(x => x.Scope.Kind)
 677            .ThenBy(x => x.Connection.DisplayOrder)
 678            .ThenBy(x => x.Connection.Key, StringComparer.Ordinal)
 679            .ThenBy(x => x.Connection.Id, StringComparer.Ordinal)
 280            .ToArray();
 881        return await Task.WhenAll(matches.Select(x => validityAssessor.AssessAsync(x, cancellationToken).AsTask()));
 282    }
 83
 84    public async ValueTask<ManagementConnectionMutationResult> CreateAsync(IdentityProviderConnection connection, Claims
 85    {
 2786        NormalizeForCreate(connection, targetTenantId);
 2787        if (!CanMutate(connection.TenantId, targetTenantId))
 088            return new ManagementConnectionMutationResult.Forbidden();
 2789        var validation = await ValidateAsync(connection, actor, targetTenantId, requireCompleteConfiguration: false, con
 2790        if (!validation.IsValid)
 791            return new ManagementConnectionMutationResult.ValidationFailed(validation);
 2092        if (await CollidesWithConfigurationOrHostAsync(connection, null, targetTenantId, cancellationToken))
 293            return new ManagementConnectionMutationResult.Conflict("connection_key_conflict");
 94
 1895        connection.MaterialRevision = revisionCalculator.CalculateMaterialRevision(connection);
 1896        var result = await store.CreateAsync(connection, cancellationToken);
 1897        return await ProcessMutationAsync(result, actor, "created", null, cancellationToken);
 2798    }
 99
 100    public async ValueTask<ManagementConnectionMutationResult> UpdateAsync(string id, IdentityProviderConnection candida
 101    {
 19102        var existing = await store.FindByIdAsync(id, cancellationToken);
 19103        if (existing is null || existing.TenantId != ConnectionScope.HostTenantId)
 0104            return new ManagementConnectionMutationResult.NotFound();
 19105        if (!CanMutate(existing.TenantId, targetTenantId))
 0106            return new ManagementConnectionMutationResult.Forbidden();
 107
 19108        candidate.Id = existing.Id;
 19109        candidate.CreatedAt = existing.CreatedAt;
 19110        candidate.Revision = existing.Revision;
 19111        candidate.ArchivedAt = existing.ArchivedAt;
 19112        NormalizeForUpdate(candidate, existing);
 19113        if (!CanMutate(candidate.TenantId, targetTenantId))
 0114            return new ManagementConnectionMutationResult.Forbidden();
 19115        if (!string.Equals(existing.Key, candidate.Key, StringComparison.Ordinal))
 1116            return new ManagementConnectionMutationResult.Conflict("connection_key_immutable");
 18117        var requireUnsafeConfirmation = adapters.TryGet(candidate.AdapterType, out var candidateAdapter) &&
 18118            UnsafeSettingsChanged(existing.AdapterSettings, candidate.AdapterSettings, candidateAdapter.Describe());
 18119        var validation = await ValidateAsync(candidate, actor, targetTenantId, requireCompleteConfiguration: candidate.I
 17120        if (!validation.IsValid)
 4121            return new ManagementConnectionMutationResult.ValidationFailed(validation);
 122
 13123        if (await IsBlockedByFinalLoginPathGuardAsync(existing, candidate, targetTenantId, actor, confirmFinalLoginPathO
 1124            return new ManagementConnectionMutationResult.Conflict("final_login_path_guard");
 125
 12126        candidate.MaterialRevision = revisionCalculator.CalculateMaterialRevision(candidate);
 12127        var result = await store.UpdateAsync(candidate, expectedRevision, cancellationToken);
 12128        return await ProcessMutationAsync(result, actor, "updated", GetLifecycle(existing), cancellationToken, existing)
 18129    }
 130
 131    public async ValueTask<ManagementConnectionMutationResult> ChangeLifecycleAsync(string id, ConnectionLifecycle actio
 132    {
 5133        var existing = await store.FindByIdAsync(id, cancellationToken);
 5134        if (existing is null || existing.TenantId != ConnectionScope.HostTenantId)
 0135            return new ManagementConnectionMutationResult.NotFound();
 5136        if (!CanMutate(existing.TenantId, targetTenantId))
 0137            return new ManagementConnectionMutationResult.Forbidden();
 138
 5139        var previousLifecycle = GetLifecycle(existing);
 5140        var candidate = IdentityProviderConnectionCloner.Clone(existing);
 141        switch (action)
 142        {
 143            case ConnectionLifecycle.Enabled:
 144            {
 2145                var validation = await ValidateAsync(candidate, actor, targetTenantId, requireCompleteConfiguration: tru
 2146                if (!validation.IsValid)
 1147                    return new ManagementConnectionMutationResult.ValidationFailed(validation);
 1148                if (candidate.ArchivedAt.HasValue)
 0149                    return new ManagementConnectionMutationResult.Conflict("connection_archived");
 1150                if (candidate.IsPreferred)
 151                {
 0152                    var current = await registry.GetAsync(targetTenantId, cancellationToken);
 0153                    if (current.Connections.Any(x =>
 0154                            x is { Ownership: ConnectionSourceOwnership.Configuration, IsShadowed: false, Connection: { 
 0155                            x.Validity != ConnectionValidity.Invalid &&
 0156                            (!candidate.OverridesConfigurationConnection || !string.Equals(x.Connection.Key, candidate.K
 0157                        return new ManagementConnectionMutationResult.Conflict("configuration_preferred_connection");
 0158                    if (current.Connections.Any(x => x is { Ownership: ConnectionSourceOwnership.Database, Connection: {
 0159                        return new ManagementConnectionMutationResult.Conflict("default_connection_conflict");
 160                }
 1161                candidate.IsEnabled = true;
 1162                break;
 163            }
 164            case ConnectionLifecycle.Disabled:
 1165                if (candidate.ArchivedAt.HasValue)
 0166                    return new ManagementConnectionMutationResult.Conflict("connection_archived");
 1167                candidate.IsEnabled = false;
 1168                break;
 169            case ConnectionLifecycle.Archived:
 1170                candidate.IsEnabled = false;
 1171                candidate.ArchivedAt = clock.UtcNow;
 1172                break;
 173            case ConnectionLifecycle.Draft:
 1174                if (!candidate.ArchivedAt.HasValue)
 0175                    return new ManagementConnectionMutationResult.Conflict("connection_not_archived");
 1176                candidate.ArchivedAt = null;
 1177                candidate.IsEnabled = false;
 1178                break;
 179            default:
 0180                return new ManagementConnectionMutationResult.Conflict("invalid_lifecycle_action");
 181        }
 182
 4183        candidate.UpdatedAt = clock.UtcNow;
 4184        candidate.MaterialRevision = revisionCalculator.CalculateMaterialRevision(candidate);
 4185        if (await IsBlockedByFinalLoginPathGuardAsync(existing, candidate, targetTenantId, actor, confirmFinalLoginPathO
 0186            return new ManagementConnectionMutationResult.Conflict("final_login_path_guard");
 4187        var result = await store.UpdateAsync(candidate, expectedRevision, cancellationToken);
 4188        var processed = await ProcessMutationAsync(result, actor, action.ToString().ToLowerInvariant(), previousLifecycl
 4189        if (processed is ManagementConnectionMutationResult.Success && action == ConnectionLifecycle.Disabled && revokeA
 190        {
 1191            var connectionKey = ConnectionRevisionCalculator.NormalizeKey(candidate.Key);
 1192            var revokedCount = await sessions.RevokeActiveForConnectionAsync(connectionKey, "connection_disabled", clock
 1193            await PublishBulkSessionRevocationAsync(candidate, actor, revokedCount);
 194        }
 4195        return processed;
 5196    }
 197
 198    public async ValueTask<ConnectionValidationResult> ValidateAsync(IdentityProviderConnection connection, ClaimsPrinci
 199    {
 51200        var errors = new List<ConnectionValidationError>();
 51201        var warnings = new List<string>();
 51202        var configuredOptions = options.Value;
 51203        ValidateEnvelope(connection, configuredOptions, errors);
 204
 51205        if (!adapters.TryGet(connection.AdapterType, out var adapter) || !IsAllowed(configuredOptions.AllowedAdapterType
 0206            errors.Add(new("adapterType", "unavailable", "The selected adapter is not installed or is not allowed by thi
 207
 51208        await ValidatePolicyAsync(connection, actor, targetTenantId, configuredOptions, errors, cancellationToken);
 50209        ValidateGrantSources(connection, configuredOptions, errors);
 50210        if (connection.PermissionGrantSources.Count != 0)
 211        {
 0212            var delegation = await delegationAuthorizer.AuthorizeAsync(actor, connection.PermissionGrantSources.ToArray(
 0213            if (!delegation.IsAuthorized)
 0214                errors.Add(new("permissionGrantSources", "delegation_denied", "The caller may not delegate one or more c
 215        }
 216
 50217        if (adapter is null)
 0218            return new(false, errors, warnings);
 219
 50220        await ApplySettingsMigrationAsync(connection, errors, cancellationToken);
 50221        if (errors.Count != 0)
 10222            return new(false, errors, warnings);
 223
 40224        var descriptor = adapter.Describe();
 40225        ValidateSecretBindingFields(connection, descriptor, requireCompleteConfiguration, errors);
 40226        ValidateSecretBindingsAreNotAdapterSettings(connection.AdapterSettings, descriptor, errors);
 40227        if (requireUnsafeConfirmation && UsesUnsafeSettings(connection.AdapterSettings, descriptor) && (!confirmUnsafeSe
 0228            errors.Add(new("adapterSettings", "unsafe_confirmation_required", "Unsafe provider trust settings require pe
 229
 40230        if (requireCompleteConfiguration)
 231        {
 7232            var secretStates = await GetSecretStatesAsync(connection, cancellationToken);
 16233            foreach (var (name, state) in secretStates)
 234            {
 1235                if (!state.IsConfigured)
 0236                    errors.Add(new($"secretBindings.{name}", "required", "A required secret binding is not configured.")
 1237                else if (!state.IsResolvable)
 0238                    errors.Add(new($"secretBindings.{name}", "unresolvable", "The secret binding cannot be resolved."));
 239            }
 240        }
 241
 40242        if (errors.Count != 0 || allowIncompleteDraft)
 34243            return new(errors.Count == 0, errors, warnings);
 244
 6245        var effective = ToEffective(connection);
 6246        var adapterValidation = await adapter.ValidateAsync(new(effective, new Dictionary<string, ResolvedSecretBinding>
 6247        errors.AddRange(adapterValidation.Errors);
 6248        warnings.AddRange(adapterValidation.Warnings);
 6249        return new(errors.Count == 0 && adapterValidation.IsValid, errors, warnings);
 50250    }
 251
 42252    public ValueTask<IReadOnlyDictionary<string, SecretBindingState>> GetSecretBindingStatesAsync(IdentityProviderConnec
 253
 5254    public SecretBindingPresentation PresentSecretBinding(SecretBinding binding, SecretBindingState? state) => new(
 5255        binding.Ownership == SecretBindingOwnership.Managed ? "managed" : "external",
 5256        state?.IsConfigured ?? false,
 5257        state?.IsResolvable ?? false);
 258
 1259    public bool CanCreateConfigurationOverride() => options.Value.AllowConfigurationConnectionOverrides;
 260
 261    public bool CanPromoteToConfigurationOverride(EffectiveIdentityProviderConnection connection) =>
 42262        connection is { Ownership: ConnectionSourceOwnership.Database, IsShadowed: true, Connection.ArchivedAt: null } &
 42263        options.Value.AllowConfigurationConnectionOverrides;
 264
 265    private async ValueTask<bool> IsBlockedByFinalLoginPathGuardAsync(IdentityProviderConnection existing, IdentityProvi
 266    {
 17267        var guard = services.GetService<FinalLoginPathGuard>();
 17268        if (guard is null)
 0269            return false;
 270
 17271        var guardExisting = existing;
 17272        if (!existing.OverridesConfigurationConnection && candidate.OverridesConfigurationConnection)
 273        {
 2274            var normalizedKey = ConnectionRevisionCalculator.NormalizeKey(candidate.Key);
 2275            var effective = await registry.GetAsync(targetTenantId, cancellationToken);
 2276            var displacedConfigurationConnection = effective.Connections.FirstOrDefault(x =>
 4277                x is { Ownership: ConnectionSourceOwnership.Configuration, IsShadowed: false } &&
 4278                string.Equals(ConnectionRevisionCalculator.NormalizeKey(x.Connection.Key), normalizedKey, StringComparis
 2279            if (displacedConfigurationConnection is not null)
 2280                guardExisting = displacedConfigurationConnection.Connection;
 2281        }
 282
 17283        return await guard.AuthorizeAsync(guardExisting, candidate, targetTenantId, actor, confirmedOverride, cancellati
 17284    }
 285
 286    private async ValueTask<ManagementConnectionMutationResult> ProcessMutationAsync(ConnectionMutationResult result, Cl
 287    {
 288        switch (result)
 289        {
 290            case ConnectionMutationResult.Created(var createdConnection):
 18291                await RunPostCommitActionsAsync(createdConnection, actor, operation, previousLifecycle, previousConnecti
 18292                return new ManagementConnectionMutationResult.Success(createdConnection);
 293            case ConnectionMutationResult.Updated(var updatedConnection):
 14294                await RunPostCommitActionsAsync(updatedConnection, actor, operation, previousLifecycle, previousConnecti
 14295                return new ManagementConnectionMutationResult.Success(updatedConnection);
 296            case ConnectionMutationResult.NotFound:
 0297                return new ManagementConnectionMutationResult.NotFound();
 298            case ConnectionMutationResult.DuplicateKey:
 0299                return new ManagementConnectionMutationResult.Conflict("connection_key_conflict");
 300            case ConnectionMutationResult.RevisionConflict(var currentRevision):
 2301                return new ManagementConnectionMutationResult.PreconditionFailed(currentRevision);
 302            default:
 0303                throw new InvalidOperationException("The connection store returned an unknown mutation result.");
 304        }
 34305    }
 306
 307    private async ValueTask RunPostCommitActionsAsync(IdentityProviderConnection connection, ClaimsPrincipal actor, stri
 308    {
 309        try
 310        {
 32311            await registryVersions.AdvanceAsync(CancellationToken.None);
 32312        }
 0313        catch (Exception exception)
 314        {
 0315            logger.LogCritical(exception, "Connection {ConnectionId} was committed, but advancing the external-authentic
 0316        }
 317
 318        try
 319        {
 32320            await PublishAsync(connection, actor, operation, previousLifecycle, previousConnection, CancellationToken.No
 31321        }
 1322        catch (Exception exception)
 323        {
 1324            logger.LogError(exception, "Connection {ConnectionId} was committed, but publishing external-authentication 
 1325        }
 32326    }
 327
 328    private async ValueTask PublishBulkSessionRevocationAsync(IdentityProviderConnection connection, ClaimsPrincipal act
 329    {
 1330        if (revokedCount == 0)
 0331            return;
 332
 1333        var notificationSender = services.GetService<INotificationSender>();
 1334        if (notificationSender is null)
 0335            return;
 336
 337        try
 338        {
 1339            var context = new SecurityEventContext(
 1340                actor.FindFirstValue(ClaimTypes.NameIdentifier) ?? actor.FindFirstValue("sub"),
 1341                connection.TenantId,
 1342                connection.Id,
 1343                null,
 1344                clock.UtcNow,
 1345                SecurityEventOutcome.Succeeded,
 1346                Guid.NewGuid().ToString("N"),
 1347                "Active external authentication sessions were revoked when the connection was disabled.");
 1348            await notificationSender.SendAsync(new ExternalAuthenticationConnectionSessionsRevoked(context, revokedCount
 1349        }
 0350        catch (Exception exception)
 351        {
 0352            logger.LogError(exception, "Connection {ConnectionId} sessions were revoked, but publishing the aggregate se
 0353        }
 1354    }
 355
 356    private async ValueTask PublishAsync(IdentityProviderConnection connection, ClaimsPrincipal actor, string operation,
 357    {
 32358        var notificationSender = services.GetService<INotificationSender>();
 32359        if (notificationSender is null)
 0360            return;
 361
 32362        var context = new SecurityEventContext(
 32363            actor.FindFirstValue(ClaimTypes.NameIdentifier) ?? actor.FindFirstValue("sub"),
 32364            connection.TenantId,
 32365            connection.Id,
 32366            null,
 32367            clock.UtcNow,
 32368            SecurityEventOutcome.Succeeded,
 32369            Guid.NewGuid().ToString("N"),
 32370            "Identity provider connection management operation completed.");
 32371        await notificationSender.SendAsync(new IdentityProviderConnectionChanged(context, operation, connection.Revision
 31372        if (previousLifecycle is { } previous && previous != GetLifecycle(connection))
 4373            await notificationSender.SendAsync(new IdentityProviderConnectionLifecycleChanged(context, previous.ToString
 31374        if (previousConnection is not null)
 375        {
 13376            var fields = previousConnection.SecretBindings.Keys
 13377                .Concat(connection.SecretBindings.Keys)
 13378                .Distinct(StringComparer.Ordinal)
 17379                .Where(field => !previousConnection.SecretBindings.TryGetValue(field, out var before) || !connection.Sec
 32380            foreach (var field in fields)
 381            {
 3382                previousConnection.SecretBindings.TryGetValue(field, out var previousBinding);
 3383                connection.SecretBindings.TryGetValue(field, out var binding);
 3384                var isConfigured = binding is not null && _secretBindingResolvers.TryGetValue(binding.ResolverType, out 
 3385                await notificationSender.SendAsync(new IdentityProviderConnectionSecretBindingChanged(context, field, bi
 3386            }
 387        }
 31388    }
 389
 390    private async ValueTask<bool> CollidesWithConfigurationOrHostAsync(IdentityProviderConnection candidate, string? sel
 391    {
 20392        var lookupTenant = candidate.TenantId == ConnectionScope.HostTenantId ? ConnectionScope.HostTenantId : candidate
 20393        var effective = await registry.GetAsync(lookupTenant, cancellationToken);
 20394        var key = ConnectionRevisionCalculator.NormalizeKey(candidate.Key);
 22395        if (effective.Connections.Any(x => x.Ownership == ConnectionSourceOwnership.Configuration && x.Scope.TenantId ==
 1396            return !candidate.OverridesConfigurationConnection;
 397
 19398        if (candidate.TenantId != ConnectionScope.HostTenantId)
 0399            return effective.Connections.Any(x =>
 0400            !string.Equals(x.Connection.Id, selfId, StringComparison.Ordinal) &&
 0401            x.Scope.Kind == ConnectionScopeKind.Host &&
 0402            string.Equals(ConnectionRevisionCalculator.NormalizeKey(x.Connection.Key), key, StringComparison.Ordinal));
 403
 19404        var rows = await store.FindAsync(new(), cancellationToken);
 19405        if (rows.Items.Any(x =>
 22406                !string.Equals(x.Id, selfId, StringComparison.Ordinal) &&
 22407                x.TenantId != ConnectionScope.HostTenantId &&
 22408                string.Equals(ConnectionRevisionCalculator.NormalizeKey(x.Key), key, StringComparison.Ordinal)))
 1409            return true;
 410
 18411        return options.Value.ConfigurationConnections.Any(x =>
 18412            x.TenantId != ConnectionScope.HostTenantId &&
 18413            string.Equals(ConnectionRevisionCalculator.NormalizeKey(x.Key), key, StringComparison.Ordinal));
 20414    }
 415
 416    private async ValueTask<IReadOnlyDictionary<string, SecretBindingState>> GetSecretStatesAsync(IdentityProviderConnec
 417    {
 49418        var states = new Dictionary<string, SecretBindingState>(StringComparer.Ordinal);
 110419        foreach (var (name, binding) in connection.SecretBindings)
 420        {
 6421            if (!_secretBindingResolvers.TryGetValue(binding.ResolverType, out var resolver))
 0422                states[name] = new(false, false);
 423            else
 6424                states[name] = await resolver.GetStateAsync(binding, cancellationToken);
 425        }
 426
 49427        return states;
 49428    }
 429
 430    private async ValueTask ApplySettingsMigrationAsync(IdentityProviderConnection connection, ICollection<ConnectionVal
 431    {
 432        try
 433        {
 50434            var migration = await settingsMigrations.MigrateAsync(connection.AdapterType, connection.AdapterSettingsVers
 48435            connection.AdapterSettingsVersion = migration.SettingsVersion;
 48436            connection.AdapterSettings = migration.Settings;
 48437        }
 2438        catch (InvalidOperationException)
 439        {
 2440            errors.Add(new("adapterSettingsVersion", "migration_unavailable", "The adapter settings version is not compa
 2441        }
 50442    }
 443
 444    private void NormalizeForCreate(IdentityProviderConnection connection, string targetTenantId)
 445    {
 27446        connection.Id = string.IsNullOrWhiteSpace(connection.Id) ? Guid.NewGuid().ToString("N") : connection.Id;
 27447        connection.Key = connection.Key?.Trim() ?? string.Empty;
 27448        connection.TenantId = ConnectionScope.HostTenantId;
 27449        connection.IsEnabled = false;
 27450        connection.ArchivedAt = null;
 27451        connection.Revision = 1;
 27452        connection.CreatedAt = clock.UtcNow;
 27453        connection.UpdatedAt = clock.UtcNow;
 27454        connection.MaterialRevision = revisionCalculator.CalculateMaterialRevision(connection);
 27455    }
 456
 457    private void NormalizeForUpdate(IdentityProviderConnection candidate, IdentityProviderConnection existing)
 458    {
 19459        candidate.Key = candidate.Key?.Trim() ?? string.Empty;
 19460        candidate.TenantId = ConnectionScope.HostTenantId;
 19461        candidate.IsEnabled = existing.IsEnabled;
 19462        candidate.UpdatedAt = clock.UtcNow;
 19463        candidate.SecretBindings ??= new Dictionary<string, SecretBinding>(StringComparer.Ordinal);
 19464        candidate.PermissionGrantSources ??= [];
 19465        candidate.ClaimProjection ??= ClaimProjection.Empty;
 19466    }
 467
 468    private static void ValidateEnvelope(IdentityProviderConnection connection, ExternalAuthenticationOptions configured
 469    {
 51470        if (!configuredOptions.EnableDatabaseConnections)
 0471            errors.Add(new("source", "disabled", "Database-owned connections are disabled by deployment configuration.")
 51472        if (connection.OverridesConfigurationConnection && !configuredOptions.AllowConfigurationConnectionOverrides)
 1473            errors.Add(new("overridesConfigurationConnection", "not_allowed", "This deployment does not allow database c
 51474        if (string.IsNullOrWhiteSpace(connection.Key) || connection.Key.Length > 128 || connection.Key.Any(char.IsWhiteS
 0475            errors.Add(new("key", "invalid", "Connection keys must be non-empty lowercase URL-safe tokens up to 128 char
 51476        else if (!ConnectionKeyPattern().IsMatch(connection.Key))
 1477            errors.Add(new("key", "invalid", "Connection keys must use lowercase letters, digits, and interior hyphens o
 51478        if (string.IsNullOrWhiteSpace(connection.DisplayName) || connection.DisplayName.Trim().Length > 256)
 0479            errors.Add(new("displayName", "invalid", "Display name is required and may not exceed 256 characters."));
 51480        if (connection.AdapterSettingsVersion <= 0)
 0481            errors.Add(new("adapterSettingsVersion", "invalid", "Adapter settings version must be positive."));
 51482        if (!Enum.IsDefined(connection.UpstreamLogoutMode))
 0483            errors.Add(new("upstreamLogoutMode", "invalid", "Upstream logout mode is invalid."));
 51484        if (!IsValidScope(connection.TenantId))
 0485            errors.Add(new("scope", "host_scope_required", "Identity provider connections are managed host-wide in this 
 51486        if (connection.ClaimProjection.MaximumClaimCount < 0 || connection.ClaimProjection.MaximumValueLength < 0 || con
 51487            connection.ClaimProjection.MaximumClaimCount > configuredOptions.Claims.MaximumClaimCount || connection.Clai
 0488            errors.Add(new("claimProjection", "invalid", "Claim projection limits exceed deployment bounds."));
 51489        if (!connection.ClaimProjection.RedactedClaimTypes.IsSubsetOf(connection.ClaimProjection.AllowedClaimTypes))
 0490            errors.Add(new("claimProjection.redactedClaimTypes", "invalid", "Redacted claim types must also be allowed c
 51491    }
 492
 493    private async ValueTask ValidatePolicyAsync(IdentityProviderConnection connection, ClaimsPrincipal actor, string tar
 494    {
 495        // The roles the candidate would actually assign. A policy that does not create users assigns none,
 496        // and so does no policy at all -- which is what makes switching away from, or clearing, a stored
 497        // create-user fallback a change rather than a no-op.
 51498        var defaultRoleIds = connection.UnlinkedPolicy is { } candidate && UsesCreateUserFallback(candidate)
 51499            ? Policies.CreateUserUnlinkedIdentityPolicy.ReadRoleIds(candidate.Settings)
 51500            : [];
 501
 502        // Two independent checks, reported separately because they answer different questions. The
 503        // permission asks whether this actor may decide what auto-created users receive; the subset rule
 504        // asks whether these particular roles stay within what the actor already holds. Only the second
 505        // existed, which left the sibling resource guarded on the write path while the roles inside it
 506        // were not -- see #7977.
 507        //
 508        // Gated on the effective set *changing*, not on it being non-empty, and evaluated before the
 509        // null-policy early return. Validation runs on every update, on enabling a connection, and on
 510        // read-only validate, so keying off presence would stop an administrator without this permission
 511        // from editing an unrelated field once anyone had set roles. Evaluating it only for non-null
 512        // policies would be worse: omitting unlinkedPolicy from an update clears a stored fallback and
 513        // drops its role assignments, the same decision as switching it to 'reject'.
 514        //
 515        // The cheap in-memory permission check goes first: the unchanged-roles comparison rebuilds the
 516        // effective registry, and its answer is irrelevant for an actor who holds the permission.
 51517        if (!permissionEvaluator.HasPermission(actor, ExternalAuthenticationResourcePermissions.PolicyDefaultRoles, Core
 51518            && !await DefaultRolesAreUnchangedAsync(connection, targetTenantId, defaultRoleIds, cancellationToken))
 4519            errors.Add(new("unlinkedPolicy.defaultRoleIds", "forbidden", "Changing the default roles for an unlinked ide
 520
 51521        if (connection.UnlinkedPolicy is not { } policy)
 36522            return;
 15523        if (!configuredOptions.UnlinkedIdentityPolicy.AllowDatabaseConnectionOverride)
 0524            errors.Add(new("unlinkedPolicy", "not_allowed", "This deployment does not allow database connection policy o
 15525        else if (policy.SettingsVersion <= 0 || !policies.TryGet(policy.Type, out _) || !IsAllowed(configuredOptions.All
 0526            errors.Add(new("unlinkedPolicy", "unavailable", "The selected unlinked identity policy is not installed or a
 527        else
 528        {
 529            // The subset rule only has something to say about roles actually being assigned.
 15530            if (UsesCreateUserFallback(policy) && !await roleAuthorizationService.CanAssignRolesAsync(actor, defaultRole
 1531                errors.Add(new("unlinkedPolicy.defaultRoleIds", "forbidden", "The selected default roles are unavailable
 532
 14533            if (string.Equals(policy.Type, Policies.MatchExternalUserUnlinkedIdentityPolicy.PolicyType, StringComparison
 14534                (!TryGetMatcherSelection(policy.Settings, out var matcherType, out var matcherSettingsVersion) ||
 14535                 !IsAllowed(configuredOptions.AllowedExternalUserMatcherTypes, matcherType) ||
 14536                 !matchers.TryGet(matcherType, out _) ||
 27537                 matchers.ListDescriptors().All(x => !string.Equals(x.Type, matcherType, StringComparison.Ordinal) || x.
 1538                errors.Add(new("unlinkedPolicy.matcher", "unavailable", "The selected external user matcher is not insta
 14539        }
 50540    }
 541
 542    /// <summary>Whether <paramref name="candidateRoleIds"/> matches what the connection already assigns.</summary>
 543    /// <remarks>
 544    /// The baseline comes from the registry rather than the database store, because a configuration-owned
 545    /// connection has no database record: looking only there made its configured roles read as newly assigned
 546    /// on every validation, so a caller with view access could not validate one at all. The registry answers
 547    /// for both ownerships, which is the question being asked -- what does this connection assign today.
 548    /// </remarks>
 549    private async ValueTask<bool> DefaultRolesAreUnchangedAsync(IdentityProviderConnection connection, string targetTena
 550    {
 11551        var existing = string.IsNullOrWhiteSpace(connection.Id)
 11552            ? null
 11553            : (await registry.FindByIdAsync(targetTenantId, connection.Id, cancellationToken))?.Connection
 11554              ?? await store.FindByIdAsync(connection.Id, cancellationToken);
 11555        var storedRoleIds = existing?.UnlinkedPolicy is { } storedPolicy && UsesCreateUserFallback(storedPolicy)
 11556            ? Policies.CreateUserUnlinkedIdentityPolicy.ReadRoleIds(storedPolicy.Settings)
 11557            : [];
 558
 559        // Order is not meaningful in a role set, so a reordering is not a change.
 19560        return storedRoleIds.OrderBy(x => x, StringComparer.Ordinal).SequenceEqual(candidateRoleIds.OrderBy(x => x, Stri
 11561    }
 562
 563    private static bool UsesCreateUserFallback(PolicySelection policy) =>
 35564        string.Equals(policy.Type, Policies.CreateUserUnlinkedIdentityPolicy.PolicyType, StringComparison.Ordinal) ||
 35565        string.Equals(policy.Type, Policies.MatchExternalUserUnlinkedIdentityPolicy.PolicyType, StringComparison.Ordinal
 35566        string.Equals(ReadString(policy.Settings, "noMatchAction"), "create-user", StringComparison.OrdinalIgnoreCase);
 567
 568    private static bool TryGetMatcherSelection(JsonElement settings, out string matcherType, out int settingsVersion)
 569    {
 14570        matcherType = string.Empty;
 14571        settingsVersion = 0;
 14572        if (settings.ValueKind != JsonValueKind.Object || !settings.TryGetProperty("matcher", out var matcher) || matche
 0573            return false;
 574
 14575        matcherType = type.GetString() ?? string.Empty;
 14576        return !string.IsNullOrWhiteSpace(matcherType) && matcher.TryGetProperty("settingsVersion", out var version) && 
 577    }
 578
 579    private static string? ReadString(JsonElement settings, string propertyName) =>
 35580        settings.ValueKind == JsonValueKind.Object &&
 35581        settings.TryGetProperty(propertyName, out var value) &&
 35582        value.ValueKind == JsonValueKind.String
 35583            ? value.GetString()
 35584            : null;
 585
 586    private void ValidateGrantSources(IdentityProviderConnection connection, ExternalAuthenticationOptions configuredOpt
 587    {
 50588        var orders = new HashSet<int>();
 100589        foreach (var source in connection.PermissionGrantSources)
 590        {
 0591            if (source.SettingsVersion <= 0 || !grantSources.TryGet(source.Type, out _) || !IsAllowed(configuredOptions.
 0592                errors.Add(new("permissionGrantSources", "unavailable", "A selected permission grant source is not insta
 0593            if (!orders.Add(source.Order))
 0594                errors.Add(new("permissionGrantSources", "duplicate_order", "Permission grant source orders must be uniq
 595        }
 50596    }
 597
 598    private static void ValidateSecretBindingFields(IdentityProviderConnection connection, ExternalAuthenticationAdapter
 599    {
 160600        var secretFields = descriptor.Fields.Where(x => x.IsSecretBinding).ToDictionary(x => x.Name, StringComparer.Ordi
 92601        foreach (var name in connection.SecretBindings.Keys)
 6602            if (!secretFields.ContainsKey(name))
 0603                errors.Add(new($"secretBindings.{name}", "undeclared", "The adapter does not declare this secret binding
 40604        if (!requireCompleteConfiguration)
 33605            return;
 23606        foreach (var field in secretFields.Values.Where(x => x.IsRequired))
 1607            if (!connection.SecretBindings.ContainsKey(field.Name))
 1608                errors.Add(new($"secretBindings.{field.Name}", "required", "A required secret binding is missing."));
 7609    }
 610
 611    private static void ValidateSecretBindingsAreNotAdapterSettings(JsonElement settings, ExternalAuthenticationAdapterD
 612    {
 40613        if (settings.ValueKind != JsonValueKind.Object)
 0614            return;
 615
 240616        foreach (var field in descriptor.Fields.Where(x => x.IsSecretBinding))
 40617            if (settings.TryGetProperty(field.Name, out _))
 1618                errors.Add(new($"adapterSettings.{field.Name}", "secret_binding_required", "Secret fields must be config
 40619    }
 620
 621    private static bool UsesUnsafeSettings(JsonElement settings, ExternalAuthenticationAdapterDescriptor descriptor)
 622    {
 21623        if (settings.ValueKind != JsonValueKind.Object)
 0624            return false;
 63625        return descriptor.Fields.Where(x => x.IsUnsafe).Any(field =>
 42626            settings.TryGetProperty(field.Name, out var value) && IsUnsafeFieldValue(field, value));
 627    }
 628
 629    private static bool UnsafeSettingsChanged(JsonElement beforeSettings, JsonElement afterSettings, ExternalAuthenticat
 630    {
 18631        if (afterSettings.ValueKind != JsonValueKind.Object)
 0632            return false;
 633
 108634        foreach (var field in descriptor.Fields.Where(x => x.IsUnsafe))
 635        {
 18636            if (!afterSettings.TryGetProperty(field.Name, out var afterValue) || !IsUnsafeFieldValue(field, afterValue))
 637                continue;
 2638            if (beforeSettings.ValueKind != JsonValueKind.Object || !beforeSettings.TryGetProperty(field.Name, out var b
 0639                return true;
 640        }
 641
 18642        return false;
 0643    }
 644
 645    private static bool IsUnsafeFieldValue(SettingFieldDescriptor field, JsonElement value) =>
 3646        value.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined and not JsonValueKind.False &&
 3647        (!string.Equals(field.Name, "providerPkce", StringComparison.Ordinal) || value.ValueKind != JsonValueKind.String
 648
 649    private static bool JsonValueEquals(JsonElement left, JsonElement right) =>
 2650        left.ValueKind == right.ValueKind && string.Equals(left.GetRawText(), right.GetRawText(), StringComparison.Ordin
 651
 6652    private static EffectiveIdentityProviderConnection ToEffective(IdentityProviderConnection connection) => new(connect
 6653    private static ConnectionScope ToScope(string tenantId) => tenantId == ConnectionScope.HostTenantId ? ConnectionScop
 70654    private static bool CanMutate(string connectionTenantId, string targetTenantId) => connectionTenantId == ConnectionS
 51655    private static bool IsValidScope(string tenantId) => tenantId == ConnectionScope.HostTenantId;
 0656    private static string NormalizeScopeTenantId(string requestedTenantId, string fallback) => requestedTenantId is null
 80657    private static bool IsAllowed(ICollection<string> allowedTypes, string type) => allowedTypes.Count == 0 || allowedTy
 34658    private static ConnectionLifecycle GetLifecycle(IdentityProviderConnection connection) => connection.ArchivedAt.HasV
 659    private static bool Matches(EffectiveIdentityProviderConnection connection, ConnectionFilter filter) =>
 6660        (filter.Ownership is null || filter.Ownership == connection.Ownership) &&
 6661        (filter.Scope is null || filter.Scope == connection.Scope) &&
 6662        (string.IsNullOrWhiteSpace(filter.Search) || connection.Connection.Key.Contains(filter.Search, StringComparison.
 6663        (string.IsNullOrWhiteSpace(filter.AdapterType) || string.Equals(filter.AdapterType, connection.Connection.Adapte
 6664        (!filter.IsEnabled.HasValue || filter.IsEnabled.Value == connection.Connection.IsEnabled) &&
 6665        (!filter.IsArchived.HasValue || filter.IsArchived.Value == connection.Connection.ArchivedAt.HasValue);
 666
 667    [System.Text.RegularExpressions.GeneratedRegex("^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$")]
 668    private static partial System.Text.RegularExpressions.Regex ConnectionKeyPattern();
 669}
 670
 671public abstract record ManagementConnectionLookupResult
 672{
 673    private ManagementConnectionLookupResult() { }
 674    public sealed record Found(EffectiveIdentityProviderConnection Connection) : ManagementConnectionLookupResult;
 675    public sealed record NotFound : ManagementConnectionLookupResult;
 676}
 677
 678public abstract record ManagementConnectionMutationResult
 679{
 680    private ManagementConnectionMutationResult() { }
 681    public sealed record Success(IdentityProviderConnection Connection) : ManagementConnectionMutationResult;
 682    public sealed record NotFound : ManagementConnectionMutationResult;
 683    public sealed record Conflict(string Code) : ManagementConnectionMutationResult;
 684    public sealed record PreconditionFailed(long CurrentRevision) : ManagementConnectionMutationResult;
 685    public sealed record Forbidden : ManagementConnectionMutationResult;
 686    public sealed record ValidationFailed(ConnectionValidationResult Validation) : ManagementConnectionMutationResult;
 687}

Methods/Properties

ConnectionKeyPattern()
.ctor(Elsa.ExternalAuthentication.Contracts.IIdentityProviderConnectionStore,Elsa.ExternalAuthentication.Contracts.IIdentityProviderConnectionRegistry,Elsa.ExternalAuthentication.Contracts.IIdentityProviderConnectionValidityAssessor,Elsa.ExternalAuthentication.Contracts.IConnectionRegistryVersionStore,Elsa.ExternalAuthentication.Contracts.IExternalAuthenticationAdapterRegistry,Elsa.ExternalAuthentication.Contracts.IAdapterSettingsMigrationService,Elsa.ExternalAuthentication.Contracts.IUnlinkedIdentityPolicyRegistry,Elsa.ExternalAuthentication.Contracts.IExternalUserMatcherRegistry,Elsa.ExternalAuthentication.Contracts.IPermissionGrantSourceRegistry,System.Collections.Generic.IEnumerable`1<Elsa.ExternalAuthentication.Contracts.ISecretBindingResolver>,System.Collections.Generic.IEnumerable`1<Elsa.ExternalAuthentication.Contracts.IManagedSecretBindingWriter>,Elsa.ExternalAuthentication.Contracts.IPermissionDelegationAuthorizer,Elsa.Authorization.IPermissionEvaluator,Elsa.ExternalAuthentication.Services.ConnectionRevisionCalculator,Elsa.Common.ISystemClock,Microsoft.Extensions.Options.IOptions`1<Elsa.ExternalAuthentication.Options.ExternalAuthenticationOptions>,Elsa.Identity.Contracts.IRoleAuthorizationService,Elsa.ExternalAuthentication.Contracts.IExternalAuthenticationSessionStore,System.IServiceProvider,Microsoft.Extensions.Logging.ILogger`1<Elsa.ExternalAuthentication.Services.IdentityProviderConnectionManagementService>)
FindAsync()
GetProviderCallbackUri(Elsa.ExternalAuthentication.Models.IdentityProviderConnection)
GetProviderPreviewCallbackUri(Elsa.ExternalAuthentication.Models.IdentityProviderConnection)
ListAsync()
CreateAsync()
UpdateAsync()
ChangeLifecycleAsync()
ValidateAsync()
GetSecretBindingStatesAsync(Elsa.ExternalAuthentication.Models.IdentityProviderConnection,System.Threading.CancellationToken)
PresentSecretBinding(Elsa.ExternalAuthentication.Models.SecretBinding,Elsa.ExternalAuthentication.Models.SecretBindingState)
CanCreateConfigurationOverride()
CanPromoteToConfigurationOverride(Elsa.ExternalAuthentication.Contracts.EffectiveIdentityProviderConnection)
IsBlockedByFinalLoginPathGuardAsync()
ProcessMutationAsync()
RunPostCommitActionsAsync()
PublishBulkSessionRevocationAsync()
PublishAsync()
CollidesWithConfigurationOrHostAsync()
GetSecretStatesAsync()
ApplySettingsMigrationAsync()
NormalizeForCreate(Elsa.ExternalAuthentication.Models.IdentityProviderConnection,System.String)
NormalizeForUpdate(Elsa.ExternalAuthentication.Models.IdentityProviderConnection,Elsa.ExternalAuthentication.Models.IdentityProviderConnection)
ValidateEnvelope(Elsa.ExternalAuthentication.Models.IdentityProviderConnection,Elsa.ExternalAuthentication.Options.ExternalAuthenticationOptions,System.Collections.Generic.ICollection`1<Elsa.ExternalAuthentication.Models.ConnectionValidationError>)
ValidatePolicyAsync()
DefaultRolesAreUnchangedAsync()
UsesCreateUserFallback(Elsa.ExternalAuthentication.Models.PolicySelection)
TryGetMatcherSelection(System.Text.Json.JsonElement,System.String&,System.Int32&)
ReadString(System.Text.Json.JsonElement,System.String)
ValidateGrantSources(Elsa.ExternalAuthentication.Models.IdentityProviderConnection,Elsa.ExternalAuthentication.Options.ExternalAuthenticationOptions,System.Collections.Generic.ICollection`1<Elsa.ExternalAuthentication.Models.ConnectionValidationError>)
ValidateSecretBindingFields(Elsa.ExternalAuthentication.Models.IdentityProviderConnection,Elsa.ExternalAuthentication.Models.ExternalAuthenticationAdapterDescriptor,System.Boolean,System.Collections.Generic.ICollection`1<Elsa.ExternalAuthentication.Models.ConnectionValidationError>)
ValidateSecretBindingsAreNotAdapterSettings(System.Text.Json.JsonElement,Elsa.ExternalAuthentication.Models.ExternalAuthenticationAdapterDescriptor,System.Collections.Generic.ICollection`1<Elsa.ExternalAuthentication.Models.ConnectionValidationError>)
UsesUnsafeSettings(System.Text.Json.JsonElement,Elsa.ExternalAuthentication.Models.ExternalAuthenticationAdapterDescriptor)
UnsafeSettingsChanged(System.Text.Json.JsonElement,System.Text.Json.JsonElement,Elsa.ExternalAuthentication.Models.ExternalAuthenticationAdapterDescriptor)
IsUnsafeFieldValue(Elsa.ExternalAuthentication.Models.SettingFieldDescriptor,System.Text.Json.JsonElement)
JsonValueEquals(System.Text.Json.JsonElement,System.Text.Json.JsonElement)
ToEffective(Elsa.ExternalAuthentication.Models.IdentityProviderConnection)
ToScope(System.String)
CanMutate(System.String,System.String)
IsValidScope(System.String)
NormalizeScopeTenantId(System.String,System.String)
IsAllowed(System.Collections.Generic.ICollection`1<System.String>,System.String)
GetLifecycle(Elsa.ExternalAuthentication.Models.IdentityProviderConnection)
Matches(Elsa.ExternalAuthentication.Contracts.EffectiveIdentityProviderConnection,Elsa.ExternalAuthentication.Models.ConnectionFilter)