| | | 1 | | using System.Security.Claims; |
| | | 2 | | using Elsa.AI.Host.Options; |
| | | 3 | | using Elsa.Authorization; |
| | | 4 | | using Elsa.Common.Multitenancy; |
| | | 5 | | using Microsoft.AspNetCore.Http; |
| | | 6 | | using Microsoft.Extensions.DependencyInjection; |
| | | 7 | | |
| | | 8 | | namespace Elsa.AI.Host.Endpoints.AI; |
| | | 9 | | |
| | | 10 | | internal static class AIHttpContextIdentity |
| | | 11 | | { |
| | | 12 | | private const string TenantIdClaimType = "http://schemas.microsoft.com/identity/claims/tenantid"; |
| | | 13 | | |
| | | 14 | | public static string GetActorId(HttpContext? context) => |
| | 11 | 15 | | context?.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? |
| | 11 | 16 | | context?.User.FindFirstValue("sub") ?? |
| | 11 | 17 | | context?.User.Identity?.Name ?? |
| | 11 | 18 | | "anonymous"; |
| | | 19 | | |
| | | 20 | | public static string? GetTenantId(HttpContext? context) |
| | | 21 | | { |
| | 11 | 22 | | var tenantAccessor = context?.RequestServices?.GetService<ITenantAccessor>(); |
| | 11 | 23 | | if (tenantAccessor != null) |
| | 1 | 24 | | return tenantAccessor.TenantId; |
| | | 25 | | |
| | 10 | 26 | | return context?.User.FindFirstValue(TenantIdClaimType) ?? |
| | 10 | 27 | | context?.User.FindFirstValue("tenant_id") ?? |
| | 10 | 28 | | context?.User.FindFirstValue("tenantId"); |
| | | 29 | | } |
| | | 30 | | |
| | | 31 | | public static ICollection<string> GetPermissions(HttpContext? context) => |
| | 11 | 32 | | context?.User |
| | 11 | 33 | | .FindAll(PermissionNames.ClaimType) |
| | 1 | 34 | | .Select(x => x.Value) |
| | 1 | 35 | | .Where(x => !string.IsNullOrWhiteSpace(x)) |
| | 11 | 36 | | .Distinct(StringComparer.OrdinalIgnoreCase) |
| | 11 | 37 | | .ToList() ?? []; |
| | | 38 | | |
| | | 39 | | public static string? GetAuthorizedAgent(string? requestedAgent, AIHostOptions options, ClaimsPrincipal? user) |
| | | 40 | | { |
| | 11 | 41 | | if (string.IsNullOrWhiteSpace(requestedAgent)) |
| | 8 | 42 | | return null; |
| | | 43 | | |
| | 6 | 44 | | var agent = options.Agents.FirstOrDefault(x => string.Equals(x.Name, requestedAgent, StringComparison.OrdinalIgn |
| | 3 | 45 | | if (agent == null || !HasRequiredPermissions(agent.Permissions, user)) |
| | 1 | 46 | | return null; |
| | | 47 | | |
| | 2 | 48 | | return agent.Name; |
| | | 49 | | } |
| | | 50 | | |
| | | 51 | | // Routed through the shared evaluator so a wildcard grant such as ai/*:execute reaches an agent's declared |
| | | 52 | | // permissions, and so the comparison is ordinal like every other site in the model. The previous |
| | | 53 | | // case-insensitive exact-set containment did neither: it admitted casing the rest of the model rejects, |
| | | 54 | | // while refusing the wildcards the rest of the model honours. |
| | | 55 | | private static bool HasRequiredPermissions(ICollection<string> requiredPermissions, ClaimsPrincipal? user) |
| | | 56 | | { |
| | 2 | 57 | | if (requiredPermissions.Count == 0) |
| | 2 | 58 | | return true; |
| | 0 | 59 | | if (user is null) |
| | 0 | 60 | | return false; |
| | | 61 | | |
| | 0 | 62 | | return requiredPermissions.All(x => Permission.TryParse(x, out var required) && PermissionEvaluator.Shared.HasPe |
| | | 63 | | } |
| | | 64 | | } |