| | | 1 | | using System.Security.Cryptography; |
| | | 2 | | using System.Text; |
| | | 3 | | using Elsa.Extensions; |
| | | 4 | | using Elsa.Identity.Contracts; |
| | | 5 | | using Elsa.Identity.Entities; |
| | | 6 | | using JetBrains.Annotations; |
| | | 7 | | |
| | | 8 | | namespace Elsa.Identity.Services; |
| | | 9 | | |
| | | 10 | | /// <summary>Computes a stamp that changes whenever a user's effective grants change.</summary> |
| | | 11 | | public interface IPermissionStampCalculator |
| | | 12 | | { |
| | | 13 | | /// <summary>The current stamp for <paramref name="user"/>.</summary> |
| | | 14 | | ValueTask<string> ComputeAsync(User user, CancellationToken cancellationToken = default); |
| | | 15 | | } |
| | | 16 | | |
| | | 17 | | /// <inheritdoc /> |
| | | 18 | | /// <remarks> |
| | | 19 | | /// The stamp is <em>derived</em> from the user's roles and those roles' permissions rather than stored as |
| | | 20 | | /// a counter on the user. That is deliberate: a stored counter would change the Identity schema and |
| | | 21 | | /// require migrations across all five EF providers, which would make revocation tightening depend on the |
| | | 22 | | /// tenancy milestone. A derived stamp needs no schema, and every node computes the same value from the |
| | | 23 | | /// same store without any cross-node invalidation -- which matters, because Elsa has none. |
| | | 24 | | /// |
| | | 25 | | /// It changes when a role is added to or removed from the user, and when any held role's permissions |
| | | 26 | | /// change. It does not change when an unrelated role changes, so it is no broader than it needs to be. |
| | | 27 | | /// </remarks> |
| | | 28 | | [UsedImplicitly] |
| | 5 | 29 | | public class PermissionStampCalculator(IRoleProvider roleProvider) : IPermissionStampCalculator |
| | | 30 | | { |
| | | 31 | | /// <summary>The claim carrying the stamp issued with a token.</summary> |
| | | 32 | | public const string ClaimType = "elsa:permission_stamp"; |
| | | 33 | | |
| | | 34 | | /// <inheritdoc /> |
| | | 35 | | public async ValueTask<string> ComputeAsync(User user, CancellationToken cancellationToken = default) |
| | | 36 | | { |
| | 0 | 37 | | var roles = (await roleProvider.FindByIdsAsync(user.Roles, cancellationToken)).ToList(); |
| | | 38 | | |
| | 0 | 39 | | var material = string.Join( |
| | 0 | 40 | | "\n", |
| | 0 | 41 | | roles |
| | 0 | 42 | | .OrderBy(x => x.Id, StringComparer.Ordinal) |
| | 0 | 43 | | .Select(role => $"{role.Id}={string.Join(",", role.Permissions.OrderBy(x => x, StringComparer.Ordinal))} |
| | | 44 | | |
| | 0 | 45 | | var hash = SHA256.HashData(Encoding.UTF8.GetBytes(material)); |
| | | 46 | | |
| | 0 | 47 | | return Convert.ToHexString(hash)[..16]; |
| | 0 | 48 | | } |
| | | 49 | | } |