Merge remote-tracking branch 'origin/main' into codex/security-health-checks
This commit is contained in:
commit
e362fc057d
|
|
@ -0,0 +1,12 @@
|
|||
namespace Elsa.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Options for the localhost permission requirement.
|
||||
/// </summary>
|
||||
public class LocalHostPermissionRequirementOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets whether localhost requests may satisfy the security-root permission requirement without other credentials.
|
||||
/// </summary>
|
||||
public bool EnableLocalHostPermissionGrant { get; set; }
|
||||
}
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
using System.Security.Claims;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Options;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Requirements;
|
||||
|
||||
/// <summary>
|
||||
/// Add the "create:application" permission to the current user if the request is local.
|
||||
/// Adds security-root bootstrap permissions to the current user when explicit localhost permission grants are enabled and the request is local.
|
||||
/// </summary>
|
||||
public class LocalHostPermissionRequirement : IAuthorizationRequirement
|
||||
{
|
||||
|
|
@ -18,32 +20,58 @@ public class LocalHostPermissionRequirement : IAuthorizationRequirement
|
|||
[PublicAPI]
|
||||
public class LocalHostPermissionRequirementHandler : AuthorizationHandler<LocalHostPermissionRequirement>
|
||||
{
|
||||
private static readonly string[] BootstrapPermissions =
|
||||
[
|
||||
"create:application",
|
||||
"create:user",
|
||||
"create:role"
|
||||
];
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IOptions<LocalHostPermissionRequirementOptions> _options;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LocalHostPermissionRequirementHandler(IHttpContextAccessor httpContextAccessor)
|
||||
public LocalHostPermissionRequirementHandler(IHttpContextAccessor httpContextAccessor) : this(
|
||||
httpContextAccessor,
|
||||
Microsoft.Extensions.Options.Options.Create(new LocalHostPermissionRequirementOptions()))
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LocalHostPermissionRequirementHandler(IHttpContextAccessor httpContextAccessor, IOptions<LocalHostPermissionRequirementOptions> options)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalHostPermissionRequirement requirement)
|
||||
{
|
||||
if (_httpContextAccessor.HttpContext?.Request.IsLocal() == false)
|
||||
if (!_options.Value.EnableLocalHostPermissionGrant)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var currentIdentity = context.User.Identity;
|
||||
|
||||
if (currentIdentity?.IsAuthenticated == false)
|
||||
if (_httpContextAccessor.HttpContext?.Request.IsLocal() != true)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (context.User.Identities.Any(x => x.IsAuthenticated))
|
||||
{
|
||||
var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
|
||||
identity.AddClaim(new Claim("permissions", "create:application"));
|
||||
identity.AddClaim(new Claim("permissions", "create:user"));
|
||||
identity.AddClaim(new Claim("permissions", "create:role"));
|
||||
context.User.AddIdentity(identity);
|
||||
if (HasBootstrapPermissions(context.User))
|
||||
context.Succeed(requirement);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
|
||||
identity.AddClaims(BootstrapPermissions.Select(permission => new Claim(PermissionNames.ClaimType, permission)));
|
||||
context.User.AddIdentity(identity);
|
||||
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasBootstrapPermissions(ClaimsPrincipal user)
|
||||
{
|
||||
var permissions = user.FindAll(PermissionNames.ClaimType).Select(x => x.Value).ToHashSet(StringComparer.Ordinal);
|
||||
return permissions.Contains(PermissionNames.All) || BootstrapPermissions.All(permissions.Contains);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Elsa.Features.Services;
|
|||
using Elsa.Identity.Constants;
|
||||
using Elsa.Identity.Options;
|
||||
using Elsa.Identity.Providers;
|
||||
using Elsa.Options;
|
||||
using Elsa.Requirements;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
|
|
@ -22,17 +23,32 @@ public class DefaultAuthenticationFeature : FeatureBase
|
|||
{
|
||||
private const string MultiScheme = "Jwt-or-ApiKey";
|
||||
private Func<AuthenticationBuilder, AuthenticationBuilder> _configureApiKeyAuthorization = builder => builder.AddApiKeyInAuthorizationHeader<DefaultApiKeyProvider>();
|
||||
private Action<AuthorizationOptions>? _configureAuthorizationOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DefaultAuthenticationFeature(IModule module) : base(module)
|
||||
{
|
||||
ConfigureAuthorizationOptions = ConfigureDefaultSecurityRootPolicy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="ApiKeyProviderType"/>.
|
||||
/// </summary>
|
||||
public Type ApiKeyProviderType { get; set; } = typeof(DefaultApiKeyProvider);
|
||||
public Action<AuthorizationOptions> ConfigureAuthorizationOptions { get; set; } = options => options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.AddRequirements(new LocalHostPermissionRequirement()));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authorization options configuration.
|
||||
/// </summary>
|
||||
public Action<AuthorizationOptions> ConfigureAuthorizationOptions
|
||||
{
|
||||
get => _configureAuthorizationOptions ?? ConfigureDefaultSecurityRootPolicy;
|
||||
set => _configureAuthorizationOptions = value ?? ConfigureDefaultSecurityRootPolicy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether localhost requests may satisfy the security-root permission requirement without other credentials.
|
||||
/// </summary>
|
||||
public bool EnableLocalHostPermissionGrant { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configures the API key provider type.
|
||||
|
|
@ -79,23 +95,40 @@ public class DefaultAuthenticationFeature : FeatureBase
|
|||
/// </summary>
|
||||
/// <returns>The current <see cref="DefaultAuthenticationFeature"/>.</returns>
|
||||
public DefaultAuthenticationFeature UseDevelopmentAdminApiKey() => UseAdminApiKey(AdminApiKeyProvider.DevelopmentApiKey);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Disables the local host requirement for the security root policy.
|
||||
/// This is useful when privileged identity bootstrap is handled through features such as <see cref="DefaultAdminUserFeature"/>.
|
||||
/// Enables the legacy localhost permission grant for the security root policy.
|
||||
/// </summary>
|
||||
public DefaultAuthenticationFeature DisableLocalHostRequirement()
|
||||
public DefaultAuthenticationFeature EnableLocalHostPermissionGrantForSecurityRoot()
|
||||
{
|
||||
ConfigureAuthorizationOptions = options => options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.RequireAuthenticatedUser());
|
||||
EnableLocalHostPermissionGrant = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables the localhost permission grant for the security root policy.
|
||||
/// This is useful when privileged identity bootstrap is handled through features such as <see cref="DefaultAdminUserFeature"/>.
|
||||
/// </summary>
|
||||
public DefaultAuthenticationFeature DisableLocalHostPermissionGrantForSecurityRoot()
|
||||
{
|
||||
EnableLocalHostPermissionGrant = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables the legacy localhost permission grant for the security root policy.
|
||||
/// This is useful when privileged identity bootstrap is handled through features such as <see cref="DefaultAdminUserFeature"/>.
|
||||
/// </summary>
|
||||
[Obsolete("Use DisableLocalHostPermissionGrantForSecurityRoot instead.")]
|
||||
public DefaultAuthenticationFeature DisableLocalHostRequirement() => DisableLocalHostPermissionGrantForSecurityRoot();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
Services.ConfigureOptions<ConfigureJwtBearerOptions>();
|
||||
Services.Configure<AdminApiKeyOptions>(_ => { });
|
||||
Services.AddIdentityTokenOptionsValidation();
|
||||
Services.Configure<LocalHostPermissionRequirementOptions>(options => options.EnableLocalHostPermissionGrant = EnableLocalHostPermissionGrant);
|
||||
|
||||
var authBuilder = Services
|
||||
.AddAuthentication(MultiScheme)
|
||||
|
|
@ -119,4 +152,22 @@ public class DefaultAuthenticationFeature : FeatureBase
|
|||
Services.AddScoped<IApiKeyProvider>(sp => (IApiKeyProvider)sp.GetRequiredService(ApiKeyProviderType));
|
||||
Services.AddAuthorization(ConfigureAuthorizationOptions);
|
||||
}
|
||||
|
||||
private static void ConfigureAuthenticatedSecurityRootPolicy(AuthorizationOptions options)
|
||||
{
|
||||
options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.RequireAuthenticatedUser());
|
||||
}
|
||||
|
||||
private void ConfigureDefaultSecurityRootPolicy(AuthorizationOptions options)
|
||||
{
|
||||
if (EnableLocalHostPermissionGrant)
|
||||
ConfigureLocalHostSecurityRootPolicy(options);
|
||||
else
|
||||
ConfigureAuthenticatedSecurityRootPolicy(options);
|
||||
}
|
||||
|
||||
private static void ConfigureLocalHostSecurityRootPolicy(AuthorizationOptions options)
|
||||
{
|
||||
options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.AddRequirements(new LocalHostPermissionRequirement()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,3 +83,4 @@ identity.UseDefaultAdmin("admin", "REPLACE_WITH_SECURE_BOOTSTRAP_PASSWORD", "adm
|
|||
- Do not keep development defaults in production.
|
||||
- Prefer environment variables or a secret manager for admin credentials.
|
||||
- After first bootstrap, rotate credentials according to your security policy.
|
||||
- Localhost requests no longer satisfy `SecurityRoot` by default. Legacy localhost bootstrap requires an explicit opt-in: call `EnableLocalHostPermissionGrantForSecurityRoot()` in code-first configuration or set `EnableLocalHostPermissionGrant` on the shell `DefaultAuthentication` feature; prefer `DefaultAdminUser` instead.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Elsa.Extensions;
|
|||
using Elsa.Identity.Constants;
|
||||
using Elsa.Identity.Options;
|
||||
using Elsa.Identity.Providers;
|
||||
using Elsa.Options;
|
||||
using Elsa.PackageManifest.Generator.Hints;
|
||||
using Elsa.Requirements;
|
||||
using JetBrains.Annotations;
|
||||
|
|
@ -53,6 +54,11 @@ public class DefaultAuthenticationFeature : IShellFeature
|
|||
RestartRequired = true)]
|
||||
public bool UseDevelopmentAdminApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether localhost requests may satisfy the security-root permission requirement without other credentials.
|
||||
/// </summary>
|
||||
public bool EnableLocalHostPermissionGrant { get; set; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
var resolvedAdminApiKey = UseDevelopmentAdminApiKey ? AdminApiKeyProvider.DevelopmentApiKey : AdminApiKey;
|
||||
|
|
@ -61,6 +67,7 @@ public class DefaultAuthenticationFeature : IShellFeature
|
|||
|
||||
services.ConfigureOptions<ConfigureJwtBearerOptions>();
|
||||
services.AddIdentityTokenOptionsValidation();
|
||||
services.Configure<LocalHostPermissionRequirementOptions>(options => options.EnableLocalHostPermissionGrant = EnableLocalHostPermissionGrant);
|
||||
services.Configure<AdminApiKeyOptions>(options =>
|
||||
{
|
||||
options.ApiKey = resolvedAdminApiKey;
|
||||
|
|
@ -93,7 +100,10 @@ public class DefaultAuthenticationFeature : IShellFeature
|
|||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.RequireAuthenticatedUser());
|
||||
if (EnableLocalHostPermissionGrant)
|
||||
options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.AddRequirements(new LocalHostPermissionRequirement()));
|
||||
else
|
||||
options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.RequireAuthenticatedUser());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using Elsa.Abstractions;
|
||||
using Elsa.Workflows.Api.Constants;
|
||||
using Elsa.Workflows.Api.Requirements;
|
||||
using Elsa.Workflows.Api.Security;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
|
|
@ -15,6 +13,7 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Import;
|
|||
[PublicAPI]
|
||||
internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
|
||||
{
|
||||
private readonly IWorkflowDefinitionStore _workflowDefinitionStore;
|
||||
private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
|
||||
private readonly IWorkflowDefinitionLinker _linker;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
|
@ -22,11 +21,13 @@ internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
|
||||
/// <inheritdoc />
|
||||
public Import(
|
||||
IWorkflowDefinitionStore workflowDefinitionStore,
|
||||
IWorkflowDefinitionImporter workflowDefinitionImporter,
|
||||
IWorkflowDefinitionLinker linker,
|
||||
IAuthorizationService authorizationService,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService)
|
||||
{
|
||||
_workflowDefinitionStore = workflowDefinitionStore;
|
||||
_workflowDefinitionImporter = workflowDefinitionImporter;
|
||||
_linker = linker;
|
||||
_authorizationService = authorizationService;
|
||||
|
|
@ -54,10 +55,7 @@ internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
return;
|
||||
}
|
||||
|
||||
var result = await ImportSingleWorkflowDefinitionAsync(model, cancellationToken);
|
||||
var definition = result.WorkflowDefinition;
|
||||
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
|
||||
var authorizationResult = await _authorizationService.AuthorizeWorkflowDefinitionImportAsync(User, _workflowDefinitionStore, model, cancellationToken);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
|
|
@ -65,6 +63,8 @@ internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
return;
|
||||
}
|
||||
|
||||
var result = await ImportSingleWorkflowDefinitionAsync(model, cancellationToken);
|
||||
var definition = result.WorkflowDefinition;
|
||||
var updatedModel = await _linker.MapAsync(definition, cancellationToken);
|
||||
|
||||
if (result.Succeeded)
|
||||
|
|
@ -98,5 +98,4 @@ internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
using System.Security.Claims;
|
||||
using Elsa.Common.Models;
|
||||
using Elsa.Workflows.Api.Constants;
|
||||
using Elsa.Workflows.Api.Requirements;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.Management.Filters;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions;
|
||||
|
||||
internal static class ImportAuthorizationExtensions
|
||||
{
|
||||
public static async Task<AuthorizationResult> AuthorizeWorkflowDefinitionImportAsync(
|
||||
this IAuthorizationService authorizationService,
|
||||
ClaimsPrincipal user,
|
||||
IWorkflowDefinitionStore workflowDefinitionStore,
|
||||
WorkflowDefinitionModel model,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var definition = await FindExistingDefinitionAsync(workflowDefinitionStore, model.DefinitionId, cancellationToken);
|
||||
return await authorizationService.AuthorizeAsync(user, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
|
||||
}
|
||||
|
||||
public static async Task<AuthorizationResult> AuthorizeWorkflowDefinitionImportsAsync(
|
||||
this IAuthorizationService authorizationService,
|
||||
ClaimsPrincipal user,
|
||||
IWorkflowDefinitionStore workflowDefinitionStore,
|
||||
IEnumerable<WorkflowDefinitionModel> models,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var modelList = models.ToList();
|
||||
|
||||
if (modelList.Count == 0)
|
||||
return await authorizationService.AuthorizeAsync(user, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
|
||||
|
||||
var definitions = await FindExistingDefinitionsAsync(workflowDefinitionStore, modelList, cancellationToken);
|
||||
|
||||
foreach (var model in modelList)
|
||||
{
|
||||
definitions.TryGetValue(model.DefinitionId ?? string.Empty, out var definition);
|
||||
var authorizationResult = await authorizationService.AuthorizeAsync(user, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
return authorizationResult;
|
||||
}
|
||||
|
||||
return AuthorizationResult.Success();
|
||||
}
|
||||
|
||||
private static async Task<WorkflowDefinition?> FindExistingDefinitionAsync(
|
||||
IWorkflowDefinitionStore workflowDefinitionStore,
|
||||
string? definitionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(definitionId))
|
||||
return null;
|
||||
|
||||
return await workflowDefinitionStore.FindAsync(new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionId = definitionId,
|
||||
VersionOptions = VersionOptions.Latest
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<IDictionary<string, WorkflowDefinition>> FindExistingDefinitionsAsync(
|
||||
IWorkflowDefinitionStore workflowDefinitionStore,
|
||||
IEnumerable<WorkflowDefinitionModel> models,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var definitionIds = models
|
||||
.Select(x => x.DefinitionId)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(x => x!)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
if (definitionIds.Count == 0)
|
||||
return new Dictionary<string, WorkflowDefinition>();
|
||||
|
||||
var definitions = await workflowDefinitionStore.FindManyAsync(new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionIds = definitionIds,
|
||||
VersionOptions = VersionOptions.Latest
|
||||
}, cancellationToken);
|
||||
|
||||
return definitions
|
||||
.GroupBy(x => x.DefinitionId, StringComparer.Ordinal)
|
||||
.ToDictionary(x => x.Key, x => x.First(), StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
using Elsa.Abstractions;
|
||||
using Elsa.Workflows.Api.Constants;
|
||||
using Elsa.Workflows.Api.Requirements;
|
||||
using Elsa.Workflows.Api.Security;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Mappers;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
|
@ -17,25 +14,22 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.ImportFiles;
|
|||
[PublicAPI]
|
||||
internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
|
||||
{
|
||||
private readonly IWorkflowDefinitionService _workflowDefinitionService;
|
||||
private readonly IWorkflowDefinitionStore _workflowDefinitionStore;
|
||||
private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
|
||||
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
|
||||
private readonly IApiSerializer _apiSerializer;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly PythonWorkflowDefinitionAuthorizationService _pythonAuthorizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImportFiles(
|
||||
IWorkflowDefinitionService workflowDefinitionService,
|
||||
IWorkflowDefinitionStore workflowDefinitionStore,
|
||||
IWorkflowDefinitionImporter workflowDefinitionImporter,
|
||||
WorkflowDefinitionMapper workflowDefinitionMapper,
|
||||
IApiSerializer apiSerializer,
|
||||
IAuthorizationService authorizationService,
|
||||
PythonWorkflowDefinitionAuthorizationService pythonAuthorizationService)
|
||||
{
|
||||
_workflowDefinitionService = workflowDefinitionService;
|
||||
_workflowDefinitionStore = workflowDefinitionStore;
|
||||
_workflowDefinitionImporter = workflowDefinitionImporter;
|
||||
_workflowDefinitionMapper = workflowDefinitionMapper;
|
||||
_apiSerializer = apiSerializer;
|
||||
_authorizationService = authorizationService;
|
||||
_pythonAuthorizationService = pythonAuthorizationService;
|
||||
|
|
@ -52,17 +46,21 @@ internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
/// <inheritdoc />
|
||||
public override async Task HandleAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
await Send.ForbiddenAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Files.Any())
|
||||
{
|
||||
var count = await ImportFilesAsync(Files, cancellationToken);
|
||||
var models = await ReadWorkflowDefinitionModelsAsync(Files, cancellationToken);
|
||||
if (!await AuthorizePythonUsageAsync(models, cancellationToken))
|
||||
return;
|
||||
|
||||
var authorizationResult = await _authorizationService.AuthorizeWorkflowDefinitionImportsAsync(User, _workflowDefinitionStore, models, cancellationToken);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
await Send.ForbiddenAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var count = await ImportWorkflowDefinitionsAsync(models, cancellationToken);
|
||||
|
||||
if (!ValidationFailed && !HttpContext.Response.HasStarted)
|
||||
await Send.OkAsync(new { Count = count }, cancellationToken);
|
||||
|
|
@ -72,28 +70,39 @@ internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
|
|||
await Send.ErrorsAsync(400, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<int> ImportFilesAsync(IFormFileCollection files, CancellationToken cancellationToken)
|
||||
private async Task<ICollection<WorkflowDefinitionModel>> ReadWorkflowDefinitionModelsAsync(IFormFileCollection files, CancellationToken cancellationToken)
|
||||
{
|
||||
var models = await WorkflowDefinitionImportFileReader.ReadAsync(files, _apiSerializer, () => HttpContext.Response.HasStarted, cancellationToken);
|
||||
return models.ToList();
|
||||
}
|
||||
|
||||
private async Task<bool> AuthorizePythonUsageAsync(IEnumerable<WorkflowDefinitionModel> models, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var model in models)
|
||||
{
|
||||
var pythonAuthorizationResult = await _pythonAuthorizationService.AuthorizeAsync(model, User, cancellationToken);
|
||||
if (pythonAuthorizationResult != PythonWorkflowDefinitionAuthorizationResult.Allowed)
|
||||
{
|
||||
await PythonWorkflowDefinitionAuthorizationFailure.SendAsync(pythonAuthorizationResult, Send.ForbiddenAsync, message => AddError(message), Send.ErrorsAsync, cancellationToken);
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<int> ImportWorkflowDefinitionsAsync(IEnumerable<WorkflowDefinitionModel> models, CancellationToken cancellationToken)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var model in models)
|
||||
{
|
||||
if (HttpContext.Response.HasStarted)
|
||||
return count;
|
||||
|
||||
await ImportSingleWorkflowDefinitionAsync(model, cancellationToken);
|
||||
count++;
|
||||
var result = await ImportSingleWorkflowDefinitionAsync(model, cancellationToken);
|
||||
|
||||
if (result.Succeeded)
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics;
|
||||
using Elsa.Common.Models;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Testing.Shared.Models;
|
||||
|
|
@ -7,6 +8,8 @@ using Elsa.Workflows.ComponentTests.Abstractions;
|
|||
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||
using Elsa.Workflows.ComponentTests.Scenarios.Activities.Composition.BulkDispatchWorkflows.Workflows;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.Management.Filters;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.State;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -16,11 +19,17 @@ namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Composition.BulkDis
|
|||
public class BulkDispatchWorkflowsTests : AppComponentTest
|
||||
{
|
||||
private const int ChildWorkflowTimeoutSeconds = 30;
|
||||
private const int InitialChildWorkflowPollingIntervalMilliseconds = 250;
|
||||
private const int MaxChildWorkflowPollingIntervalMilliseconds = 1000;
|
||||
// The runtime persists this marker under the activity property name; resume handlers read the same key from WorkflowState.Properties.
|
||||
private const string WaitForCompletionPropertyName = nameof(Elsa.Workflows.Runtime.Activities.BulkDispatchWorkflows.WaitForCompletion);
|
||||
private readonly AsyncWorkflowRunner _workflowRunner;
|
||||
private readonly IWorkflowInstanceStore _workflowInstanceStore;
|
||||
|
||||
public BulkDispatchWorkflowsTests(App app) : base(app)
|
||||
{
|
||||
_workflowRunner = Scope.ServiceProvider.GetRequiredService<AsyncWorkflowRunner>();
|
||||
_workflowInstanceStore = Scope.ServiceProvider.GetRequiredService<IWorkflowInstanceStore>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "BulkDispatchWorkflows should wait for all child workflows to complete")]
|
||||
|
|
@ -37,21 +46,19 @@ public class BulkDispatchWorkflowsTests : AppComponentTest
|
|||
{
|
||||
var expectedChildCount = 3;
|
||||
|
||||
// Run the main workflow and wait for child workflows to complete
|
||||
var (result, completedChildWorkflows) = await RunWorkflowAndWaitForChildWorkflowsAsync(
|
||||
BulkDispatchFireAndForgetWorkflow.DefinitionId,
|
||||
var result = await RunWorkflowAsync(BulkDispatchFireAndForgetWorkflow.DefinitionId);
|
||||
|
||||
AssertWorkflowFinished(result);
|
||||
var childWorkflowInstances = await WaitForChildWorkflowInstancesAsync(
|
||||
result.WorkflowExecutionContext.Id,
|
||||
SlowBulkChildWorkflow.DefinitionId,
|
||||
expectedChildCount);
|
||||
|
||||
AssertWorkflowFinished(result);
|
||||
var mainWorkflowCompletedAt = result.WorkflowExecutionContext.UpdatedAt;
|
||||
|
||||
// Assert that all child workflows completed after the main workflow
|
||||
Assert.Equal(expectedChildCount, completedChildWorkflows.Count);
|
||||
foreach (var childContext in completedChildWorkflows)
|
||||
Assert.Equal(expectedChildCount, childWorkflowInstances.Count);
|
||||
foreach (var childWorkflowInstance in childWorkflowInstances)
|
||||
{
|
||||
Assert.True(childContext.UpdatedAt > mainWorkflowCompletedAt,
|
||||
$"Child workflow should complete after main workflow. Main: {mainWorkflowCompletedAt}, Child: {childContext.UpdatedAt}");
|
||||
Assert.Equal(result.WorkflowExecutionContext.Id, childWorkflowInstance.ParentWorkflowInstanceId);
|
||||
Assert.False(childWorkflowInstance.WorkflowState.Properties.ContainsKey(WaitForCompletionPropertyName));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,4 +183,36 @@ public class BulkDispatchWorkflowsTests : AppComponentTest
|
|||
workflowEvents.WorkflowStateCommitted -= OnWorkflowStateCommitted;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<WorkflowInstance>> WaitForChildWorkflowInstancesAsync(
|
||||
string parentWorkflowInstanceId,
|
||||
string childWorkflowDefinitionId,
|
||||
int expectedChildCount,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var timeout = TimeSpan.FromSeconds(ChildWorkflowTimeoutSeconds);
|
||||
var pollingInterval = TimeSpan.FromMilliseconds(InitialChildWorkflowPollingIntervalMilliseconds);
|
||||
var maxPollingInterval = TimeSpan.FromMilliseconds(MaxChildWorkflowPollingIntervalMilliseconds);
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var filter = new WorkflowInstanceFilter
|
||||
{
|
||||
DefinitionId = childWorkflowDefinitionId,
|
||||
ParentWorkflowInstanceIds = [parentWorkflowInstanceId]
|
||||
};
|
||||
var actualChildCount = 0;
|
||||
|
||||
while (stopwatch.Elapsed < timeout)
|
||||
{
|
||||
var instances = (await _workflowInstanceStore.FindManyAsync(filter, cancellationToken)).ToList();
|
||||
actualChildCount = instances.Count;
|
||||
|
||||
if (instances.Count >= expectedChildCount)
|
||||
return instances;
|
||||
|
||||
await Task.Delay(pollingInterval, cancellationToken);
|
||||
pollingInterval = TimeSpan.FromMilliseconds(Math.Min(pollingInterval.TotalMilliseconds * 2, maxPollingInterval.TotalMilliseconds));
|
||||
}
|
||||
|
||||
throw new TimeoutException($"Expected {expectedChildCount} child workflow instances of definition '{childWorkflowDefinitionId}' for parent '{parentWorkflowInstanceId}', but found {actualChildCount}.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
|
||||
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.ComponentTests.Abstractions;
|
||||
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Filters;
|
||||
using Elsa.Workflows.Management.Materializers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Refit;
|
||||
using WorkflowDefinitionEntity = Elsa.Workflows.Management.Entities.WorkflowDefinition;
|
||||
|
||||
namespace Elsa.Workflows.ComponentTests.Scenarios.RestApis.Endpoints.WorkflowDefinitions.Import;
|
||||
|
||||
public class ImportAuthorizationTests : AppComponentTest
|
||||
{
|
||||
private readonly IWorkflowDefinitionStore _store;
|
||||
private readonly IActivitySerializer _activitySerializer;
|
||||
private readonly IWorkflowDefinitionsApi _client;
|
||||
|
||||
public ImportAuthorizationTests(App app) : base(app)
|
||||
{
|
||||
_store = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionStore>();
|
||||
_activitySerializer = Scope.ServiceProvider.GetRequiredService<IActivitySerializer>();
|
||||
_client = WorkflowServer.CreateApiClient<IWorkflowDefinitionsApi>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportExistingReadOnlyDefinition_ShouldReturnForbiddenAndLeaveStorageUnchanged()
|
||||
{
|
||||
var definitionId = $"readonly-import-{Guid.NewGuid():N}";
|
||||
await SaveDefinitionAsync(definitionId, "Original", isReadonly: true);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<ApiException>(() => _client.ImportAsync(CreateImportModel(definitionId, "Updated")));
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, exception.StatusCode);
|
||||
await AssertDefinitionUnchangedAsync(definitionId, "Original", isReadonly: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportFilesWithReadOnlyTarget_ShouldReturnForbiddenAndLeaveStorageUnchanged()
|
||||
{
|
||||
var writableDefinitionId = $"writable-import-files-{Guid.NewGuid():N}";
|
||||
var readOnlyDefinitionId = $"readonly-import-files-{Guid.NewGuid():N}";
|
||||
await SaveDefinitionAsync(writableDefinitionId, "Writable Original");
|
||||
await SaveDefinitionAsync(readOnlyDefinitionId, "ReadOnly Original", isReadonly: true);
|
||||
|
||||
await using var writableStream = CreateImportStream(writableDefinitionId, "Writable Updated");
|
||||
await using var readOnlyStream = CreateImportStream(readOnlyDefinitionId, "ReadOnly Updated");
|
||||
var files = new List<StreamPart>
|
||||
{
|
||||
new(writableStream, "writable.json", "application/json"),
|
||||
new(readOnlyStream, "readonly.json", "application/json")
|
||||
};
|
||||
|
||||
var exception = await Assert.ThrowsAsync<ApiException>(() => _client.ImportFilesAsync(files));
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, exception.StatusCode);
|
||||
await AssertDefinitionUnchangedAsync(writableDefinitionId, "Writable Original");
|
||||
await AssertDefinitionUnchangedAsync(readOnlyDefinitionId, "ReadOnly Original", isReadonly: true);
|
||||
}
|
||||
|
||||
private async Task SaveDefinitionAsync(string definitionId, string name, bool isReadonly = false)
|
||||
{
|
||||
await _store.SaveAsync(new WorkflowDefinitionEntity
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
DefinitionId = definitionId,
|
||||
Name = name,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
IsLatest = true,
|
||||
IsReadonly = isReadonly,
|
||||
MaterializerName = JsonWorkflowMaterializer.MaterializerName,
|
||||
StringData = _activitySerializer.Serialize(new Sequence())
|
||||
});
|
||||
}
|
||||
|
||||
private async Task AssertDefinitionUnchangedAsync(string definitionId, string expectedName, bool isReadonly = false)
|
||||
{
|
||||
var definitions = (await _store.FindManyAsync(new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionId = definitionId
|
||||
})).ToList();
|
||||
|
||||
var definition = Assert.Single(definitions);
|
||||
Assert.Equal(expectedName, definition.Name);
|
||||
Assert.Equal(isReadonly, definition.IsReadonly);
|
||||
}
|
||||
|
||||
private static WorkflowDefinitionModel CreateImportModel(string definitionId, string name)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
DefinitionId = definitionId,
|
||||
Name = name
|
||||
};
|
||||
}
|
||||
|
||||
private static MemoryStream CreateImportStream(string definitionId, string name)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(CreateImportModel(definitionId, name), new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
return new(Encoding.UTF8.GetBytes(json));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using Elsa.Features.Services;
|
||||
using Elsa.Identity.Features;
|
||||
using Elsa.Requirements;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Identity.UnitTests.Features;
|
||||
|
||||
public class DefaultAuthenticationFeatureTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultSecurityRootPolicyRequiresAuthenticatedUser()
|
||||
{
|
||||
var feature = new DefaultAuthenticationFeature(Substitute.For<IModule>());
|
||||
var options = new AuthorizationOptions();
|
||||
|
||||
feature.ConfigureAuthorizationOptions(options);
|
||||
|
||||
var policy = options.GetPolicy(IdentityPolicyNames.SecurityRoot);
|
||||
|
||||
Assert.NotNull(policy);
|
||||
Assert.Contains(policy.Requirements, requirement => requirement is DenyAnonymousAuthorizationRequirement);
|
||||
Assert.DoesNotContain(policy.Requirements, requirement => requirement is LocalHostPermissionRequirement);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnableLocalHostPermissionGrantForSecurityRootConfiguresExplicitLocalhostPolicy()
|
||||
{
|
||||
var feature = new DefaultAuthenticationFeature(Substitute.For<IModule>());
|
||||
var options = new AuthorizationOptions();
|
||||
|
||||
feature.EnableLocalHostPermissionGrantForSecurityRoot();
|
||||
feature.ConfigureAuthorizationOptions(options);
|
||||
|
||||
var policy = options.GetPolicy(IdentityPolicyNames.SecurityRoot);
|
||||
|
||||
Assert.True(feature.EnableLocalHostPermissionGrant);
|
||||
Assert.NotNull(policy);
|
||||
Assert.Contains(policy.Requirements, requirement => requirement is LocalHostPermissionRequirement);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnableLocalHostPermissionGrantDoesNotOverwriteCustomAuthorizationConfiguration()
|
||||
{
|
||||
var feature = new DefaultAuthenticationFeature(Substitute.For<IModule>());
|
||||
feature.ConfigureAuthorizationOptions = options => options.AddPolicy("Custom", policy => policy.RequireAuthenticatedUser());
|
||||
|
||||
feature.EnableLocalHostPermissionGrantForSecurityRoot();
|
||||
var options = new AuthorizationOptions();
|
||||
feature.ConfigureAuthorizationOptions(options);
|
||||
|
||||
Assert.True(feature.EnableLocalHostPermissionGrant);
|
||||
Assert.NotNull(options.GetPolicy("Custom"));
|
||||
Assert.Null(options.GetPolicy(IdentityPolicyNames.SecurityRoot));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullConfigureAuthorizationOptionsFallsBackToDefaultSecurityRootPolicy()
|
||||
{
|
||||
var feature = new DefaultAuthenticationFeature(Substitute.For<IModule>())
|
||||
{
|
||||
ConfigureAuthorizationOptions = null!
|
||||
};
|
||||
var options = new AuthorizationOptions();
|
||||
|
||||
feature.ConfigureAuthorizationOptions(options);
|
||||
|
||||
var policy = options.GetPolicy(IdentityPolicyNames.SecurityRoot);
|
||||
|
||||
Assert.NotNull(policy);
|
||||
Assert.Contains(policy.Requirements, requirement => requirement is DenyAnonymousAuthorizationRequirement);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisableLocalHostPermissionGrantForSecurityRootClearsOptInFlag()
|
||||
{
|
||||
var feature = new DefaultAuthenticationFeature(Substitute.For<IModule>());
|
||||
|
||||
feature.EnableLocalHostPermissionGrantForSecurityRoot();
|
||||
feature.DisableLocalHostPermissionGrantForSecurityRoot();
|
||||
|
||||
Assert.False(feature.EnableLocalHostPermissionGrant);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using Elsa;
|
||||
using Elsa.Options;
|
||||
using Elsa.Requirements;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using OptionsFactory = Microsoft.Extensions.Options.Options;
|
||||
|
||||
namespace Elsa.Identity.UnitTests.Requirements;
|
||||
|
||||
public class LocalHostPermissionRequirementHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task DoesNotGrantPermissionsToLocalhostRequestsByDefault()
|
||||
{
|
||||
var context = await AuthorizeAsync(enableLocalHostPermissionGrant: false, isLocal: true);
|
||||
|
||||
Assert.False(context.HasSucceeded);
|
||||
Assert.Empty(context.User.FindAll(PermissionNames.ClaimType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GrantsPermissionsToLocalhostRequestsWhenExplicitlyEnabled()
|
||||
{
|
||||
var context = await AuthorizeAsync(enableLocalHostPermissionGrant: true, isLocal: true);
|
||||
var permissions = context.User.FindAll(PermissionNames.ClaimType).Select(x => x.Value).ToList();
|
||||
|
||||
Assert.True(context.HasSucceeded);
|
||||
Assert.Contains("create:application", permissions);
|
||||
Assert.Contains("create:user", permissions);
|
||||
Assert.Contains("create:role", permissions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DoesNotGrantPermissionsToAuthenticatedLocalhostRequestsWhenExplicitlyEnabled()
|
||||
{
|
||||
var context = await AuthorizeAsync(enableLocalHostPermissionGrant: true, isLocal: true, isAuthenticated: true);
|
||||
|
||||
Assert.False(context.HasSucceeded);
|
||||
Assert.True(context.User.Identity?.IsAuthenticated);
|
||||
Assert.Empty(context.User.FindAll(PermissionNames.ClaimType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SucceedsForAuthenticatedLocalhostRequestsWithExistingBootstrapPermissionsWhenExplicitlyEnabled()
|
||||
{
|
||||
var context = await AuthorizeAsync(enableLocalHostPermissionGrant: true, isLocal: true, isAuthenticated: true, permissions: BootstrapPermissions);
|
||||
var permissions = context.User.FindAll(PermissionNames.ClaimType).Select(x => x.Value).ToList();
|
||||
|
||||
Assert.True(context.HasSucceeded);
|
||||
Assert.True(context.User.Identity?.IsAuthenticated);
|
||||
Assert.Equal(3, permissions.Count);
|
||||
Assert.Contains("create:application", permissions);
|
||||
Assert.Contains("create:user", permissions);
|
||||
Assert.Contains("create:role", permissions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DoesNotDuplicateBootstrapPermissionsAcrossMultipleEvaluations()
|
||||
{
|
||||
var requirement = new LocalHostPermissionRequirement();
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
var authorizationContext = new AuthorizationHandlerContext(new[] { requirement }, user, null);
|
||||
var handler = CreateHandler(enableLocalHostPermissionGrant: true, isLocal: true);
|
||||
|
||||
await handler.HandleAsync(authorizationContext);
|
||||
await handler.HandleAsync(authorizationContext);
|
||||
|
||||
Assert.True(authorizationContext.HasSucceeded);
|
||||
Assert.Equal(1, authorizationContext.User.Identities.Count(x => x.AuthenticationType == JwtBearerDefaults.AuthenticationScheme));
|
||||
Assert.Equal(3, authorizationContext.User.FindAll(PermissionNames.ClaimType).Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DoesNotGrantPermissionsToRemoteRequestsWhenExplicitlyEnabled()
|
||||
{
|
||||
var context = await AuthorizeAsync(enableLocalHostPermissionGrant: true, isLocal: false);
|
||||
|
||||
Assert.False(context.HasSucceeded);
|
||||
Assert.Empty(context.User.FindAll(PermissionNames.ClaimType));
|
||||
}
|
||||
|
||||
private static readonly string[] BootstrapPermissions =
|
||||
[
|
||||
"create:application",
|
||||
"create:user",
|
||||
"create:role"
|
||||
];
|
||||
|
||||
private static async Task<AuthorizationHandlerContext> AuthorizeAsync(bool enableLocalHostPermissionGrant, bool isLocal, bool isAuthenticated = false, params string[] permissions)
|
||||
{
|
||||
var requirement = new LocalHostPermissionRequirement();
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(isAuthenticated ? "Test" : null));
|
||||
user.Identities.First().AddClaims(permissions.Select(x => new Claim(PermissionNames.ClaimType, x)));
|
||||
var authorizationContext = new AuthorizationHandlerContext(new[] { requirement }, user, null);
|
||||
var handler = CreateHandler(enableLocalHostPermissionGrant, isLocal);
|
||||
|
||||
await handler.HandleAsync(authorizationContext);
|
||||
|
||||
return authorizationContext;
|
||||
}
|
||||
|
||||
private static LocalHostPermissionRequirementHandler CreateHandler(bool enableLocalHostPermissionGrant, bool isLocal)
|
||||
{
|
||||
var httpContext = CreateHttpContext(isLocal);
|
||||
var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext };
|
||||
var options = OptionsFactory.Create(new LocalHostPermissionRequirementOptions
|
||||
{
|
||||
EnableLocalHostPermissionGrant = enableLocalHostPermissionGrant
|
||||
});
|
||||
|
||||
return new LocalHostPermissionRequirementHandler(httpContextAccessor, options);
|
||||
}
|
||||
|
||||
private static HttpContext CreateHttpContext(bool isLocal)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Connection.LocalIpAddress = IPAddress.Loopback;
|
||||
httpContext.Connection.RemoteIpAddress = isLocal ? IPAddress.Loopback : IPAddress.Parse("10.0.0.1");
|
||||
return httpContext;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue