Skip to content

fix(server,client): gate cluster mutations on manage_connections (#207)#217

Merged
dpage merged 4 commits into
mainfrom
fix/issue-207-cluster-auth
May 12, 2026
Merged

fix(server,client): gate cluster mutations on manage_connections (#207)#217
dpage merged 4 commits into
mainfrom
fix/issue-207-cluster-auth

Conversation

@dpage
Copy link
Copy Markdown
Member

@dpage dpage commented May 11, 2026

Summary

Fixes #207 — unauthorized users could POST to cluster-group and cluster
create endpoints; the server silently committed the rows and returned a
success status, but the rows were invisible to the creator (only admins
could see them).

An audit triggered by the report found additional unprotected mutating
routes in cluster_handlers.go. Eleven handlers in total now check
manage_connections and return 403 Forbidden on denial.

Server (cluster_handlers.go)

  • Plain admin gate on create / topology mutations:
    • POST /api/v1/cluster-groups
    • POST /api/v1/cluster-groups/{id}/clusters
    • POST /api/v1/clusters
    • POST /api/v1/clusters/{id}/servers
    • DELETE /api/v1/clusters/{id}/servers/{connectionId}
    • PUT /api/v1/clusters/{id}/connections/{connId}/relationships
    • DELETE /api/v1/clusters/{id}/connections/{connId}/relationships
    • DELETE /api/v1/clusters/{id}/relationships/{relationshipId}
  • Owner-fallback (group owner can still manage their own group):
    • DELETE /api/v1/cluster-groups/{id}
  • Plain admin gate on per-cluster updates (the clusters table has
    no owner_username column, so a true owner-fallback isn't possible
    without a schema change):
    • PUT /api/v1/clusters/{id}
    • DELETE /api/v1/clusters/{id}

The denial check fires before request decoding so payload shape
isn't leaked to unauthorized callers.

Client (AddMenu.tsx)

  • "Add Cluster Group" and "Add Cluster" menu items are hidden when the
    user lacks manage_connections. "Add Server" is unchanged.

OpenAPI

  • All 11 routes document a 403 Forbidden response with consistent
    wording (Requires manage_connections permission).
  • One previously-undocumented relationship route was added.
  • docs/admin-guide/api/openapi.json regenerated.

Tests

  • 32 new server tests in rbac_issue207_clusters_test.go covering
    403-on-denied, success-on-grant, owner-fallback, and "denied-before-
    decode" assertions.
  • 10 new client tests for AddMenu (100% line coverage on AddMenu.tsx).
  • make test-all passes; lint warning count unchanged from baseline.

Test plan

  • make test-all passes locally
  • New tests cover the 403 path for every newly gated handler
  • OpenAPI tests pass
  • AddMenu hides items for users without manage_connections
  • Verify in CI with full Postgres-backed integration suite
  • Manual: log in as a user without manage_connections, attempt to
    create a cluster group via the API → expect 403, no row inserted
  • Manual: same user via UI → Add menu shows only "Add Server"

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security

    • Cluster and cluster-group mutation operations now require the manage_connections permission and return 403 for unauthorized requests.
  • New Features / API

    • Added endpoints to set/clear per-connection relationship mappings and new request schemas for relationship updates.
  • UI Updates

    • “Add Cluster” and “Add Cluster Group” menu items are shown only when permitted.
  • Documentation

    • Added RBAC gating patterns and permission-gated UI guidance.
  • Tests

    • New regression and integration tests covering RBAC gates, owner/admin paths, and request-shape checks.

Review Change Stack

Unauthorized users could POST to cluster-group and cluster create
endpoints; the server silently committed the rows and returned a
success status. An audit identified eleven cluster handlers missing
the standard admin gate. All now check manage_connections and
return 403 on denial. The web client's Add menu hides "Add Cluster
Group" and "Add Cluster" for users without the permission. The
OpenAPI spec documents 403 Forbidden on the affected paths.

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

coderabbitai Bot commented May 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cf972223-a9a8-4e06-92b0-3c5710d7d1a4

📥 Commits

Reviewing files that changed from the base of the PR and between c6c64b2 and c1a00ec.

📒 Files selected for processing (7)
  • .claude/golang-expert/rbac-patterns.md
  • client/src/components/__tests__/AddMenu.test.tsx
  • docs/admin-guide/api/openapi.json
  • server/src/internal/api/cluster_handlers.go
  • server/src/internal/api/cluster_handlers_test.go
  • server/src/internal/api/openapi.go
  • server/src/internal/api/rbac_issue207_clusters_test.go
✅ Files skipped from review due to trivial changes (1)
  • .claude/golang-expert/rbac-patterns.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • server/src/internal/api/cluster_handlers.go
  • client/src/components/tests/AddMenu.test.tsx
  • docs/admin-guide/api/openapi.json
  • server/src/internal/api/rbac_issue207_clusters_test.go

Walkthrough

This PR enforces the manage_connections RBAC permission on eleven cluster and cluster-group mutation endpoints, updates OpenAPI and client UI to reflect the requirement, adds RBAC pattern documentation, and introduces extensive unit and integration tests validating early gate ordering and owner-fallback behavior.

Changes

RBAC Authorization for Cluster Mutations

Layer / File(s) Summary
RBAC Pattern Documentation
.claude/golang-expert/rbac-patterns.md
Defines three authorization gating variants (plain admin gate, owner-fallback gate for per-object rows, and per-object admin gate), prescribes gate ordering (authorization before decode/datastore/side effects), and specifies test patterns including denial unit tests, "denied skips decode", admin-allowed sanity tests, Postgres owner-fallback integration tests, plus a 90%+ coverage/checklist requirement.
Client Permission Gating & Quality Checklist
.claude/react-expert/quality-checklist.md, client/src/components/AddMenu.tsx
Adds React quality checklist guidance for permission-gated UI; AddMenu now imports useAuth and conditionally renders "Add Cluster" and "Add Cluster Group" only when hasPermission('manage_connections') returns true; "Add Server" remains always visible.
Client Test Implementation & Fixtures
client/src/components/__tests__/AddMenu.test.tsx, client/src/components/__tests__/ClusterNavigator.test.tsx
Adds a full AddMenu test suite mocking useAuth().hasPermission for permitted, non-permitted, unauthenticated/loading, and open=false states; updates ClusterNavigator tests to include hasPermission in the mocked auth shape and superuser overrides.
OpenAPI Specification & Docs
docs/admin-guide/api/openapi.json, server/src/internal/api/openapi.go, docs/changelog.md
Updates endpoint descriptions and 403 responses to explicitly require manage_connections; adds RelationshipInput and SetRelationshipsRequest schemas; adds PUT /clusters/{id}/connections/{connId}/relationships and DELETE /clusters/{id}/connections/{connId}/relationships; records the change in changelog.
Server Authorization Gate Implementation
server/src/internal/api/cluster_handlers.go
Adds early manage_connections checks that return 403 Forbidden before JSON decoding or datastore access across mutation handlers: createClusterGroup, deleteClusterGroup (owner-fallback), createClusterInGroup, updateCluster, deleteCluster, addServerToCluster, handleRemoveServerFromCluster, handleCreateCluster, setConnectionRelationships, handleDeleteRelationship, clearConnectionRelationships.
Server Unit Test Fixture Updates
server/src/internal/api/cluster_handlers_test.go
Updates validation-focused unit tests to construct handlers with auth.NewRBACChecker(nil) so admin gates pass in decode/validation tests; expands test schema to include clusters and connections and adjusts teardown ordering.
Comprehensive RBAC Regression Tests
server/src/internal/api/rbac_issue207_clusters_test.go
Adds a regression suite covering denial unit tests (403 before decode/datastore), "denied skips decode" tests, owner-fallback integration tests for deleteClusterGroup, admin-allowed sanity tests, and JSON request-shape smoke tests across the affected endpoints.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Handler
  participant RBACChecker
  participant Datastore
  Client->>Handler: Mutation request
  Handler->>RBACChecker: Check PermManageConnections
  RBACChecker-->>Handler: Denied (403) or Allowed
  alt Denied
    Handler-->>Client: 403 Forbidden
  else Allowed
    Handler->>Datastore: Read/Mutate
    Datastore-->>Handler: Result
    Handler-->>Client: Success/Validation error
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • pgEdge/ai-dba-workbench#22: Both PRs modify the project's OpenAPI generation (server/src/internal/api/openapi.go) and related OpenAPI/spec generation tooling.
  • pgEdge/ai-dba-workbench#159: Both PRs touch server cluster mutation handlers to enforce manage_connections RBAC checks; related work on deletion handlers.
  • pgEdge/ai-dba-workbench#92: Both PRs make RBAC and authorization changes to server cluster handlers addressing access control for cluster operations.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: gating cluster mutations on the manage_connections permission across both server and client, directly addressing the core fix for issue #207.
Linked Issues check ✅ Passed All coding requirements from issue #207 are met: manage_connections gate added to eleven cluster mutation endpoints, authorization checks occur before request decoding, 403 Forbidden returned on denial, client UI hides unauthorized actions, and comprehensive tests verify the authorization behavior including owner-fallback logic.
Out of Scope Changes check ✅ Passed All changes are directly scoped to #207: authorization gates on cluster endpoints, client UI permission gating, OpenAPI documentation updates, test coverage, and pattern documentation. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-207-cluster-auth

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

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 11, 2026

Up to standards ✅

🟢 Issues 1 high

Results:
1 new issue

Category Results
ErrorProne 1 high (1 false positive)

View in Codacy

🟢 Metrics 39 complexity · 53 duplication

Metric Results
Complexity 39
Duplication 53

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copy link
Copy Markdown

@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.

Actionable comments posted: 3

🧹 Nitpick comments (3)
server/src/internal/api/cluster_handlers.go (1)

597-629: 💤 Low value

Owner-fallback authorization correctly implemented; minor comment clarification.

The owner-fallback logic is sound: admins with manage_connections OR the group's owner can delete. The pattern matches updateClusterGroup (lines 548-595).

Minor observation: the comment at line 601 states "before any datastore read so denied callers cannot probe group existence," but GetClusterGroup executes at line 616 before the final authorization decision at line 625. This is necessary for ownership verification, but creates a 404 (not found) vs 403 (forbidden) distinction. The comment could be clarified to say "before request decoding" rather than "before any datastore read."

This doesn't affect correctness—the code properly implements the spec's owner-fallback requirement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/internal/api/cluster_handlers.go` around lines 597 - 629, Update
the inline comment in deleteClusterGroup to accurately reflect the order of
operations: instead of claiming the authorization check runs "before any
datastore read", clarify that the owner-fallback authorization (admins with
manage_connections or the group's owner) is applied early — before request
decoding/response writing — but that GetClusterGroup is necessarily called to
verify ownership; modify the comment around the owner-fallback block
(referencing deleteClusterGroup, getUserInfoCompat, HasAdminPermission, and
GetClusterGroup) to say something like "authorization enforced early (before
request decoding/output); datastore read is performed to verify ownership" so
the comment matches the code behavior.
server/src/internal/api/rbac_issue207_clusters_test.go (1)

90-105: ⚡ Quick win

assertForbiddenWithMessage should validate the expected permission-denied wording.

The helper comment says it checks canonical permission-denied messaging, but it only enforces non-empty text. Verifying the expected message (or stable substring) would better protect the API contract documented for these 403 paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/internal/api/rbac_issue207_clusters_test.go` around lines 90 -
105, The test helper assertForbiddenWithMessage currently only checks for a
non-empty error string; update it to assert the canonical permission-denied
wording by decoding ErrorResponse (already present) and verifying resp.Error
contains the expected message or stable substring (e.g., "permission denied" or
the project constant if one exists). Modify assertForbiddenWithMessage to
import/use the canonical message constant (or define expectedMsg in the test)
and replace the resp.Error == "" check with a strings.Contains(resp.Error,
expectedMsg) assertion so the test fails if the 403 message no longer matches
the documented contract.
client/src/components/__tests__/AddMenu.test.tsx (1)

127-135: ⚡ Quick win

Add an explicit “no divider/separator” assertion in denied states.

You already assert gated items are hidden; asserting no separator is rendered will lock the full UI contract and prevent subtle regressions.

✅ Suggested test hardening
         it('renders only Add Server', () => {
             renderAddMenu();

             expect(screen.getByText('Add Server')).toBeInTheDocument();
             expect(screen.queryByText('Add Cluster')).not.toBeInTheDocument();
             expect(
                 screen.queryByText('Add Cluster Group'),
             ).not.toBeInTheDocument();
+            expect(screen.queryByRole('separator')).not.toBeInTheDocument();
         });
...
         it('hides gated items when hasPermission always returns false', () => {
             mockHasPermission.mockReturnValue(false);

             renderAddMenu();

             expect(screen.getByText('Add Server')).toBeInTheDocument();
             expect(screen.queryByText('Add Cluster')).not.toBeInTheDocument();
             expect(
                 screen.queryByText('Add Cluster Group'),
             ).not.toBeInTheDocument();
+            expect(screen.queryByRole('separator')).not.toBeInTheDocument();
         });

Also applies to: 159-163

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/__tests__/AddMenu.test.tsx` around lines 127 - 135, The
test "renders only Add Server" should also assert that no UI divider/separator
is rendered when gated items are hidden to lock the contract; update the test
that uses renderAddMenu() to include an assertion that
screen.queryByRole('separator') (or queryAllByRole('separator') === 0) is not in
the document, and apply the same additional assertion to the similar test around
the 159-163 block so both denied-state tests verify absence of separators in
addition to the hidden menu items.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/admin-guide/api/openapi.json`:
- Around line 2495-2496: Update the OpenAPI docs for the deleteClusterGroup
endpoint (operationId: deleteClusterGroup) to reflect the owner-fallback rule:
change the endpoint "description" to state that deletion requires the
manage_connections permission or is permitted for the cluster-group owner via
fallback, and adjust the 403 response description to say "Forbidden: requires
manage_connections permission (owners may be allowed via owner-fallback)" so the
contract matches runtime behavior; apply the same wording change to the
duplicate cluster-group deletion entry referenced around lines 2525-2527.

In `@server/src/internal/api/openapi.go`:
- Around line 1697-1705: The OpenAPI entry for the deleteClusterGroup operation
(OperationID "deleteClusterGroup") currently states manage_connections is always
required; update the operation's description and 403 response text to reflect
the server's owner-fallback behavior so the contract is accurate: change the
Description to mention owners may delete without the permission and update the
403 Response (jsonResponse) text to something like "Requires manage_connections
permission (unless requester is the group owner)" so the Security/security
description and Responses map match the server logic for deleteClusterGroup.

In `@server/src/internal/api/rbac_issue207_clusters_test.go`:
- Around line 157-181: Update the test assertion so it fails if the handler
returns either 403 or 401 instead of only 403: in
TestClusterHandler_CreateClusterGroup_Issue207_SuperuserAllowed (and the similar
test around the other occurrence) check rec.Code against both
http.StatusForbidden and http.StatusUnauthorized after calling
handler.createClusterGroup and call t.Fatalf if rec.Code equals either value so
an unexpected 401 (authentication regression) is treated as a test failure.

---

Nitpick comments:
In `@client/src/components/__tests__/AddMenu.test.tsx`:
- Around line 127-135: The test "renders only Add Server" should also assert
that no UI divider/separator is rendered when gated items are hidden to lock the
contract; update the test that uses renderAddMenu() to include an assertion that
screen.queryByRole('separator') (or queryAllByRole('separator') === 0) is not in
the document, and apply the same additional assertion to the similar test around
the 159-163 block so both denied-state tests verify absence of separators in
addition to the hidden menu items.

In `@server/src/internal/api/cluster_handlers.go`:
- Around line 597-629: Update the inline comment in deleteClusterGroup to
accurately reflect the order of operations: instead of claiming the
authorization check runs "before any datastore read", clarify that the
owner-fallback authorization (admins with manage_connections or the group's
owner) is applied early — before request decoding/response writing — but that
GetClusterGroup is necessarily called to verify ownership; modify the comment
around the owner-fallback block (referencing deleteClusterGroup,
getUserInfoCompat, HasAdminPermission, and GetClusterGroup) to say something
like "authorization enforced early (before request decoding/output); datastore
read is performed to verify ownership" so the comment matches the code behavior.

In `@server/src/internal/api/rbac_issue207_clusters_test.go`:
- Around line 90-105: The test helper assertForbiddenWithMessage currently only
checks for a non-empty error string; update it to assert the canonical
permission-denied wording by decoding ErrorResponse (already present) and
verifying resp.Error contains the expected message or stable substring (e.g.,
"permission denied" or the project constant if one exists). Modify
assertForbiddenWithMessage to import/use the canonical message constant (or
define expectedMsg in the test) and replace the resp.Error == "" check with a
strings.Contains(resp.Error, expectedMsg) assertion so the test fails if the 403
message no longer matches the documented contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 500fe738-7035-4b15-8a26-e0ea5a53a6a8

📥 Commits

Reviewing files that changed from the base of the PR and between e2e6d78 and 922c034.

📒 Files selected for processing (11)
  • .claude/golang-expert/rbac-patterns.md
  • .claude/react-expert/quality-checklist.md
  • client/src/components/AddMenu.tsx
  • client/src/components/__tests__/AddMenu.test.tsx
  • client/src/components/__tests__/ClusterNavigator.test.tsx
  • docs/admin-guide/api/openapi.json
  • docs/changelog.md
  • server/src/internal/api/cluster_handlers.go
  • server/src/internal/api/cluster_handlers_test.go
  • server/src/internal/api/openapi.go
  • server/src/internal/api/rbac_issue207_clusters_test.go

Comment thread docs/admin-guide/api/openapi.json Outdated
Comment thread server/src/internal/api/openapi.go Outdated
Comment thread server/src/internal/api/rbac_issue207_clusters_test.go
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@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 (1)
server/src/internal/api/cluster_handlers_test.go (1)

203-208: ⚡ Quick win

Extract the “permission-satisfied” test handler setup.

The same NewClusterHandler(nil, nil, auth.NewRBACChecker(nil)) pattern and explanation now appear in five tests. A tiny helper would centralize the nil-store RBAC assumption and make future auth changes easier to update in one place.

Also applies to: 231-234, 259-262, 476-479, 626-629

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/internal/api/cluster_handlers_test.go` around lines 203 - 208,
Extract the repeated test setup into a small helper that returns the handler
pre-configured with a nil-store RBAC checker (e.g. add a function like
permissionSatisfiedHandler() that calls NewClusterHandler(nil, nil,
auth.NewRBACChecker(nil)) and returns the handler); replace the five direct
usages of NewClusterHandler(nil, nil, auth.NewRBACChecker(nil)) in the tests
with calls to this helper so the nil-store RBAC assumption is centralized and
easy to update (refer to NewClusterHandler and auth.NewRBACChecker(nil) when
locating the occurrences to change).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/src/internal/api/cluster_handlers_test.go`:
- Around line 203-208: Extract the repeated test setup into a small helper that
returns the handler pre-configured with a nil-store RBAC checker (e.g. add a
function like permissionSatisfiedHandler() that calls NewClusterHandler(nil,
nil, auth.NewRBACChecker(nil)) and returns the handler); replace the five direct
usages of NewClusterHandler(nil, nil, auth.NewRBACChecker(nil)) in the tests
with calls to this helper so the nil-store RBAC assumption is centralized and
easy to update (refer to NewClusterHandler and auth.NewRBACChecker(nil) when
locating the occurrences to change).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9d7b2eb3-eb94-4a75-a4e6-7dab4b446460

📥 Commits

Reviewing files that changed from the base of the PR and between 922c034 and c6c64b2.

📒 Files selected for processing (2)
  • server/src/internal/api/cluster_handlers_test.go
  • server/src/internal/api/rbac_issue207_clusters_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/src/internal/api/rbac_issue207_clusters_test.go

dpage and others added 2 commits May 11, 2026 16:08
- OpenAPI deleteClusterGroup wording reflects owner-fallback
- assertGatePassed also rejects 401 (auth regression)
- assertForbiddenWithMessage validates canonical message substring
- Clarify deleteClusterGroup datastore-read comment
- Extract permissionSatisfiedHandler helper

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per CodeRabbit nit on PR #217: locks the contract so a regression that
leaves the divider visible while hiding the items would still fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dpage dpage merged commit 27b3b1c into main May 12, 2026
15 checks passed
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.

Unauthorized Users Can Create Cluster Objects

1 participant