* refactor(identity)!: retire the SecurityRoot policy in favour of endpoint permissions Completes T040. ADR 0010 already decided SecurityRoot was overloaded and that endpoints should be authorized by their own permissions; this removes the last of it. Roles/Create and Applications/Create carried Policies(SecurityRoot) alongside an existing RequirePermission, so the policy was redundant there and the line is simply dropped. Secrets/Hash carried only the policy. By default SecurityRoot resolved to RequireAuthenticatedUser(), so any signed-in caller could exercise the password hasher. It now declares identity/users:create, on the grounds that hashing a secret is a step in provisioning a credential. This is a tightening: callers who could hash before and hold no user-creation permission will now be refused. The policy, its two registration paths and the IdentityPolicyNames constant are removed. ConfigureAuthorizationOptions stays public and now defaults to a no-op so hosts that add their own policies are unaffected. BREAKING CHANGE: the SecurityRoot authorization policy and the IdentityPolicyNames class are removed. Hosts referencing either should rely on endpoint permissions, and use DefaultAdminUserFeature for initial bootstrap. Note: SecurityRoot was the only attachment point for LocalHostPermissionRequirement, so the localhost permission grant is now inert. The requirement type and the EnableLocalHostPermissionGrantForSecurityRoot toggles are left in place rather than deleted, but they no longer gate anything -- see the PR for why that path was already incoherent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(identity)!: delete the localhost bootstrap grant and its machinery Follows the SecurityRoot removal in the previous commit. SecurityRoot was the only attachment point for LocalHostPermissionRequirement, so the localhost permission grant is now removed outright rather than left inert: LocalHostPermissionRequirement, LocalHostRequirement (already dead -- registered as a handler but consumed by no policy), LocalHostPermissionRequirementOptions and the two feature toggles all go. The grant was the weakest of the three bootstrap mechanisms Elsa already has. It trusted network position, which stops meaning anything behind a reverse proxy, inside a container, or across a port-forward; it granted unauthenticated access, so the bootstrap action carried no identity; it covered only localhost, so it did nothing for a deployed environment; and it could not perform its headline job, because it granted identity/users:create while POST /identity/users does not carry the policy that injected it. The replacements already exist and both work in deployed environments: UseDefaultAdmin(...) seeds an admin role and user at startup, idempotently, and UseAdminApiKey(...) accepts an out-of-band key. What the localhost grant did usefully provide was a hint that something needed configuring, so IdentityBootstrapDiagnostic replaces that: when the user store is empty and neither mechanism is configured, startup logs an error naming both, instead of every endpoint answering 403 with no explanation. BREAKING CHANGE: LocalHostRequirement, LocalHostPermissionRequirement, LocalHostPermissionRequirementOptions and the Enable/DisableLocalHostPermissionGrantForSecurityRoot toggles are removed. Use UseDefaultAdmin or UseAdminApiKey to bootstrap an instance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(identity): scope the hash endpoint's documentation to users, and pin the declarations The hash endpoint's remarks said the callers that need it are "the ones standing up users and applications", while the endpoint requires identity/users:create alone. An application provisioner reading that would have been sent into a 403. The documentation was the part that was wrong. `POST /identity/applications` generates and hashes the client secret and the API key itself and returns both the plaintext and the hash, so identity/applications:create is already sufficient to create an application and the hash endpoint is not on that path at all. Say so, in the endpoint and in the migration guide, rather than widening a grant nobody needs. Adds EndpointPermissionTests over the three endpoints that carried the retired SecurityRoot policy: the two that only lost a redundant policy line must keep the permission they already declared, and Secrets/Hash must keep the one it gained. The coverage gate only asks whether an endpoint declares something, so either half could otherwise change unnoticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(identity): state the hash endpoint's user-only scope in the summary, and complete the removal list Moves the user-only scoping into the endpoint's <summary>, which is the part that reaches the generated API description, rather than leaving it to a paragraph further down. The remark now says outright that no application-provisioning flow reaches this endpoint and none is documented to, with the reason: POST /identity/applications generates the client secret and the API key itself, hashes both, and returns each plaintext alongside its hash. The migration guide's removal list was partial — it named the requirements and the two toggles but not the handlers, the options type, the EnableLocalHostPermissionGrant property on either feature, or the already-obsolete DisableLocalHostRequirement() alias. A reader hitting a compile error on any of those would not have found it in the guide. It also now records that ConfigureAuthorizationOptions survives as a no-op default. Adds the store-failure case to IdentityBootstrapDiagnosticTests: the broad catch is load-bearing — an unmigrated database must not stop the host from starting — and nothing was holding it in place. Disposes the test service provider, and folds the repeated arrange blocks in DefaultAuthenticationFeatureTests into fields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
49 lines
1.7 KiB
C#
49 lines
1.7 KiB
C#
using Elsa.Features.Services;
|
|
using Elsa.Identity.Features;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using NSubstitute;
|
|
|
|
namespace Elsa.Identity.UnitTests.Features;
|
|
|
|
/// <summary>
|
|
/// These tests previously asserted the shape of the SecurityRoot policy. That policy has been retired in
|
|
/// favour of endpoint permissions (ADR 0010), so what matters now is that the feature registers no policy of
|
|
/// its own and still honours a host's own authorization configuration.
|
|
/// </summary>
|
|
public class DefaultAuthenticationFeatureTests
|
|
{
|
|
private readonly DefaultAuthenticationFeature _feature = new(Substitute.For<IModule>());
|
|
private readonly AuthorizationOptions _options = new();
|
|
|
|
[Fact]
|
|
public void DefaultAuthorizationConfigurationRegistersNoPolicy()
|
|
{
|
|
_feature.ConfigureAuthorizationOptions(_options);
|
|
|
|
Assert.Null(_options.GetPolicy("SecurityRoot"));
|
|
}
|
|
|
|
[Fact]
|
|
public void CustomAuthorizationConfigurationIsHonoured()
|
|
{
|
|
_feature.ConfigureAuthorizationOptions = options => options.AddPolicy("Custom", policy => policy.RequireAuthenticatedUser());
|
|
|
|
_feature.ConfigureAuthorizationOptions(_options);
|
|
|
|
Assert.NotNull(_options.GetPolicy("Custom"));
|
|
Assert.Null(_options.GetPolicy("SecurityRoot"));
|
|
}
|
|
|
|
[Fact]
|
|
public void NullConfigureAuthorizationOptionsFallsBackToANoOp()
|
|
{
|
|
// A host clearing the hook must not take the process down on the next Apply().
|
|
_feature.ConfigureAuthorizationOptions = null!;
|
|
|
|
var exception = Record.Exception(() => _feature.ConfigureAuthorizationOptions(_options));
|
|
|
|
Assert.Null(exception);
|
|
Assert.Null(_options.GetPolicy("SecurityRoot"));
|
|
}
|
|
}
|