| | | 1 | | using Elsa.Authorization; |
| | | 2 | | |
| | | 3 | | namespace Elsa.Permissions; |
| | | 4 | | |
| | | 5 | | /// <summary>Why one submitted permission was rejected.</summary> |
| | | 6 | | public record PermissionGrantError(string Permission, string Reason); |
| | | 7 | | |
| | | 8 | | /// <summary>The outcome of validating a set of submitted permissions.</summary> |
| | 42 | 9 | | public record PermissionGrantValidationResult(IReadOnlyCollection<PermissionGrantError> Errors) |
| | | 10 | | { |
| | | 11 | | /// <summary>Whether every submitted permission was acceptable.</summary> |
| | 20 | 12 | | public bool IsValid => Errors.Count == 0; |
| | | 13 | | |
| | | 14 | | /// <summary>An accepted result.</summary> |
| | 12 | 15 | | public static PermissionGrantValidationResult Valid { get; } = new([]); |
| | | 16 | | } |
| | | 17 | | |
| | | 18 | | /// <summary>Validates permissions submitted when authoring a role, before they are persisted.</summary> |
| | | 19 | | public interface IPermissionGrantValidator |
| | | 20 | | { |
| | | 21 | | /// <summary>Validates <paramref name="permissions"/> against the catalog.</summary> |
| | | 22 | | PermissionGrantValidationResult Validate(IEnumerable<string>? permissions); |
| | | 23 | | } |
| | | 24 | | |
| | | 25 | | /// <inheritdoc /> |
| | | 26 | | /// <remarks> |
| | | 27 | | /// Concrete segments are validated against the registry; wildcard segments are validated structurally |
| | | 28 | | /// only. A wildcard is deliberately accepted **even when it currently matches nothing** — a grant naming |
| | | 29 | | /// a module that is not installed yet must survive, because installing that module later is exactly what |
| | | 30 | | /// gives the grant meaning. Validating wildcards against the catalog would reject <c>workflows/*:view</c>, |
| | | 31 | | /// which is the grant the hierarchy exists to make possible. |
| | | 32 | | /// </remarks> |
| | | 33 | | public sealed class PermissionGrantValidator(IPermissionDescriptorRegistry registry) : IPermissionGrantValidator |
| | | 34 | | { |
| | | 35 | | /// <inheritdoc /> |
| | | 36 | | public PermissionGrantValidationResult Validate(IEnumerable<string>? permissions) |
| | | 37 | | { |
| | | 38 | | if (permissions is null) |
| | | 39 | | return PermissionGrantValidationResult.Valid; |
| | | 40 | | |
| | | 41 | | var errors = new List<PermissionGrantError>(); |
| | | 42 | | |
| | | 43 | | foreach (var value in permissions.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.Ordinal)) |
| | | 44 | | { |
| | | 45 | | if (!Permission.TryParse(value, out var permission)) |
| | | 46 | | { |
| | | 47 | | errors.Add(new(value, "Not a well-formed permission. Expected '{resource}:{verb}'.")); |
| | | 48 | | continue; |
| | | 49 | | } |
| | | 50 | | |
| | | 51 | | // An entry like 'workflows*:delete' parses but the matcher never satisfies it, so it would |
| | | 52 | | // be persisted as a grant that silently reaches nothing. |
| | | 53 | | if (!permission.IsValidPattern) |
| | | 54 | | { |
| | | 55 | | errors.Add(new(value, "Places '*' where it has no meaning and would match nothing. A wildcard may only b |
| | | 56 | | continue; |
| | | 57 | | } |
| | | 58 | | |
| | | 59 | | if (permission.IsResourceWildcard || permission.IsSubtree) |
| | | 60 | | continue; |
| | | 61 | | |
| | | 62 | | var descriptor = registry.Find(permission.Resource); |
| | | 63 | | |
| | | 64 | | if (descriptor is null) |
| | | 65 | | { |
| | | 66 | | errors.Add(new(value, $"No module registers the resource '{permission.Resource}'.")); |
| | | 67 | | continue; |
| | | 68 | | } |
| | | 69 | | |
| | | 70 | | if (permission.IsVerbWildcard) |
| | | 71 | | continue; |
| | | 72 | | |
| | | 73 | | if (!descriptor.Supports(permission.Verb)) |
| | | 74 | | errors.Add(new(value, $"The resource '{permission.Resource}' does not support the verb '{permission.Verb |
| | | 75 | | } |
| | | 76 | | |
| | | 77 | | return errors.Count == 0 ? PermissionGrantValidationResult.Valid : new(errors); |
| | | 78 | | } |
| | | 79 | | } |