fix(identity): SECURITY — permission gates fail open on .RequireAuthorization() route-groups#1290
Conversation
…ts (broken access control) Permission checks live in the "RequiredPermission" authorization policy, which reads each endpoint's RequiredPermissionAttribute (added by .RequirePermission(...)). It was wired ONLY as the FallbackPolicy. In ASP.NET Core the fallback policy runs only on endpoints with no authorization metadata — but Catalog, Billing, Chat, Files, Notifications, Tickets and Webhooks all call .RequireAuthorization() at the route-group level. That attaches the built-in default policy (authenticated-only), which suppressed the fallback, so .RequirePermission(...) was attached as metadata but NEVER evaluated. Result: any authenticated tenant member could perform gated writes in those modules — e.g. a Basic user creating/deleting products despite lacking Products.Create/Delete. (Identity/Auditing/Multitenancy have no group-level .RequireAuthorization(), so their fallback ran and they were unaffected.) Fix: set DefaultPolicy to the same RequiredPermission policy as the fallback, so .RequireAuthorization() (default policy) also evaluates permissions. Endpoints without permission metadata still just require authentication (the handler succeeds when no RequiredPermissionAttribute is present). One-line wiring change fixes all 7 modules. Adds a regression test (CreateProduct_Should_Return403_When_AuthenticatedUserLacksPermission) covering the suite's blind spot: it previously only tested admin->200 and unauthenticated->401, never authenticated-but-forbidden->403. Verified red (2xx) before the fix, green (403) after; the full ProductsEndpointTests class stays green (admin create/update/delete/restore/search unaffected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…from Basic users CI: the Release `-warnaserror` build was already red on main — SonarAnalyzer S125 false-positives on 6 descriptive test comments (semicolon/code-like prose, not dead code) in Integration.Tests and Architecture.Tests. Suppress S125 in those two test projects (deleting the comments would lose useful documentation). Dashboard: the Users/Roles/Groups nav items had no permission gate, so they showed to everyone. Gate each on its *.Update permission. Basic users lack Update, so the identity-admin pages hide for them — without touching View Users/Roles/Groups (those stay IsBasic because the chat/user picker depends on Users.View). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Follow-on commits in this PR (per request to make it self-contained): CI unblock (S125). The Release Hide identity-admin nav from Basic users. The Users/Roles/Groups nav items had no permission gate ( Known follow-up (not in this PR): the dashboard has no route guards, so a Basic user who types Verification recap: new 403 test red→green; full integration suite 713 passed / 0 failed / 1 skipped; Release build clean; dashboard |
Severity: High — Broken Access Control (OWASP A01)
A Basic tenant user can perform privileged writes they have no permission for — e.g. create/delete products.
Root cause
Permission checks are implemented by the
RequiredPermissionauthorization policy, which reads each endpoint''sRequiredPermissionAttribute(attached by.RequirePermission(...)). That policy was wired only as theFallbackPolicy(JwtAuthenticationExtensions.cs).In ASP.NET Core the fallback policy runs only on endpoints with no authorization metadata. But these modules call
.RequireAuthorization()at the route-group level:.RequireAuthorization()(no args) attaches the built-in default policy (authenticated-only). That auth metadata suppresses the fallback, so theRequiredPermissionpolicy never runs —.RequirePermission(...)is present as metadata but never evaluated. Any authenticated tenant member passes.Identity / Auditing / Multitenancy have no group-level
.RequireAuthorization(), so their fallback runs — they were correctly enforced (which is why a Basic user''s view of Users/Roles/Groups is legitimately allowed — Basic holds thoseViewpermissions — while their Identity writes stayed blocked).Fix
Set
DefaultPolicyto the sameRequiredPermissionpolicy as the fallback. Now.RequireAuthorization()(which uses the default policy) also evaluates permissions. Endpoints without.RequirePermissionmetadata still just require authentication (the handler succeeds when no attribute is present). One wiring change fixes all 7 modules — no per-module edits, no change to the protected BuildingBlocks.Verification (red → green)
Added
CreateProduct_Should_Return403_When_AuthenticatedUserLacksPermission— the gap the suite never covered (it only had admin→200 and unauthenticated→401, never authenticated-but-forbidden→403).ProductsEndpointTestsclass stays green (24/24) — admin create/update/delete/restore/search unaffected.Notes
View Users/Roles/Groupsare flaggedIsBasic(a design question to revisit — should Basic users see the tenant directory?), not a frontend leak.dotnet test; I used-p:TreatWarningsAsErrors=falseto run. Worth a separate cleanup.🤖 Generated with Claude Code