< Summary

Information
Class: Elsa.ExternalAuthentication.Services.ExternalAuthenticationRoleDeletionDependencyContributor
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs
Line coverage
91%
Covered lines: 218
Uncovered lines: 19
Coverable lines: 237
Total lines: 438
Line coverage: 91.9%
Branch coverage
78%
Covered branches: 120
Total branches: 152
Branch coverage: 78.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1using System.Security.Claims;
 2using System.Security.Cryptography;
 3using System.Text;
 4using System.Text.Json;
 5using System.Text.Json.Nodes;
 6using Elsa.Authorization;
 7using Elsa.Common.Multitenancy;
 8using Elsa.ExternalAuthentication.Contracts;
 9using Elsa.ExternalAuthentication.Models;
 10using Elsa.ExternalAuthentication.Notifications;
 11using Elsa.ExternalAuthentication.Options;
 12using Elsa.ExternalAuthentication.Permissions;
 13using Elsa.ExternalAuthentication.Policies;
 14using Elsa.Identity.Contracts;
 15using Elsa.Identity.Entities;
 16using Elsa.Identity.Models;
 17using Microsoft.Extensions.Options;
 18
 19namespace Elsa.ExternalAuthentication.Services;
 20
 21/// <summary>Guards Elsa Role deletion against JIT-policy default-role references.</summary>
 22/// <remarks>
 23/// Roles are tenant-scoped, so impact and remediation only ever consider connections in the role's own tenant
 24/// context: the resolved role's own <c>TenantId</c>. The tenant active on <see cref="ITenantAccessor"/> is used
 25/// only as a fallback when the role cannot be resolved, because with multitenancy disabled (the default) the EF
 26/// Core role store installs no tenant query filter and can resolve a tenant-owned role by ID regardless of the
 27/// ambient tenant; trusting the ambient tenant instead of the resolved role's tenant would then let this
 28/// contributor scan the wrong tenant's connections while the coordinator deletes a role belonging to another
 29/// tenant. A connection carrying another tenant's ID is out of scope in both directions, for impact and for
 30/// remediation.
 31/// Host-scoped connections (<see cref="ConnectionScope.HostTenantId"/>, and configuration entries that leave the
 32/// tenant blank, which are materialized at host scope) stay in scope for every tenant. The connection registry
 33/// resolves the host scope for every signing-in tenant, and a connection's default role IDs are then resolved by
 34/// <c>ExternalIdentityUserProvisioningService</c> through <see cref="IRoleProvider"/> in the signing-in user's
 35/// tenant, so a host connection naming role ID X really does reference tenant A's role X.
 36/// A tenant-agnostic role (<see cref="Tenant.AgnosticTenantId"/>) is visible from every tenant, so its tenant
 37/// context is every tenant: impact and remediation for such a role scan every stored connection and every
 38/// configuration entry regardless of tenant, instead of the single active tenant plus host scope. Authorizing a
 39/// replacement role, however, is still performed through the ambient tenant's role services, so when the
 40/// deletion target is agnostic the replacement role must itself be agnostic; a tenant-scoped replacement is
 41/// rejected rather than being authorized in one tenant and written into every tenant's connections.
 42/// In EF Core persistence and <c>MemoryRoleStore</c>, a role's key is its ID alone, so a role ID is unique across
 43/// all tenants and an agnostic/tenant-scoped collision cannot exist. A custom <see cref="IRoleStore"/> can still
 44/// violate that invariant, so resolving a role ID against it can be genuinely ambiguous. That ambiguity is never
 45/// resolved by guessing: widening a tenant-scoped deletion would expose another tenant's references, and narrowing
 46/// an agnostic deletion would leave an agnostic role's references dangling. It fails closed instead. The same
 47/// defensive rule applies to a replacement candidate: an ID that resolves to more than one role is rejected as not
 48/// agnostic rather than guessed at, so it is reported as <c>replacement_role_unavailable_or_unauthorized</c>
 49/// instead of surfacing as an exception.
 50/// </remarks>
 2451public sealed class ExternalAuthenticationRoleDeletionDependencyContributor(
 2452    IIdentityProviderConnectionStore store,
 2453    IOptionsMonitor<ExternalAuthenticationOptions> options,
 2454    IEnumerable<IRoleAuthorizationService> roleAuthorizationServices,
 2455    IEnumerable<IRoleStore> roleStores,
 2456    IConnectionRegistryVersionStore registryVersions,
 2457    ConnectionRevisionCalculator revisionCalculator,
 2458    ExternalAuthenticationSecurityNotifier notifier,
 2459    IPermissionEvaluator permissionEvaluator,
 2460    ITenantAccessor tenantAccessor) : IRoleDeletionDependencyContributor
 61{
 62    public const string SourceName = "external-authentication";
 16063    public string Source => SourceName;
 64
 65    /// <summary>
 66    /// Match the default DI container's direct-service semantics: when persistence replaces the in-memory
 67    /// store, the last registration is the active store.
 68    /// </summary>
 9369    private IRoleStore? ActiveRoleStore => roleStores.LastOrDefault();
 70
 71    public async ValueTask<RoleDeletionDependencySnapshot> InspectAsync(string roleId, CancellationToken cancellationTok
 72    {
 5273        var roleTenantId = await ResolveRoleTenantIdAsync(roleId, cancellationToken);
 4974        var dependencies = new List<RoleDeletionDependency>();
 4975        var configuredConnections = options.CurrentValue.ConfigurationConnections ?? [];
 4976        var configurationIndex = 0;
 10877        foreach (var connection in configuredConnections)
 78        {
 79            // The index is part of the configuration path an operator edits, so out-of-scope entries are
 80            // skipped without renumbering the entries that remain.
 581            if (IsInRoleTenantScope(GetConfigurationScopeTenantId(connection), roleTenantId))
 482                dependencies.AddRange(GetConfigurationDependencies(connection, configurationIndex, roleId));
 583            configurationIndex++;
 84        }
 85
 22286        foreach (var connection in await FindConnectionsInRoleTenantScopeAsync(roleTenantId, cancellationToken))
 87        {
 6288            if (!TryGetRoleReference(connection.UnlinkedPolicy, roleId, out var policyBranch, out _, out var removesLast
 89                continue;
 6290            dependencies.Add(new(
 6291                Source,
 6292                connection.Id,
 6293                connection.Key,
 6294                policyBranch,
 6295                RoleDeletionDependencyOwnership.Database,
 6296                null,
 6297                connection.Revision,
 6298                removesLastDefaultRole));
 99        }
 100
 49101        var ordered = dependencies
 34102            .OrderBy(x => x.Ownership)
 34103            .ThenBy(x => x.OwnerId, StringComparer.Ordinal)
 34104            .ThenBy(x => x.ConfigurationPath, StringComparer.Ordinal)
 49105            .ToArray();
 49106        return new(Source, CalculateVersion(ordered), false, ordered);
 49107    }
 108
 109    public async ValueTask<RoleReferenceRemovalValidationResult> ValidateRemovalAsync(RoleReferenceRemovalRequest reques
 110    {
 29111        var roleAuthorizationService = roleAuthorizationServices.SingleOrDefault();
 29112        if (roleAuthorizationService is null)
 0113            return new RoleReferenceRemovalValidationResult.Forbidden("role_authorization_unavailable");
 29114        if (!permissionEvaluator.HasPermission(request.Actor, ExternalAuthenticationResourcePermissions.Connections, Cor
 29115            !permissionEvaluator.HasPermission(request.Actor, ExternalAuthenticationResourcePermissions.Policies, CoreVe
 29116            !permissionEvaluator.HasPermission(request.Actor, ExternalAuthenticationResourcePermissions.PolicyDefaultRol
 3117            return new RoleReferenceRemovalValidationResult.Forbidden("missing_policy_permissions");
 26118        if (request.Dependencies.Count == 0 ||
 62119            request.Dependencies.Any(x => x.Ownership != RoleDeletionDependencyOwnership.Database || !string.Equals(x.So
 0120            return new RoleReferenceRemovalValidationResult.Conflict("invalid_dependency_set");
 121
 26122        var current = await InspectAsync(request.RoleId, cancellationToken);
 24123        if (!string.Equals(current.Version, request.ExpectedContributorVersion, StringComparison.Ordinal) ||
 55124            current.Dependencies.Any(x => x.Ownership == RoleDeletionDependencyOwnership.Configuration))
 2125            return new RoleReferenceRemovalValidationResult.Conflict("dependency_changed");
 126
 54127        var expectedOwners = request.Dependencies.Select(x => x.OwnerId).ToHashSet(StringComparer.Ordinal);
 22128        var currentOwners = current.Dependencies
 30129            .Where(x => x.Ownership == RoleDeletionDependencyOwnership.Database)
 30130            .Select(x => x.OwnerId)
 22131            .ToHashSet(StringComparer.Ordinal);
 22132        if (!expectedOwners.IsSubsetOf(currentOwners))
 2133            return new RoleReferenceRemovalValidationResult.Conflict("dependency_changed");
 134
 20135        var roleTenantId = await ResolveRoleTenantIdAsync(request.RoleId, cancellationToken);
 82136        foreach (var dependency in request.Dependencies)
 137        {
 24138            var connection = await FindConnectionInRoleTenantScopeAsync(dependency.OwnerId, roleTenantId, cancellationTo
 24139            if (connection is null || connection.Revision != dependency.ExpectedRevision ||
 24140                !TryGetRoleReference(connection.UnlinkedPolicy, request.RoleId, out _, out var roleIds, out _))
 0141                return new RoleReferenceRemovalValidationResult.Conflict("connection_revision_changed");
 51142            var remainingRoleIds = roleIds.Where(x => !string.Equals(x, request.RoleId, StringComparison.Ordinal)).ToArr
 24143            var requiresReplacement = remainingRoleIds.Length == 0 && request.SelectedReferences is not null;
 24144            if (requiresReplacement &&
 24145                (string.IsNullOrWhiteSpace(request.ReplacementRoleId) ||
 24146                 string.Equals(request.ReplacementRoleId, request.RoleId, StringComparison.Ordinal)))
 1147                return new RoleReferenceRemovalValidationResult.Forbidden("replacement_role_unavailable_or_unauthorized"
 148
 149            // Authorization below still resolves through the ambient tenant's role services, so an agnostic
 150            // deletion target may only be replaced by another agnostic role; a tenant-scoped replacement would
 151            // otherwise be authorized in this tenant and then written into every other tenant's connections.
 152            // This does not extend to host-scoped connections: IdentityProviderConnectionManagementService
 153            // forces every managed connection to host scope, and in a deployment without multitenancy roles
 154            // are created scoped to the default tenant rather than agnostic, so requiring an agnostic
 155            // replacement for host-scoped connections would make every replacement remediation impossible in
 156            // the default deployment.
 23157            if (requiresReplacement &&
 23158                string.Equals(roleTenantId, Tenant.AgnosticTenantId, StringComparison.Ordinal) &&
 23159                !await IsAgnosticRoleAsync(request.ReplacementRoleId, cancellationToken))
 4160                return new RoleReferenceRemovalValidationResult.Forbidden("replacement_role_unavailable_or_unauthorized"
 161
 19162            var rolesToAssign = requiresReplacement
 19163                ? new[] { request.ReplacementRoleId! }
 19164                : remainingRoleIds;
 19165            if (!await roleAuthorizationService.CanAssignRolesAsync(request.Actor, rolesToAssign, cancellationToken))
 166            {
 1167                var code = requiresReplacement ? "replacement_role_unavailable_or_unauthorized" : "role_assignment_denie
 1168                return new RoleReferenceRemovalValidationResult.Forbidden(code);
 169            }
 18170        }
 171
 14172        return new RoleReferenceRemovalValidationResult.Valid();
 27173    }
 174
 175    public async ValueTask<RoleReferenceRemovalResult> RemoveEditableReferencesAsync(RoleReferenceRemovalRequest request
 176    {
 12177        var roleAuthorizationService = roleAuthorizationServices.SingleOrDefault();
 12178        if (roleAuthorizationService is null)
 0179            return new RoleReferenceRemovalResult.Failed("role_authorization_unavailable", []);
 12180        var validation = await ValidateRemovalAsync(request, cancellationToken);
 11181        if (validation is RoleReferenceRemovalValidationResult.Forbidden forbidden)
 3182            return new RoleReferenceRemovalResult.Failed(forbidden.Code, []);
 8183        if (validation is RoleReferenceRemovalValidationResult.Conflict conflict)
 1184            return new RoleReferenceRemovalResult.Conflict(conflict.Code, []);
 185
 7186        var roleTenantId = await ResolveRoleTenantIdAsync(request.RoleId, cancellationToken);
 7187        var changedOwnerIds = new List<string>();
 188        try
 189        {
 40190            foreach (var dependency in request.Dependencies.OrderBy(x => x.OwnerId, StringComparer.Ordinal))
 191            {
 9192                var connection = await FindConnectionInRoleTenantScopeAsync(dependency.OwnerId, roleTenantId, cancellati
 9193                if (connection is null || connection.Revision != dependency.ExpectedRevision ||
 9194                    !TryGetRoleReference(connection.UnlinkedPolicy, request.RoleId, out _, out _, out var removesLastDef
 1195                    return new RoleReferenceRemovalResult.Conflict("connection_revision_changed", changedOwnerIds);
 196
 8197                var candidate = IdentityProviderConnectionCloner.Clone(connection);
 8198                candidate.UnlinkedPolicy = candidate.UnlinkedPolicy is { } policy
 8199                    ? policy with { Settings = RemoveRole(policy.Settings, request.RoleId, request.SelectedReferences is
 8200                    : null;
 8201                candidate.UpdatedAt = DateTimeOffset.UtcNow;
 8202                candidate.MaterialRevision = revisionCalculator.CalculateMaterialRevision(candidate);
 203
 8204                if (request.SelectedReferences is not null && removesLastDefaultRole)
 205                {
 4206                    var roleStore = ActiveRoleStore;
 4207                    if (roleStore is null)
 0208                        return new RoleReferenceRemovalResult.Failed("replacement_role_unavailable_or_unauthorized", cha
 4209                    var replacement = await roleStore.FindAsync(new() { Id = request.ReplacementRoleId }, cancellationTo
 4210                    if (replacement is null ||
 4211                        !await roleAuthorizationService.CanAssignRolesAsync(request.Actor, [replacement.Id], cancellatio
 0212                        return new RoleReferenceRemovalResult.Failed("replacement_role_unavailable_or_unauthorized", cha
 213
 214                    // Authorization above still resolves through the ambient tenant's role services, so an
 215                    // agnostic deletion target may only be replaced by another agnostic role; a tenant-scoped
 216                    // replacement would otherwise be authorized in this tenant and then written into every other
 217                    // tenant's connections. This does not extend to host-scoped connections: see the matching
 218                    // guard in ValidateRemovalAsync for why. The check is re-run through IsAgnosticRoleAsync
 219                    // rather than trusting the TenantId on `replacement` from the FindAsync call above, because
 220                    // a same-ID tenant-scoped role added after validation could make that lookup ambiguous;
 221                    // IsAgnosticRoleAsync resolves the candidate itself and rejects an ambiguous match instead
 222                    // of accepting whichever role FindAsync happened to return.
 4223                    if (string.Equals(roleTenantId, Tenant.AgnosticTenantId, StringComparison.Ordinal) &&
 4224                        !await IsAgnosticRoleAsync(request.ReplacementRoleId, cancellationToken))
 0225                        return new RoleReferenceRemovalResult.Failed("replacement_role_unavailable_or_unauthorized", cha
 226                }
 227
 8228                var update = await store.UpdateAsync(candidate, connection.Revision, cancellationToken);
 8229                if (update is not ConnectionMutationResult.Updated updated)
 0230                    return new RoleReferenceRemovalResult.Conflict("connection_revision_changed", changedOwnerIds);
 231
 8232                changedOwnerIds.Add(updated.Connection.Id);
 8233                await registryVersions.AdvanceAsync(cancellationToken);
 8234                await notifier.PublishAsync(
 8235                    new IdentityProviderConnectionChanged(
 8236                        ExternalAuthenticationSecurityNotifier.Context(
 8237                            request.Actor.FindFirstValue(ClaimTypes.NameIdentifier) ?? request.Actor.FindFirstValue("sub
 8238                            updated.Connection.TenantId,
 8239                            updated.Connection.Id,
 8240                            null,
 8241                            SecurityEventOutcome.Succeeded,
 8242                            "An Elsa Role reference was removed from an external authentication JIT policy."),
 8243                        "default-role-removed",
 8244                        updated.Connection.Revision,
 8245                        updated.Connection.MaterialRevision),
 8246                    cancellationToken);
 8247            }
 6248        }
 0249        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 250        {
 0251            throw;
 252        }
 0253        catch
 254        {
 0255            return new RoleReferenceRemovalResult.Failed("storage_error", changedOwnerIds);
 256        }
 257
 6258        return new RoleReferenceRemovalResult.Success(changedOwnerIds);
 11259    }
 260
 261    /// <summary>
 262    /// Resolves the tenant context for the role being deleted: the role's own <c>TenantId</c>
 263    /// (<see cref="Tenant.AgnosticTenantId"/> normalized when the role is tenant-agnostic, in which case its
 264    /// tenant context is every tenant). In EF Core persistence and <c>MemoryRoleStore</c>, a role ID is unique
 265    /// across all tenants (the <c>Roles</c> table and in-memory store both key on <c>Id</c> alone), so this lookup
 266    /// resolves to at most one role there. If a custom store violates that invariant and the ID resolves to more
 267    /// than one role, which tenant's role the coordinator's own delete actually targets is already ambiguous, and
 268    /// this method cannot make the operation consistent by guessing in either direction -- widening would expose
 269    /// another tenant's references for what may be a tenant-scoped deletion, and narrowing would leave an agnostic
 270    /// role's references dangling. It fails closed instead.
 271    /// A missing store or no matching role falls back to the ambient tenant on <see cref="ITenantAccessor"/>,
 272    /// which is the only case where the ambient tenant is trusted: the role cannot be resolved at all, so there
 273    /// is no resolved tenant to prefer over it.
 274    /// </summary>
 275    private async ValueTask<string> ResolveRoleTenantIdAsync(string roleId, CancellationToken cancellationToken)
 276    {
 79277        var roles = await FindRolesByIdAsync(roleId, cancellationToken);
 79278        return roles.Length switch
 79279        {
 0280            0 => tenantAccessor.TenantId.NormalizeTenantId(),
 76281            1 => roles[0].TenantId.NormalizeTenantId(),
 3282            _ => throw new InvalidOperationException(
 3283                $"Role '{roleId}' resolves to {roles.Length} roles across tenant scopes; the deletion target is ambiguou
 79284        };
 76285    }
 286
 287    /// <summary>
 288    /// Resolves whether a candidate role ID (typically a replacement role) is itself tenant-agnostic. Unlike
 289    /// <see cref="ResolveRoleTenantIdAsync"/>, an ambiguous candidate -- a role ID that resolves to more than one
 290    /// role, which a custom store can produce despite the global ID invariant of the built-in stores -- is not the
 291    /// coordinator's own deletion target, so there is no operation to fail closed on by throwing; it is instead
 292    /// treated the same as an unresolved candidate and reported as not agnostic, since there is no single resolved
 293    /// role to trust as safe to write into every tenant's connections.
 294    /// </summary>
 295    private async ValueTask<bool> IsAgnosticRoleAsync(string? roleId, CancellationToken cancellationToken)
 296    {
 10297        if (string.IsNullOrWhiteSpace(roleId))
 0298            return false;
 10299        var roles = await FindRolesByIdAsync(roleId, cancellationToken);
 10300        return roles.Length == 1 && string.Equals(roles[0].TenantId.NormalizeTenantId(), Tenant.AgnosticTenantId, String
 10301    }
 302
 303    /// <summary>Loads every role matching the given ID from the active role store, or an empty result if none is config
 304    private async ValueTask<Role[]> FindRolesByIdAsync(string roleId, CancellationToken cancellationToken)
 305    {
 89306        var roleStore = ActiveRoleStore;
 89307        if (roleStore is null)
 0308            return [];
 89309        return (await roleStore.FindManyAsync(new() { Id = roleId }, cancellationToken)).ToArray();
 89310    }
 311
 312    /// <summary>
 313    /// Loads every stored connection in a single snapshot and filters it in memory to the role's tenant context.
 314    /// Composing the result from separate per-scope reads instead would let a connection's <c>TenantId</c> change
 315    /// between those reads (<see cref="RemoveEditableReferencesAsync"/> permits it via <c>UpdateAsync</c>), so the
 316    /// connection could fall between the reads and appear in neither result. A single snapshot has no gap to fall
 317    /// through.
 318    /// </summary>
 319    private async ValueTask<IReadOnlyCollection<IdentityProviderConnection>> FindConnectionsInRoleTenantScopeAsync(strin
 320    {
 49321        var connections = (await store.FindAsync(new(), cancellationToken)).Items;
 116322        return connections.Where(x => IsInRoleTenantScope(x.TenantId, roleTenantId)).ToArray();
 49323    }
 324
 325    /// <summary>
 326    /// Loads one dependency's connection, reporting a connection outside the role's tenant context as absent so
 327    /// that a caller-supplied owner ID cannot reach across a tenant boundary. An agnostic role's tenant context
 328    /// is every tenant, so any connection loaded by owner ID qualifies.
 329    /// </summary>
 330    private async ValueTask<IdentityProviderConnection?> FindConnectionInRoleTenantScopeAsync(string ownerId, string rol
 331    {
 33332        var connection = await store.FindByIdAsync(ownerId, cancellationToken);
 33333        return connection is not null && IsInRoleTenantScope(connection.TenantId, roleTenantId) ? connection : null;
 33334    }
 335
 336    private static bool IsInRoleTenantScope(string? connectionTenantId, string roleTenantId) =>
 105337        string.Equals(roleTenantId, Tenant.AgnosticTenantId, StringComparison.Ordinal) ||
 105338        string.Equals(connectionTenantId, ConnectionScope.HostTenantId, StringComparison.Ordinal) ||
 105339        string.Equals(connectionTenantId.NormalizeTenantId(), roleTenantId, StringComparison.Ordinal);
 340
 341    /// <summary>A configuration entry that leaves the tenant blank is materialized at host scope.</summary>
 342    private static string GetConfigurationScopeTenantId(IdentityProviderConnection connection) =>
 5343        string.IsNullOrWhiteSpace(connection.TenantId) ? ConnectionScope.HostTenantId : connection.TenantId;
 344
 345    private IEnumerable<RoleDeletionDependency> GetConfigurationDependencies(IdentityProviderConnection connection, int 
 346    {
 4347        if (!TryGetRoleReference(connection.UnlinkedPolicy, roleId, out var policyBranch, out var roleIds, out var remov
 0348            yield break;
 349
 4350        var roleIndex = 0;
 18351        foreach (var configuredRoleId in ReadRoleIdsWithDuplicates(connection.UnlinkedPolicy!.Settings))
 352        {
 5353            if (string.Equals(configuredRoleId, roleId, StringComparison.Ordinal))
 354            {
 4355                yield return new(
 4356                    Source,
 4357                    string.IsNullOrWhiteSpace(connection.Id) ? $"configuration:{connectionIndex}" : connection.Id,
 4358                    connection.Key,
 4359                    policyBranch,
 4360                    RoleDeletionDependencyOwnership.Configuration,
 4361                    $"ExternalAuthentication:Connections:{connectionIndex}:UnlinkedPolicy:Settings:defaultRoleIds:{roleI
 4362                    null,
 4363                    removesLastDefaultRole);
 364            }
 365
 5366            roleIndex++;
 367        }
 4368    }
 369
 370    private static bool TryGetRoleReference(PolicySelection? policy, string roleId, out string policyBranch, out IReadOn
 371    {
 98372        policyBranch = string.Empty;
 98373        roleIds = [];
 98374        removesLastDefaultRole = false;
 98375        if (policy is null)
 0376            return false;
 377
 98378        if (string.Equals(policy.Type, CreateUserUnlinkedIdentityPolicy.PolicyType, StringComparison.Ordinal))
 97379            policyBranch = "create-user";
 1380        else if (string.Equals(policy.Type, MatchExternalUserUnlinkedIdentityPolicy.PolicyType, StringComparison.Ordinal
 1381                 string.Equals(ReadString(policy.Settings, "noMatchAction"), "create-user", StringComparison.OrdinalIgno
 1382            policyBranch = "matcher-no-match-create-user";
 383        else
 0384            return false;
 385
 98386        roleIds = CreateUserUnlinkedIdentityPolicy.ReadRoleIds(policy.Settings);
 98387        if (!roleIds.Contains(roleId, StringComparer.Ordinal))
 0388            return false;
 98389        removesLastDefaultRole = roleIds.Count == 1;
 98390        return true;
 391    }
 392
 393    private static JsonElement RemoveRole(JsonElement settings, string roleId, string? replacementRoleId = null)
 394    {
 8395        var root = settings.ValueKind == JsonValueKind.Object
 8396            ? JsonNode.Parse(settings.GetRawText()) as JsonObject
 8397            : new();
 8398        root ??= new();
 8399        var remainingRoleIds = ReadRoleIdsWithDuplicates(settings)
 9400            .Where(x => !string.Equals(x, roleId, StringComparison.Ordinal))
 8401            .Distinct(StringComparer.Ordinal)
 8402            .ToArray();
 8403        if (remainingRoleIds.Length == 0 && !string.IsNullOrWhiteSpace(replacementRoleId))
 4404            remainingRoleIds = [replacementRoleId];
 8405        var roleNodes = new JsonNode?[remainingRoleIds.Length];
 26406        for (var index = 0; index < remainingRoleIds.Length; index++)
 5407            roleNodes[index] = JsonValue.Create(remainingRoleIds[index]);
 8408        root["defaultRoleIds"] = new JsonArray(roleNodes);
 8409        return JsonSerializer.SerializeToElement(root);
 410    }
 411
 412    private static IReadOnlyCollection<string> ReadRoleIdsWithDuplicates(JsonElement settings) =>
 12413        settings.ValueKind == JsonValueKind.Object &&
 12414        settings.TryGetProperty("defaultRoleIds", out var values) &&
 12415        values.ValueKind == JsonValueKind.Array
 12416            ? values.EnumerateArray()
 14417                .Where(x => x.ValueKind == JsonValueKind.String)
 14418                .Select(x => x.GetString())
 14419                .Where(x => !string.IsNullOrWhiteSpace(x))
 12420                .Cast<string>()
 12421                .ToArray()
 12422            : [];
 423
 424    private static string? ReadString(JsonElement settings, string propertyName) =>
 1425        settings.ValueKind == JsonValueKind.Object &&
 1426        settings.TryGetProperty(propertyName, out var value) &&
 1427        value.ValueKind == JsonValueKind.String
 1428            ? value.GetString()
 1429            : null;
 430
 431    private static string CalculateVersion(IEnumerable<RoleDeletionDependency> dependencies)
 432    {
 49433        var payload = string.Join(
 49434            "\n",
 115435            dependencies.Select(x => $"{x.OwnerId}|{x.OwnerKey}|{x.PolicyBranch}|{x.Ownership}|{x.ConfigurationPath}|{x.
 49436        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payload))).ToLowerInvariant();
 437    }
 438}

Methods/Properties

.ctor(Elsa.ExternalAuthentication.Contracts.IIdentityProviderConnectionStore,Microsoft.Extensions.Options.IOptionsMonitor`1<Elsa.ExternalAuthentication.Options.ExternalAuthenticationOptions>,System.Collections.Generic.IEnumerable`1<Elsa.Identity.Contracts.IRoleAuthorizationService>,System.Collections.Generic.IEnumerable`1<Elsa.Identity.Contracts.IRoleStore>,Elsa.ExternalAuthentication.Contracts.IConnectionRegistryVersionStore,Elsa.ExternalAuthentication.Services.ConnectionRevisionCalculator,Elsa.ExternalAuthentication.Services.ExternalAuthenticationSecurityNotifier,Elsa.Authorization.IPermissionEvaluator,Elsa.Common.Multitenancy.ITenantAccessor)
get_Source()
get_ActiveRoleStore()
InspectAsync()
ValidateRemovalAsync()
RemoveEditableReferencesAsync()
ResolveRoleTenantIdAsync()
IsAgnosticRoleAsync()
FindRolesByIdAsync()
FindConnectionsInRoleTenantScopeAsync()
FindConnectionInRoleTenantScopeAsync()
IsInRoleTenantScope(System.String,System.String)
GetConfigurationScopeTenantId(Elsa.ExternalAuthentication.Models.IdentityProviderConnection)
GetConfigurationDependencies()
TryGetRoleReference(Elsa.ExternalAuthentication.Models.PolicySelection,System.String,System.String&,System.Collections.Generic.IReadOnlyCollection`1<System.String>&,System.Boolean&)
RemoveRole(System.Text.Json.JsonElement,System.String,System.String)
ReadRoleIdsWithDuplicates(System.Text.Json.JsonElement)
ReadString(System.Text.Json.JsonElement,System.String)
CalculateVersion(System.Collections.Generic.IEnumerable`1<Elsa.Identity.Models.RoleDeletionDependency>)