Skip to content

feat(authz): enforce disabled org state on the authorization path - #1860

Open
AmanGIT07 wants to merge 1 commit into
mainfrom
feat/disabled-org-gate
Open

feat(authz): enforce disabled org state on the authorization path#1860
AmanGIT07 wants to merge 1 commit into
mainfrom
feat/disabled-org-gate

Conversation

@AmanGIT07

@AmanGIT07 AmanGIT07 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Disabling an org only flips its state in Postgres while SpiceDB tuples stay in place, so RPCs that never read the org row kept working for members of a disabled org. This adds one gate on the authorization path: after a permission check passes, the target's org is resolved and the request fails with FailedPrecondition when that org is disabled.

Changes

  • Add the gate: resolve the authorized object to its owning org (org, project, group, invitation, service user, custom resource via its project) and reject targets in disabled orgs
  • Run the gate on every allowed request in IsAuthorized; denied callers get PermissionDenied before any org lookup
  • Org names resolve state-blind (GetRaw) inside the permission check, so a caller without access learns nothing about org state whether it uses the id or the name; the gate enforces state only after the check passes
  • Platform superusers bypass the gate
  • Move the five PAT self-management RPCs from the authorization skip list into the authorization map: CheckCurrentUserPATTitle requires the same org access as CreateCurrentUserPAT; the id-based RPCs check the token's org state through an owner-scoped lookup, so an unknown and a foreign token id behave the same
  • An org admin can no longer delete their own disabled org; superusers still can
  • Add core/resource to the mockery config (its mocks existed but were not regenerable)

Technical Details

  • The gate resolves or steps aside: if the child row is not visible (disabled project or group, unknown resource), the handler keeps its own not-found behavior, so re-enable flows keep working
  • Groups are fetched with disabled ones included so a disabled group in an enabled org can still be re-enabled
  • Internal flows (deletion cascades) don't pass through the interceptor and are unaffected
  • Group and domain handlers keep their own org checks until the follow-up cleanup, so those RPCs read the org twice for now and superusers remain blocked there exactly as before this change
  • The gate adds one to three single-row reads per allowed request depending on the namespace; the AuthToken path is on the skip list and unaffected
  • AuthToken's optional project claim is dropped, not failed, when the project's org is disabled — matching how other authorization failures already behave on that path

Test Plan

  • Unit tests cover check ordering, each namespace, superuser bypass, owner-scoped PAT lookups, and by-name references for members and non-members
  • A namespace coverage test pins which predefined namespaces the gate resolves and which stay org-less
  • Manual testing completed (disabled an org and exercised the RPCs via the e2e scenario locally)
  • Build and type checking passes

@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview Aug 7, 2026 10:38am

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added organization activity checks across requests, authorization targets, and personal access token operations.
    • Disabled organizations can no longer access protected resources or manage tokens.
    • Authorization now supports invitation-based fallback when the initial permission check is denied.
    • Personal access tokens are validated against their organization’s active status.
  • Bug Fixes

    • Improved authorization handling for organization-owned resources and missing or invalid targets.
    • Superuser access remains available when organization activity checks apply.

Walkthrough

The change enforces active-organization checks for authorized resources and personal access tokens. It adds organization resolution, PAT endpoint validation, invitation authorization fallback handling, generated mocks, and table-driven tests.

Changes

Authorization enforcement

Layer / File(s) Summary
Organization-state gates and lookup
core/resource/..., internal/api/v1beta1connect/org_state.go, internal/api/v1beta1connect/mocks/user_pat_service.go, .mockery.yaml
Adds organization and PAT activity checks, resolves owning organizations across resource namespaces, and adds the OrgService.GetRaw contract with generated mock support.
Authorization and PAT endpoint enforcement
internal/api/v1beta1connect/authorize.go, internal/api/v1beta1connect/authorize_test.go, pkg/server/connect_interceptors/authorization.go
Applies invitation fallback results, requires active target organizations, validates current-user PAT operations, and adds coverage for authorization, namespace resolution, and PAT states.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • raystack/frontier#1516 — Both modify PAT authorization handling in pkg/server/connect_interceptors/authorization.go.
  • raystack/frontier#1622 — Both strengthen endpoint authorization in pkg/server/connect_interceptors/authorization.go.
  • raystack/frontier#1792 — Both add organization-state enforcement for PAT and service-user authorization.

Suggested reviewers: rohilsurana, whoabhisheksah

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/api/v1beta1connect/org_state.go (1)

78-157: 🚀 Performance & Scalability | 🔵 Trivial

Consider caching organization state for the authorization hot path.

resolveObjectOrg runs after every successful permission check. For a project target it adds one project read plus one organization read. For a custom resource it adds a resource read, a project read, and an organization read. This applies to every authorized RPC, so read load on those tables increases with total request volume.

A short-lived cache of organization state, or a cached project-to-organization mapping, would remove most of this overhead. Track the added latency before and after rollout.

internal/api/v1beta1connect/authorize_test.go (1)

91-334: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the Internal branches of organization resolution.

The table covers the not-found and invalid-ID branches of resolveObjectOrg. It does not cover the branches that wrap an unexpected service error into connect.CodeInternal. Those are internal/api/v1beta1connect/org_state.go lines 89, 101, 118, 128, 147, and 154. A regression that swallows such an error would let a request through the gate without any test failing.

One case per resolution path is enough. For example, make projectSvc.Get return a generic error and assert the code is connect.CodeInternal.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 287f32f5-f6ae-46cc-bf98-453d77482eec

📥 Commits

Reviewing files that changed from the base of the PR and between c294c5f and dd838bd.

📒 Files selected for processing (6)
  • internal/api/v1beta1connect/authorize.go
  • internal/api/v1beta1connect/authorize_test.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/user_pat_service.go
  • internal/api/v1beta1connect/org_state.go
  • pkg/server/connect_interceptors/authorization.go

Comment thread pkg/server/connect_interceptors/authorization.go
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 31157939344

Coverage increased (+0.2%) to 48.154%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 41 uncovered changes across 2 files (83 of 124 lines covered, 66.94%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
internal/api/v1beta1connect/org_state.go 95 74 77.89%
pkg/server/connect_interceptors/authorization.go 20 0 0.0%
Total (3 files) 124 83 66.94%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 39968
Covered Lines: 19246
Line Coverage: 48.15%
Coverage Strength: 15.37 hits per line

💛 - Coveralls

Resolve every authorized object to its owning org and reject requests
with FailedPrecondition when that org is disabled. Superusers bypass
the gate so they can inspect and re-enable orgs. PAT self-management
RPCs now authorize through the same gate instead of skipping
authorization.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
internal/api/v1beta1connect/authorize_test.go (2)

111-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for a generic orgService.Get failure.

The table covers organization.ErrDisabled and organization.ErrNotExist. It does not cover a generic error from orgService.Get. That path decides whether an infrastructure failure surfaces as CodeInternal and whether it fails closed. Add a case that returns a plain error and asserts the resulting code.

🧪 Proposed additional table case
+		{
+			name:   "org lookup failure surfaces as internal",
+			object: relation.Object{Namespace: schema.OrganizationNamespace, ID: testAuthzOrgID},
+			setup: func(m authorizeMocks, object relation.Object) {
+				expectUserPrincipal(m)
+				expectCheckAuthz(m, object, testAuthzUserID, schema.GetPermission, true, nil)
+				m.orgSvc.EXPECT().Get(mock.Anything, testAuthzOrgID).Return(organization.Organization{}, errors.New("db down"))
+			},
+			wantErr: /* expected connect error from ensureOrgEnabled */,
+		},

346-374: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the namespace coverage list exhaustive.

resolveObjectOrg also short-circuits constants such as schema.SuperUserPrincipal and schema.GroupPrincipal, but TestResolveObjectOrgNamespaces only covers namespaces it can exercise through the resolved map and orgless slice. Add a test that the covered constants match the schema constants, or include the uncovered constants so the documented checklist actually requires them.

internal/api/v1beta1connect/authorize.go (1)

84-85: 🚀 Performance & Scalability | 🔵 Trivial

Reduce the lookup cost added on the authorization path.

ensureObjectOrgActive calls resolveObjectOrg for each authorized request. A custom resource path performs resourceService.Get followed by projectService.Get before checking organization state, and neither service implements cache-backed Get lookups in the checked code. Add a short TTL cache for organization/project metadata, or provide a cached path for these authorization lookups.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a308f39-1a33-490b-bae3-57431c0c16f9

📥 Commits

Reviewing files that changed from the base of the PR and between dd838bd and c06d96a.

📒 Files selected for processing (10)
  • .mockery.yaml
  • core/resource/mocks/authn_service.go
  • core/resource/mocks/org_service.go
  • core/resource/mocks/policy_service.go
  • core/resource/service.go
  • internal/api/v1beta1connect/authorize.go
  • internal/api/v1beta1connect/authorize_test.go
  • internal/api/v1beta1connect/mocks/user_pat_service.go
  • internal/api/v1beta1connect/org_state.go
  • pkg/server/connect_interceptors/authorization.go
💤 Files with no reviewable changes (1)
  • core/resource/mocks/authn_service.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/server/connect_interceptors/authorization.go
  • internal/api/v1beta1connect/org_state.go

Comment thread core/resource/service.go
@AmanGIT07

Copy link
Copy Markdown
Contributor Author

Manual verification

Verified against a local server built from this branch, exercised over ConnectRPC. Codes below are the ConnectRPC error codes returned to the caller. Platform admins bypass the gate, except on group and domain RPCs, whose handlers keep their own org-state check (handler cleanup is a TODO).

Enabled org — baseline

RPC Member Non-member
GetProject OK PermissionDenied
GetGroup OK
ListOrganizationProjects OK
GetOrganization (by id) OK PermissionDenied
GetOrganization (by name) OK PermissionDenied

Disabled org — core resource RPCs

RPC Member Non-member Platform admin
GetOrganization (by id) FailedPrecondition PermissionDenied OK
GetOrganization (by name) FailedPrecondition PermissionDenied OK
GetProject FailedPrecondition PermissionDenied OK
UpdateProject FailedPrecondition OK
ListProjectResources FailedPrecondition
GetGroup FailedPrecondition FailedPrecondition
UpdateGroup FailedPrecondition FailedPrecondition
ListGroupUsers FailedPrecondition FailedPrecondition
CreateGroup FailedPrecondition
GetServiceUser FailedPrecondition PermissionDenied OK
GetOrganizationInvitation FailedPrecondition PermissionDenied
ListOrganizationDomains FailedPrecondition FailedPrecondition
CreateOrganizationInvitation FailedPrecondition

The invitee (the user the invitation was addressed to) also gets FailedPrecondition on GetOrganizationInvitation while the org is disabled.

Object types the gate resolves to an org

Each row was reached through a real RPC. Member on a disabled org gets FailedPrecondition; non-member gets PermissionDenied.

Object type RPC used Result
organization GetOrganization resolved
project GetProject resolved
group GetGroup resolved
invitation GetOrganizationInvitation resolved
service user GetServiceUser resolved
custom resource not configured in this environment

Personal access token RPCs — disabled org

RPC Own token Another user's token id Unknown id
GetCurrentUserPAT FailedPrecondition NotFound NotFound
UpdateCurrentUserPAT FailedPrecondition NotFound
RegenerateCurrentUserPAT FailedPrecondition NotFound
DeleteCurrentUserPAT FailedPrecondition

Another user's token id returns NotFound even when that token's own org is disabled (same result as an unknown id).

CheckCurrentUserPATTitle:

Org state Member Non-member
enabled OK PermissionDenied
disabled FailedPrecondition PermissionDenied

Billing RPCs — disabled org

RPC Member Non-member Platform admin
ListBillingAccounts FailedPrecondition PermissionDenied OK
ListSubscriptions FailedPrecondition PermissionDenied OK
ListInvoices FailedPrecondition PermissionDenied
ListCheckouts FailedPrecondition PermissionDenied
ListBillingTransactions FailedPrecondition
CheckCreditEntitlement FailedPrecondition PermissionDenied

GetBillingAccount, GetBillingBalance, and GetCheckout were not exercised (they need a Stripe customer, which this environment does not have). They resolve the billing/checkout id to the org and authorize against the org, so they run through the same gate.

Lifecycle

Step Result
Platform admin disables org OK
Platform admin lists disabled orgs (ListAllOrganizations) org present
Platform admin re-enables org OK
Member access after re-enable (GetProject, GetGroup) OK, no re-grants needed
Member manages own PAT after re-enable (Get, Delete) OK
Delete an org while it is disabled OK

Other

CheckResourcePermission for an org referenced by name, while disabled, returns a normal true/false status rather than an error.

Not covered here

  • Custom resource object type (no resource config in this environment).
  • ExportOrganizations (server-streaming; covered by the e2e suite).
  • Service account as the PAT caller (covered by unit tests).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants