Skip to content

fix(authz): require permission checks on GetPolicy, ListOrganizations, ListUsers#1622

Merged
whoAbhishekSah merged 2 commits into
mainfrom
feature/fix-authz-gaps
May 20, 2026
Merged

fix(authz): require permission checks on GetPolicy, ListOrganizations, ListUsers#1622
whoAbhishekSah merged 2 commits into
mainfrom
feature/fix-authz-gaps

Conversation

@whoAbhishekSah
Copy link
Copy Markdown
Member

Summary

  • Three ConnectRPC endpoints (GetPolicy, ListOrganizations, ListUsers) accepted any authenticated user without any resource-level authorization, leaking policy details and the platform-wide org/user inventory. This patch adds authz in the existing interceptor map so the gaps are closed without proto/handler/service changes and without introducing new permissions.
  • GetPolicy now fetches the policy and checks policymanage (org/project), group admin, or delete on the underlying resource — mirroring the DeletePolicy route.
  • ListOrganizations keeps the platform-pref gate, allows self-introspection when user_id matches the caller, otherwise requires platform superuser. Regular users wanting their own orgs already have ListOrganizationsByCurrentUser (in the skip list).
  • ListUsers keeps the platform-pref gate, requires get on the org when org_id is set, get on the group when group_id is set, otherwise requires platform superuser.

SDK / consumer impact

  • No web SDK or admin/client-demo app usage — verified by direct grep of web/sdk/, web/apps/admin/, web/apps/client-demo/. The admin UI uses AdminService.SearchUsers/SearchOrganizations/ListOrganizationsByCurrentUser instead.
  • CLI commands frontier organization list and frontier user list (global) will now require platform superuser credentials, which is the correct behaviour for those admin paths.
  • E2E bootstrap helpers and most regression sites already authenticate as the admin email (configured as a platform superuser via appConfig.App.Admin.Users), so they continue to pass. Three TestUserAPI subtests that exercised filter/keyword behaviour as a non-member user were switched to the existing superuser context.

Test plan

  • gofmt clean on changed files
  • go vet ./pkg/server/connect_interceptors/...
  • go vet ./test/e2e/...
  • go build ./...
  • Run the e2e regression suite locally (requires Postgres + SpiceDB) and confirm TestUserAPI, TestOrganizationAPI, bootstrap helpers, and any policy tests still pass

🤖 Generated with Claude Code

…, ListUsers

These three endpoints previously allowed any authenticated user to read
sensitive data without resource-level authz, leaking policy details and
the platform-wide org/user inventory.

- GetPolicy: fetch the policy, then check policymanage (org/project),
  group admin, or delete on the underlying resource, mirroring
  DeletePolicy.
- ListOrganizations: keep the platform-pref gate, allow self-introspection
  when user_id matches the caller, otherwise require platform superuser.
- ListUsers: keep the platform-pref gate, require get on the org when
  org_id is set, get on the group when group_id is set, otherwise
  require platform superuser.

Adjust three TestUserAPI subtests that called ListUsers as a non-member
regular user to use the existing superuser context, since they verify
filter/keyword behaviour rather than authz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 19, 2026

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

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment May 19, 2026 7:55am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Review Change Stack

Warning

Rate limit exceeded

@whoAbhishekSah has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minute and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4aa6786d-353a-483b-94b4-311d00e931ab

📥 Commits

Reviewing files that changed from the base of the PR and between 66034e0 and a407ff8.

📒 Files selected for processing (1)
  • test/e2e/regression/api_test.go
📝 Walkthrough

Walkthrough

Authorization handlers for three Frontier service procedures are strengthened with preference gates and request-aware permission checks: ListUsers routes authorization based on org/group context, ListOrganizations permits self-introspection, and GetPolicy applies namespace-specific rules. Integration tests are updated to use org admin context for the new requirements.

Changes

Authorization strengthening for listing and policy operations

Layer / File(s) Summary
Authorization handlers for ListUsers, ListOrganizations, and GetPolicy
pkg/server/connect_interceptors/authorization.go
FrontierService/ListUsers adds PlatformDisableUsersListing gate and routes to org/group-scoped checks; ListOrganizations adds PlatformDisableOrgsListing gate and self-introspection logic; GetPolicy now fetches the policy and applies namespace-specific authorization rules instead of passing without checks.
Test updates for org-scoped user filtering
test/e2e/regression/api_test.go
Three org-scoped user filtering test cases now use org admin authenticated context instead of current user context when calling ListUsers, validating the updated authorization requirements.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Possibly related PRs

  • raystack/frontier#1516: Both PRs modify pkg/server/connect_interceptors/authorization.go to change procedure-level authorization behavior for Frontier endpoints.

Suggested reviewers

  • rsbh
  • rohilsurana
🚥 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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
pkg/server/connect_interceptors/authorization.go (1)

639-655: ⚡ Quick win

Deduplicate the policy-resource authorization rule.

This is now the same org/project/group/default permission switch used by CreatePolicy and DeletePolicy. Extract it once so future auth changes stay consistent across all three endpoints.

♻️ Suggested extraction
+func authorizePolicyResource(ctx context.Context, handler *v1beta1connect.ConnectHandler, resource string, req connect.AnyRequest) error {
+	ns, id, err := schema.SplitNamespaceAndResourceID(resource)
+	if err != nil {
+		return err
+	}
+
+	switch ns {
+	case schema.OrganizationNamespace, schema.ProjectNamespace:
+		return handler.IsAuthorized(ctx, relation.Object{Namespace: ns, ID: id}, schema.PolicyManagePermission, req)
+	case schema.GroupNamespace:
+		return handler.IsAuthorized(ctx, relation.Object{Namespace: ns, ID: id}, group.AdminPermission, req)
+	default:
+		return handler.IsAuthorized(ctx, relation.Object{Namespace: ns, ID: id}, schema.DeletePermission, req)
+	}
+}
+
 "/raystack.frontier.v1beta1.FrontierService/GetPolicy": func(ctx context.Context, handler *v1beta1connect.ConnectHandler, req connect.AnyRequest) error {
 	pbreq := req.(*connect.Request[frontierv1beta1.GetPolicyRequest])
 	policyResp, err := handler.GetPolicy(ctx, connect.NewRequest(&frontierv1beta1.GetPolicyRequest{Id: pbreq.Msg.GetId()}))
 	if err != nil {
 		return err
 	}
-	ns, id, err := schema.SplitNamespaceAndResourceID(policyResp.Msg.GetPolicy().GetResource())
-	if err != nil {
-		return err
-	}
-
-	switch ns {
-	case schema.OrganizationNamespace, schema.ProjectNamespace:
-		return handler.IsAuthorized(ctx, relation.Object{Namespace: ns, ID: id}, schema.PolicyManagePermission, req)
-	case schema.GroupNamespace:
-		return handler.IsAuthorized(ctx, relation.Object{Namespace: ns, ID: id}, group.AdminPermission, req)
-	}
-	return handler.IsAuthorized(ctx, relation.Object{Namespace: ns, ID: id}, schema.DeletePermission, req)
+	return authorizePolicyResource(ctx, handler, policyResp.Msg.GetPolicy().GetResource(), req)
 },
test/e2e/regression/api_test.go (2)

1439-1441: ⚡ Quick win

Keep the org-filter coverage off the suite admin context.

This context is also used for admin-only API calls in this suite, so these assertions still pass if ListUsers regresses back to superuser-only access. Prefer authenticating a real admin/member of existingOrg here.

Also applies to: 1454-1456


1470-1472: ⚡ Quick win

Add the negative case for global ListUsers.

The changed auth rule here is that an unscoped user search now requires platform-superuser. Please add a PermissionDenied assertion with ctxCurrentUser so that regression stays covered.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eb77cbaa-eea7-41f6-b5f5-3471edd6ee22

📥 Commits

Reviewing files that changed from the base of the PR and between 996912f and 66034e0.

📒 Files selected for processing (2)
  • pkg/server/connect_interceptors/authorization.go
  • test/e2e/regression/api_test.go

@coveralls
Copy link
Copy Markdown

coveralls commented May 19, 2026

Coverage Report for CI Build 26084048440

Coverage decreased (-0.03%) to 42.306%

Details

  • Coverage decreased (-0.03%) from the base build.
  • Patch coverage: 34 uncovered changes across 1 file (0 of 34 lines covered, 0.0%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
pkg/server/connect_interceptors/authorization.go 34 0 0.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 37746
Covered Lines: 15969
Line Coverage: 42.31%
Coverage Strength: 11.88 hits per line

💛 - Coveralls

ListUsers now requires `get` permission on the org_id passed in. After
the org is deleted, that check fails with permission_denied — which is
consistent with how the same subtest already asserts errors for
GetOrganization, GetProject, and GetProjectResource on the deleted
resource. Returning an empty user list instead would leak information
about deleted orgs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@whoAbhishekSah
Copy link
Copy Markdown
Member Author

Manual test run on local Frontier (this branch): 31/31 pass.

Setup: fresh Org A (alice=owner, bob=viewer, dave=manager), fresh Org B (charlie=owner), Project A in Org A, group reused from prior fixtures; fresh policies on each namespace.

ListUsers

# caller request expected result
1 regular user no filter permission_denied
2 platform SU no filter OK
3 org owner org_id (own) OK
4 org viewer org_id (own) OK
5 non-member org_id permission_denied
6 other-org owner org_id (own org) OK
7 platform SU org_id (any) OK
8 group owner group_id OK
9 non-member group_id permission_denied
10 regular user org_id = bogus UUID permission_denied (no leak)

ListOrganizations

# caller request expected result
1 regular user no filter permission_denied
2 platform SU no filter OK
3 regular user user_id=self OK
4 regular user user_id=other permission_denied
5 platform SU user_id=other OK

GetPolicy

# caller policy scope expected result
1 org owner org OK (policymanage)
2 org viewer org permission_denied
3 org manager org permission_denied (manager ≠ policymanage)
4 non-member org permission_denied
5 platform SU org OK
6 org owner (parent) project OK (org->project_policymanage)
7 project viewer project permission_denied
8 other-org owner project permission_denied
9 group owner group OK (delete-perm)
10 non-member group permission_denied
11 any bogus policy id not_found

Sibling paths still work

  • ListOrganizationsByCurrentUser (in skip list) — still OK for regular user ✅
  • disable_users_listing=true — returns unavailable for regular user AND platform SU (pref before authz) ✅
  • disable_orgs_listing=true — blocks self-introspection too ✅

Service-user path (Basic auth)

  • Service user with org viewer role: ListUsers(org_id) OK, ListUsers() denied, ListOrganizations(user_id=self) OK, ListOrganizations(user_id=other) denied ✅

No regressions observed in the skipped/sibling endpoints.

@whoAbhishekSah whoAbhishekSah merged commit 3f550de into main May 20, 2026
8 checks passed
@whoAbhishekSah whoAbhishekSah deleted the feature/fix-authz-gaps branch May 20, 2026 04:14
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.

3 participants