Skip to content

fix(policy): throw on unknown action, identity errors, and string grants - #3415

Merged
bpamiri merged 3 commits into
developfrom
cursor/policy-hardener-s1-s3-s4-a9b9
Aug 25, 2026
Merged

fix(policy): throw on unknown action, identity errors, and string grants#3415
bpamiri merged 3 commits into
developfrom
cursor/policy-hardener-s1-s3-s4-a9b9

Conversation

@bpamiri

@bpamiri bpamiri commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Why

Peter flipped the three Policy holds that #3414 left unflipped. Unknown policy actions were a silent deny (can() returned false, authorize() threw Wheels.NotAuthorized as if the policy had spoken). A throwing DI currentUser fell through to the authenticator user. A throwing authenticator became guest "". authorize() and can() treated the CFML strings "yes" and "true" as a grant.

This PR flips S1, S3, and S4 only. Desk IDs stay S1/S3/S4. S2 and S5–S9 already landed on develop in #3414.

Scope

  • vendor/wheels/controller/authorization.cfc
    • $invokePolicyAction() throws Wheels.Policy.UnknownAction when the method is missing
    • Reserved init and scope still deny as Wheels.NotAuthorized (S9)
    • $policyGranted() grants only when SerializeJSON(allowed) == "true"
    • $currentUserForPolicy() no longer has catch(any) failover
    • S8 production abort stays. The filter sets request.$wheelsIsolateAbort so that path throws instead of aborting the TestBox request
  • vendor/wheels/tests/specs/Authorization/AuthorizationSpec.cfc
  • Fixtures CurrentUserThrowingStub.cfc, ThrowingCurrentUserStrategy.cfc, plus yesGrant / trueGrant / boolGrant on PostPolicy.cfc
  • changelog.d/policy-hardener-s1-s3-s4.changed.md and .security.md

CLI stays out. vendor/wheels/Policy.cfc is unchanged. S2/S5–S9 specs stay as they are except the S8 abort isolate.

Tradeoffs

Wheels.Policy.UnknownAction is a programmer error (HTTP 500 via the existing Wheels.* mapping), not a deny (403). That is the typo-fails-loud point. A wrapped Wheels.Policy.IdentityResolutionFailed would hide the original boom type, so identity failures propagate as-is.

SerializeJSON(allowed) == "true" is the grant check. Boolean true serializes to the JSON boolean. The strings "yes" and "true" serialize quoted and deny. Numeric 1 serializes as 1 and also denies.

Blast Radius

can("publish", post) now throws instead of returning false. Views that used can() to hide a button for a missing method will 500 until the method exists or the call is fixed. That is the S1 flip.

Apps that registered a broken currentUser DI service and relied on authenticator failover will now see the resolver exception. Guest "" when nothing is registered is unchanged (S7).

Policy methods declared boolean that return "yes" still grant if the engine coerces the return at the method boundary. S4 is about the value authorize() / can() receive.

Desk S1–S9

ID Status Note
S1 PROVEN authorize(action="publish") and can("publish") throw Wheels.Policy.UnknownAction. Reserved init/scope stay Wheels.NotAuthorized.
S2 already on develop (#3414) Empty scope does not call whereIn.
S3 PROVEN Throwing DI currentUser does not become the authenticator user (Wheels.Policy.CurrentUserBoom). Throwing authenticator does not become guest "" (Wheels.Policy.AuthenticatorBoom).
S4 PROVEN authorize() and can() deny "yes" and "true". Boolean true still grants.
S5 already on develop (#3414) Production InvalidCollection fail-closes.
S6 already on develop (#3414) DI currentUser and authenticator identity.
S7 already on develop (#3414) Guest is "".
S8 already on develop (#3414) Production deny is HTTP 403. Spec sets $wheelsIsolateAbort and restores 200.
S9 already on develop (#3414) Reserved init/scope are Wheels.NotAuthorized.

Verification

wheels test --core --ci --filter=Authorization
38 passed (0.49s)

JSON from the same filter (directory=wheels.tests.specs.Authorization):

totalPass=38
totalFail=0
totalError=0
totalSkipped=0
totalSpecs=38
bundlesDiscovered=1

HEAD 1ee8f57bb24b338110dc1193d1c0a975c27ada02

Base 06acc07d82cbacbf7a8a43c596fc9b838878568e (develop after #3414).

Wheels CLI 4.0.6. Lucee 7.0.0.395. sqlite.

No closer keywords. Do not merge.

Open in Web Open in Cursor 

cursoragent and others added 2 commits August 25, 2026 02:42
Unknown publish, a throwing DI currentUser, a throwing authenticator,
and policy methods that return yes/true must fail the Authorization filter
on develop. Reserved init/scope stay Wheels.NotAuthorized.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
authorize() and can() throw Wheels.Policy.UnknownAction when the method
is missing. Reserved init and scope still deny as Wheels.NotAuthorized.
$currentUserForPolicy() lets a throwing currentUser or authenticator
propagate. Only boolean true grants.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
Production $notAuthorized() still writes Forbidden and abort. The
Authorization filter sets request.\$wheelsIsolateAbort so that path
throws Wheels.NotAuthorized instead of aborting the TestBox request.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
@bpamiri
bpamiri marked this pull request as ready for review August 25, 2026 02:51
@bpamiri
bpamiri merged commit 3bb4bcd into develop Aug 25, 2026
12 checks passed
@bpamiri
bpamiri deleted the cursor/policy-hardener-s1-s3-s4-a9b9 branch August 25, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants