Summary
A command declaring authorize A B requires both policies. Stage renders it as [Roles("A", "B")], which Arc evaluates as either. A rendered application therefore admits callers the document does not.
This is present today and does not depend on any unreleased change.
Evidence
Adjacency means AND. PolicyReferenceSyntax.IsAlternative documents itself as "Whether the reference was introduced with or, making it an alternative to the preceding policies" — so a reference written without or is an additional demand, not an alternative.
Stage unions every reference regardless — Source/Rendering.Cratis/Authorization/AuthorizationRenderer.cs:67-78:
var required = declared
.SelectMany(authorize => authorize!.Policies)
.Select(reference => RolesRequiredBy(reference, applicationSet, subject, diagnostics))
.ToArray();
…
var union = required.SelectMany(roles => roles).Distinct(StringComparer.Ordinal).Select(CSharpCodeBuilder.StringLiteral);
return $"Roles({string.Join(", ", union)})";
IsAlternative is never read.
Arc evaluates [Roles] as a disjunction — Arc.Core/Authorization/AuthorizationEvaluator.cs:123-124:
var requiredRoles = roles.Split(',').Select(r => r.Trim());
var userHasRequiredRole = requiredRoles.Any(user.IsInRole);
So authorize Administrator Auditor — meaning both — renders as a rule admitting anyone holding either.
Why no spec caught it
Both authorize fixtures build the reference list the same way, and can only produce disjunctions — for_AuthorizationRenderer/given/an_application_with_policies.cs:51:
new PolicyReferenceSyntax(policy, index > 0, SourceLocation.Start)
Every policy after the first is hard-coded as an alternative, so the conjunction case is unreachable from the specs.
The fix is constrained by what Arc can express
A faithful conjunction cannot be rendered with Arc as it stands:
So the honest rendering for a conjunction is [Authorize] plus a diagnostic — weaker than declared, never anonymous — rather than a Roles union that is wrong in the permissive direction.
Suggested direction
Cratis/Screenplay#68 replaces the flat list with a requirement tree, so the grouping becomes expressible. When Stage absorbs that:
PolicyReferenceSyntax → existing RolesRequiredBy
LogicalPolicyRequirementSyntax { Operator: Or } → the existing union, which is correct for a disjunction
LogicalPolicyRequirementSyntax { Operator: And } → report it and render Authorize
Use the tree rather than References() — the flattened view discards exactly the information this bug is about.
Worth fixing the fixtures in the same change, so the conjunction case is reachable from specs at all.
Summary
A command declaring
authorize A Brequires both policies. Stage renders it as[Roles("A", "B")], which Arc evaluates as either. A rendered application therefore admits callers the document does not.This is present today and does not depend on any unreleased change.
Evidence
Adjacency means AND.
PolicyReferenceSyntax.IsAlternativedocuments itself as "Whether the reference was introduced withor, making it an alternative to the preceding policies" — so a reference written withoutoris an additional demand, not an alternative.Stage unions every reference regardless —
Source/Rendering.Cratis/Authorization/AuthorizationRenderer.cs:67-78:IsAlternativeis never read.Arc evaluates
[Roles]as a disjunction —Arc.Core/Authorization/AuthorizationEvaluator.cs:123-124:So
authorize Administrator Auditor— meaning both — renders as a rule admitting anyone holding either.Why no spec caught it
Both authorize fixtures build the reference list the same way, and can only produce disjunctions —
for_AuthorizationRenderer/given/an_application_with_policies.cs:51:Every policy after the first is hard-coded as an alternative, so the conjunction case is unreachable from the specs.
The fix is constrained by what Arc can express
A faithful conjunction cannot be rendered with Arc as it stands:
[Roles]is strictly OR (above).AuthorizeAttribute.Policyis declared but read nowhere — both evaluators return only(true, authorizeAttribute.Roles). See [Authorize(Policy = "...")] is never evaluated, so a policy-protected command admits any authenticated user Arc#2464.AuthorizationAttributeEvaluatortakes.FirstOrDefault()and ignores the rest, andRolesAttributeis notAllowMultiple.So the honest rendering for a conjunction is
[Authorize]plus a diagnostic — weaker than declared, never anonymous — rather than aRolesunion that is wrong in the permissive direction.Suggested direction
Cratis/Screenplay#68 replaces the flat list with a requirement tree, so the grouping becomes expressible. When Stage absorbs that:
PolicyReferenceSyntax→ existingRolesRequiredByLogicalPolicyRequirementSyntax { Operator: Or }→ the existing union, which is correct for a disjunctionLogicalPolicyRequirementSyntax { Operator: And }→ report it and renderAuthorizeUse the tree rather than
References()— the flattened view discards exactly the information this bug is about.Worth fixing the fixtures in the same change, so the conjunction case is reachable from specs at all.