| | | 1 | | using System.Text.Json; |
| | | 2 | | using Elsa.ExternalAuthentication.Contracts; |
| | | 3 | | using Elsa.ExternalAuthentication.Models; |
| | | 4 | | |
| | | 5 | | namespace Elsa.ExternalAuthentication.Policies; |
| | | 6 | | |
| | | 7 | | /// <summary>Delegates external-to-Elsa identity matching to one trusted matcher and applies a safe configured no-match |
| | 0 | 8 | | public sealed class MatchExternalUserUnlinkedIdentityPolicy(IExternalUserMatcherRegistry matchers) : IUnlinkedIdentityPo |
| | | 9 | | { |
| | | 10 | | public const string PolicyType = "match-user"; |
| | 0 | 11 | | public string Type => PolicyType; |
| | | 12 | | |
| | 0 | 13 | | public UnlinkedIdentityPolicyDescriptor Describe() => new( |
| | 0 | 14 | | Type, |
| | 0 | 15 | | "Match an existing user", |
| | 0 | 16 | | "Uses one deployment-installed user matcher, then rejects or creates a user when no match is returned.", |
| | 0 | 17 | | 1, |
| | 0 | 18 | | [ |
| | 0 | 19 | | new("matcher", "User matcher", "Versioned matcher selection and settings.", "json", true, "json", null, [], |
| | 0 | 20 | | new("noMatchAction", "No-match action", "Reject access or create a credential-less user.", "string", true, " |
| | 0 | 21 | | new("defaultRoleIds", "Default roles", "Role IDs applied only when the no-match action creates a user.", "st |
| | 0 | 22 | | ], |
| | 0 | 23 | | null); |
| | | 24 | | |
| | | 25 | | public async ValueTask<UnlinkedIdentityDecision> EvaluateAsync(UnlinkedIdentityContext context, CancellationToken ca |
| | | 26 | | { |
| | 0 | 27 | | if (!TryReadMatcher(context.Settings, out var selection) || !matchers.TryGet(selection.Type, out var matcher)) |
| | 0 | 28 | | return new UnlinkedIdentityDecision.Reject("identity_unlinked"); |
| | 0 | 29 | | var descriptor = matchers.ListDescriptors().FirstOrDefault(x => string.Equals(x.Type, selection.Type, StringComp |
| | 0 | 30 | | if (descriptor is null || descriptor.SettingsVersion != selection.SettingsVersion) |
| | 0 | 31 | | return new UnlinkedIdentityDecision.Reject("identity_unlinked"); |
| | | 32 | | |
| | 0 | 33 | | var requiredClaims = (descriptor.RequiredClaimTypes ?? []).ToHashSet(StringComparer.Ordinal); |
| | 0 | 34 | | var claims = context.ProjectedClaims.Where(x => requiredClaims.Contains(x.Key)).ToDictionary(x => x.Key, x => x. |
| | 0 | 35 | | var match = await matcher.MatchAsync(new ExternalUserMatcherContext(context.TargetTenantId, context.Connection, |
| | 0 | 36 | | if (match is ExternalUserMatchResult.Match { UserId: { Length: > 0 } userId, AuthorizationBasis: { Length: > 0 } |
| | 0 | 37 | | return new UnlinkedIdentityDecision.LinkExistingUser(userId, basis); |
| | | 38 | | |
| | 0 | 39 | | if (match is not ExternalUserMatchResult.NoMatch) |
| | 0 | 40 | | return new UnlinkedIdentityDecision.Reject("identity_unlinked"); |
| | | 41 | | |
| | 0 | 42 | | return string.Equals(ReadString(context.Settings, "noMatchAction"), "create-user", StringComparison.OrdinalIgnor |
| | 0 | 43 | | ? new UnlinkedIdentityDecision.CreateUser(new UserCreationProposal("external", DefaultRoleIds: CreateUserUnl |
| | 0 | 44 | | : new UnlinkedIdentityDecision.Reject("identity_unlinked"); |
| | 0 | 45 | | } |
| | | 46 | | |
| | | 47 | | private static bool TryReadMatcher(JsonElement settings, out MatcherSelection selection) |
| | | 48 | | { |
| | 0 | 49 | | selection = default!; |
| | 0 | 50 | | if (settings.ValueKind != JsonValueKind.Object || !settings.TryGetProperty("matcher", out var value) || value.Va |
| | 0 | 51 | | return false; |
| | 0 | 52 | | var type = ReadString(value, "type"); |
| | 0 | 53 | | var version = value.TryGetProperty("settingsVersion", out var versionValue) && versionValue.TryGetInt32(out var |
| | 0 | 54 | | var matcherSettings = value.TryGetProperty("settings", out var matcherSettingsValue) ? matcherSettingsValue.Clon |
| | 0 | 55 | | if (string.IsNullOrWhiteSpace(type) || version <= 0) |
| | 0 | 56 | | return false; |
| | 0 | 57 | | selection = new MatcherSelection(type, version, matcherSettings); |
| | 0 | 58 | | return true; |
| | | 59 | | } |
| | | 60 | | |
| | 0 | 61 | | private static string? ReadString(JsonElement value, string name) => value.ValueKind == JsonValueKind.Object && valu |
| | 0 | 62 | | private sealed record MatcherSelection(string Type, int SettingsVersion, JsonElement Settings); |
| | | 63 | | } |