Skip to content

fix(auth): grant view:config to non-admin groups; disable settings fields for read-only users - #1428

Merged
cristim merged 1 commit into
mainfrom
fix/qa-nonadmin-settings-rbac
Jul 17, 2026
Merged

cristim merged 1 commit into
mainfrom
fix/qa-nonadmin-settings-rbac

Conversation

@cristim

@cristim cristim commented Jul 16, 2026 •

Copy link
Copy Markdown
Member

Summary

Three cascading bugs all rooted in a single missing permission grant: Standard Users and Read-Only Users lacked view:config, so GET /api/config and GET /api/ri-exchange/config returned 403 for them.

Changes

Backend / data

  • internal/auth/types.go: add view:config to DefaultUserPermissions() and DefaultReadOnlyPermissions(). update:config remains admin-only via admin:*.
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.{up,down}.sql: SQL UPDATE with idempotent NOT EXISTS guard for groups 00000000-...-0005 and 00000000-...-0006.

Frontend

  • frontend/src/permissions.generated.ts: regenerated to include view:config in USER_PERMS and READONLY_PERMS.
  • frontend/src/settings.ts: export isPermissionDeniedError for reuse by sibling modules.
  • frontend/src/riexchange.ts: graceful-degrade on permission-denied in loadAutomationSettings.

Tests (fail-before / pass-after)

  • internal/auth/types_test.go: updated counts (11->12 user, 3->4 read-only) + explicit view:config/update:config assertions.
  • internal/auth/service_group_test.go: updated three count assertions and the "missing groups" test.
  • internal/api/handler_config_test.go: new test — view:config holder without update:config gets 403 on PUT.
  • internal/database/postgres/migrations/000088_..._test.go: integration tests (grant, write-gate exclusion, sibling preservation, down migration, idempotency).
  • frontend/__tests__/permissions.test.ts: updated expected sets to include view:config; renamed readonly describe.
  • frontend/__tests__/settings-permissions.test.ts: 6 new tests — form visible + inputs disabled after successful getConfig for standard/read-only users; Purchasing Policies fields disabled; Save/Reset hidden.
  • frontend/__tests__/riexchange-automation-settings.test.ts: 5 new tests for permission-denied graceful degradation vs real errors.

Test plan

  • go test ./... passes (5728 tests)
  • golangci-lint run ./... (v2.10.1) passes
  • gocyclo -over 10 . produces no new functions
  • npm test passes (2579 tests, 79 suites)
  • npm run build succeeds

Closes #1401
Closes #1410
Closes #1413

Summary by CodeRabbit

  • New Features
    • Standard and read-only users can now view global configuration settings in a read-only mode; configuration editing remains admin-only.
    • Purchasing Policy controls remain read-only/disabled for non-admin users.
    • Automation settings access-denied due to insufficient permissions is now hidden without showing an error message or retry option.
  • Bug Fixes
    • Improved permission-denied handling for automation settings and ensured configuration UI permission gating stays consistent across roles.
  • Tests
    • Added/updated frontend, backend, and database migration tests to cover the new permission and rollback behavior.

@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/s Hours type/bug Defect labels Jul 16, 2026
@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66743466-20fb-439f-a240-f0ef88006fbf

📥 Commits

Reviewing files that changed from the base of the PR and between f0a9468 and 18674ca.

⛔ Files ignored due to path filters (1)
  • frontend/src/permissions.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (12)
  • frontend/src/__tests__/permissions.test.ts
  • frontend/src/__tests__/riexchange-automation-settings.test.ts
  • frontend/src/__tests__/settings-permissions.test.ts
  • frontend/src/riexchange.ts
  • frontend/src/settings.ts
  • internal/api/handler_config_test.go
  • internal/auth/service_group_test.go
  • internal/auth/types.go
  • internal/auth/types_test.go
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.down.sql
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.up.sql
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups_test.go
📝 Walkthrough

Walkthrough

Non-admin roles now receive view:config through defaults and migration while remaining unable to update configuration. Settings tests cover read-only visibility and Purchasing Policies controls, and Exchange Automation silently hides on permission-denied errors.

Changes

Configuration permissions

Layer / File(s) Summary
Grant config read permission
internal/auth/types.go, internal/database/postgres/migrations/...
Standard and Read-Only permissions include view:config; migration scripts add and remove the grant idempotently.
Validate permission propagation
internal/auth/*_test.go, internal/api/handler_config_test.go, internal/database/postgres/migrations/*test.go
Tests verify permission counts, migration rollback and idempotency, preserved permissions, and rejection of update:config without persistence calls.

Frontend settings behavior

Layer / File(s) Summary
Apply non-admin settings gating
frontend/src/__tests__/permissions.test.ts, frontend/src/__tests__/settings-permissions.test.ts
Tests cover view:config, denied config writes, visible populated global settings, and disabled or hidden Purchasing Policies controls.
Handle denied automation settings
frontend/src/settings.ts, frontend/src/riexchange.ts, frontend/src/__tests__/riexchange-automation-settings.test.ts
isPermissionDeniedError is exported; denied automation failures clear the section without an error or retry button, while other failures retain both.

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

Sequence Diagram(s)

sequenceDiagram
  participant loadAutomationSettings
  participant getRIExchangeConfig
  participant isPermissionDeniedError
  participant AutomationSettingsContainer
  loadAutomationSettings->>getRIExchangeConfig: request Exchange Automation config
  getRIExchangeConfig-->>loadAutomationSettings: permission-denied error
  loadAutomationSettings->>isPermissionDeniedError: classify error
  isPermissionDeniedError-->>loadAutomationSettings: true
  loadAutomationSettings->>AutomationSettingsContainer: clear content without error or retry
Loading

Possibly related PRs

Suggested labels: type/security

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title concisely describes the main auth/settings permission changes and matches the PR scope.
Linked Issues check ✅ Passed The changes implement all three linked fixes: read-only config visibility, non-interactive Purchasing Policies, and suppression of permission-denied errors.
Out of Scope Changes check ✅ Passed No obvious unrelated code changes are shown; the touched files all support permissions, settings gating, or related tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/qa-nonadmin-settings-rbac

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: 2

🤖 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 `@internal/api/handler_config_test.go`:
- Around line 1247-1271: Add mockAuth.AssertExpectations(t) after the
updateConfig assertions so the test verifies the expected HasPermissionAPI call
for "update", "config" was made, while preserving the existing store-not-called
assertions.

In
`@internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.down.sql`:
- Around line 8-25: Update migration 000088 so the up migration records which
Standard Users and Read-Only Users groups actually received the view:config
grant, and the down migration removes only those recorded grants rather than
every matching permission. Add regression coverage for groups that already had
view:config before the up migration, verifying rollback preserves their
pre-existing permission.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: d72e3141-15fd-49c4-b561-6b1ef50c9da5

📥 Commits

Reviewing files that changed from the base of the PR and between cd67485 and b757853.

⛔ Files ignored due to path filters (1)
  • frontend/src/permissions.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (12)
  • frontend/src/__tests__/permissions.test.ts
  • frontend/src/__tests__/riexchange-automation-settings.test.ts
  • frontend/src/__tests__/settings-permissions.test.ts
  • frontend/src/riexchange.ts
  • frontend/src/settings.ts
  • internal/api/handler_config_test.go
  • internal/auth/service_group_test.go
  • internal/auth/types.go
  • internal/auth/types_test.go
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.down.sql
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.up.sql
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups_test.go

Comment on lines +1247 to +1271
mockAuth.On("ValidateSession", ctx, "user-token").Return(userSession, nil)
// Grant view:config so the session itself is valid (GET would succeed).
mockAuth.On("HasPermissionAPI", ctx, userSession.UserID, "view", "config").Return(true, nil).Maybe()
// Deny update:config — this is the permission PUT /api/config checks.
mockAuth.On("HasPermissionAPI", ctx, userSession.UserID, "update", "config").Return(false, nil)
// Deny admin:* so requireAdmin also fails (belt-and-suspenders: the route
// is AuthAdmin-gated, but the handler adds its own requirePermission check).
mockAuth.On("HasPermissionAPI", ctx, userSession.UserID, mock.AnythingOfType("string"), mock.AnythingOfType("string")).
Return(false, nil).Maybe()

handler := &Handler{config: mockStore, auth: mockAuth}

req := &events.LambdaFunctionURLRequest{
Headers: map[string]string{"Authorization": "Bearer user-token"},
Body: `{"default_term": 1}`,
}
result, err := handler.updateConfig(ctx, req)
require.Error(t, err, "updateConfig must be rejected for a user who lacks update:config")
assert.Nil(t, result)
ce, ok := IsClientError(err)
require.True(t, ok, "expected ClientError, got %T: %v", err, err)
assert.Equal(t, 403, ce.code, "expected 403, got %d", ce.code)
// The store must never be reached — the permission check fires first.
mockStore.AssertNotCalled(t, "GetGlobalConfig", mock.Anything)
mockStore.AssertNotCalled(t, "SaveGlobalConfig", mock.Anything, mock.Anything)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A55 -B5 'func TestHandler_updateConfig_RequiresUpdateConfigPermission' \
  internal/api/handler_config_test.go

rg -n -P -A70 -B10 'func \(.*\) updateConfig\s*\(' internal/api

Repository: LeanerCloud/CUDly

Length of output: 9069


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the authorization helpers and the mock implementation used by this test.
rg -n -A80 -B20 'func \(h \*Handler\) requirePermission|func \(h \*Handler\) requireAdmin|type MockAuthService|HasPermissionAPI' internal/api

# If the mock is in a generated test file, show the relevant section with line numbers.
files=$(git ls-files 'internal/api/*test.go' 'internal/api/*mock*.go' 'internal/api/*_mock.go')
printf '%s\n' "$files" | sed -n '1,120p'

Repository: LeanerCloud/CUDly

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect testify/mock expectation matching in the repository's version if vendored code exists.
rg -n -A40 -B10 'func \(.*\) AssertExpectations|func \(.*\) MethodCalled|ExpectedCalls' . \
  -g '*/mock*.go' -g '*testify*' -g 'go.mod' -g 'go.sum'

Repository: LeanerCloud/CUDly

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- requirePermission / requireAdmin / updateConfig ---'
rg -n -A80 -B20 'func \(h \*Handler\) requirePermission|func \(h \*Handler\) requireAdmin|func \(h \*Handler\) updateConfig' internal/api/handler_config.go internal/api/*.go

echo '--- MockAuthService ---'
rg -n -A120 -B20 'type MockAuthService|HasPermissionAPI|ValidateSession' internal/api/*test.go internal/api/*mock*.go internal/api/*_mock.go

echo '--- testify/mock matching references if present in repo ---'
rg -n -A40 -B10 'AssertExpectations|MethodCalled|ExpectedCalls' . -g '*mock*.go' -g '*test.go' -g 'go.mod' -g 'go.sum'

Repository: LeanerCloud/CUDly

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the auth helper and the test mock used in handler_config_test.go.
sed -n '1,220p' internal/api/handler_config.go

echo '---'
sed -n '1,260p' internal/api/handler_config_test.go

Repository: LeanerCloud/CUDly

Length of output: 19536


Assert the update:config call
mockAuth.AssertExpectations(t) is missing, so the exact HasPermissionAPI(..., "update", "config") expectation is never enforced and this test can still pass on a different 403 path before the write-permission check.

🤖 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 `@internal/api/handler_config_test.go` around lines 1247 - 1271, Add
mockAuth.AssertExpectations(t) after the updateConfig assertions so the test
verifies the expected HasPermissionAPI call for "update", "config" was made,
while preserving the existing store-not-called assertions.

Comment on lines +8 to +25
UPDATE groups
SET
permissions = (
SELECT COALESCE(
jsonb_agg(elem ORDER BY elem->>'action', elem->>'resource'),
'[]'::jsonb
)
FROM jsonb_array_elements(permissions) AS elem
WHERE NOT (
elem->>'action' = 'view'
AND elem->>'resource' = 'config'
)
),
updated_at = NOW()
WHERE id IN (
'00000000-0000-5000-8000-000000000005', -- Standard Users
'00000000-0000-5000-8000-000000000006' -- Read-Only Users
);

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve pre-existing view:config grants on rollback.

Line 16 removes the permission even when the up migration did not add it: Line 26 of the up migration skips groups that already have the grant. Rolling back can therefore revoke a manually pre-existing permission. Record which grants 000088 introduced and remove only those; add an up/down regression test for this case.

🤖 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
`@internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.down.sql`
around lines 8 - 25, Update migration 000088 so the up migration records which
Standard Users and Read-Only Users groups actually received the view:config
grant, and the down migration removes only those recorded grants rather than
every matching permission. Add regression coverage for groups that already had
view:config before the up migration, verifying rollback preserves their
pre-existing permission.

cristim added a commit that referenced this pull request Jul 17, 2026
Expand-contract rename of all British-spelled 'cancelled'/'cancellable'
variants to US-spelled 'canceled'/'cancelable' across the codebase.

- Add migration 000089: adds canceled_by column alongside cancelled_by,
  widens CHECK constraints to accept both spellings, COALESCE reads both
  during the expand window (prev version 088)
- Rename field CancelledBy->CanceledBy in PurchaseExecution, update
  json tag to canceled_by; add IsImmediatelyCancelable() predicate
- Rename SetCancelledBy->SetCanceledBy in StoreInterface + all
  implementations and mocks
- Update handler_purchases, handler_purchases_revoke and all tests to
  use US spellings; replace em-dashes with double hyphens in comments
- Patch frontend history.ts, riexchange.ts and OpenAPI spec to use
  'canceled' status string

Rebased onto main (473f69b); migration renumbered from 000082 to
000089 to land after in-flight #808 (000087) and #1428 (000088).
@cristim
cristim force-pushed the fix/qa-nonadmin-settings-rbac branch from b757853 to fb736c4 Compare July 17, 2026 08:51
cristim added a commit that referenced this pull request Jul 17, 2026
Expand-contract rename of all British-spelled 'cancelled'/'cancellable'
variants to US-spelled 'canceled'/'cancelable' across the codebase.

- Add migration 000089: adds canceled_by column alongside cancelled_by,
  widens CHECK constraints to accept both spellings, COALESCE reads both
  during the expand window (prev version 088)
- Rename field CancelledBy->CanceledBy in PurchaseExecution, update
  json tag to canceled_by; add IsImmediatelyCancelable() predicate
- Rename SetCancelledBy->SetCanceledBy in StoreInterface + all
  implementations and mocks
- Update handler_purchases, handler_purchases_revoke and all tests to
  use US spellings; replace em-dashes with double hyphens in comments
- Patch frontend history.ts, riexchange.ts and OpenAPI spec to use
  'canceled' status string

Rebased onto main (473f69b); migration renumbered from 000082 to
000089 to land after in-flight #808 (000087) and #1428 (000088).
@cristim

cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Rebase complete onto current main (7f0573d, includes #1436 migration-test-pin fix)

Branch fix/qa-nonadmin-settings-rbac was already based on top of origin/main HEAD -- no rebase commits needed. Push was a no-op (up-to-date).

Migration 000088 confirmed present. Local gate results:

  • go build ./... -- exit 0
  • go vet ./... -- exit 0
  • Unit tests (internal/api, internal/auth): 2315 passed -- exit 0
  • go test ./internal/server/scheduledauth/ -count=5: 275 passed -- exit 0
  • Integration TestMigration_GrantViewConfigToNonAdminGroups (testcontainer PG): 6 passed -- exit 0
  • golangci-lint run ./... (v2.11.4, CI uses v2.10.1): no issues -- exit 0

@cristim
cristim force-pushed the fix/qa-nonadmin-settings-rbac branch from fb736c4 to f0a9468 Compare July 17, 2026 12:30
cristim added a commit that referenced this pull request Jul 17, 2026
Expand-contract rename of all British-spelled 'cancelled'/'cancellable'
variants to US-spelled 'canceled'/'cancelable' across the codebase.

- Add migration 000089: adds canceled_by column alongside cancelled_by,
  widens CHECK constraints to accept both spellings, COALESCE reads both
  during the expand window (prev version 088)
- Rename field CancelledBy->CanceledBy in PurchaseExecution, update
  json tag to canceled_by; add IsImmediatelyCancelable() predicate
- Rename SetCancelledBy->SetCanceledBy in StoreInterface + all
  implementations and mocks
- Update handler_purchases, handler_purchases_revoke and all tests to
  use US spellings; replace em-dashes with double hyphens in comments
- Patch frontend history.ts, riexchange.ts and OpenAPI spec to use
  'canceled' status string

Rebased onto main (473f69b); migration renumbered from 000082 to
000089 to land after in-flight #808 (000087) and #1428 (000088).
@cristim

cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/settings.ts (1)

1266-1302: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Actually retain and replace the previous delete-toast handle.

The comment says each delete replaces an earlier toast, but this code ignores the ToastHandle returned by showToast. Since showToast appends every toast, rapid deletes can leave multiple Undo actions visible.

Store the handle and dismiss the previous one before creating the next toast.

Suggested adjustment
-        showToast({
+        activeDeleteToast?.dismiss();
+        activeDeleteToast = showToast({

Declare activeDeleteToast at module scope with the appropriate ToastHandle type.

🤖 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 `@frontend/src/settings.ts` around lines 1266 - 1302, Update the delete flow
around showToast to retain the returned ToastHandle and prevent stacked undo
toasts. Declare an activeDeleteToast variable at module scope with the existing
ToastHandle type, dismiss and replace it before showing a new delete toast, and
assign the new handle to it.
🤖 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 `@internal/auth/types.go`:
- Around line 543-551: Update the comment above the ActionView/ResourceConfig
permission to scope the default to Standard users instead of every authenticated
user. State that Read-Only users receive this permission through the separate
default below, while preserving the existing read-only and SourceIdentity
details.

---

Outside diff comments:
In `@frontend/src/settings.ts`:
- Around line 1266-1302: Update the delete flow around showToast to retain the
returned ToastHandle and prevent stacked undo toasts. Declare an
activeDeleteToast variable at module scope with the existing ToastHandle type,
dismiss and replace it before showing a new delete toast, and assign the new
handle to it.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7f4c00e-ca70-4130-bc32-40c3432a12c9

📥 Commits

Reviewing files that changed from the base of the PR and between b757853 and f0a9468.

⛔ Files ignored due to path filters (1)
  • frontend/src/permissions.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (12)
  • frontend/src/__tests__/permissions.test.ts
  • frontend/src/__tests__/riexchange-automation-settings.test.ts
  • frontend/src/__tests__/settings-permissions.test.ts
  • frontend/src/riexchange.ts
  • frontend/src/settings.ts
  • internal/api/handler_config_test.go
  • internal/auth/service_group_test.go
  • internal/auth/types.go
  • internal/auth/types_test.go
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.down.sql
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.up.sql
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups_test.go
🚧 Files skipped from review as they are similar to previous changes (10)
  • internal/auth/service_group_test.go
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.up.sql
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups.down.sql
  • frontend/src/riexchange.ts
  • frontend/src/tests/permissions.test.ts
  • internal/api/handler_config_test.go
  • internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups_test.go
  • internal/auth/types_test.go
  • frontend/src/tests/settings-permissions.test.ts
  • frontend/src/tests/riexchange-automation-settings.test.ts

Comment thread internal/auth/types.go
…elds for read-only users

Root cause for issues #1401, #1410, and #1413: GET /api/config and
GET /api/ri-exchange/config both require view:config, but Standard
Users and Read-Only Users lacked this permission. The cascade:
- #1401: only the section header rendered; the config form was never
  shown because loadGlobalSettings returned early on 403.
- #1410: applyReadOnlySettings was never called, so Purchasing
  Policies inputs stayed enabled for non-admin sessions.
- #1413: a "permission denied" error paragraph appeared inside the
  Exchange Automation container instead of degrading gracefully.

Fix:
- internal/auth/types.go: add view:config to DefaultUserPermissions()
  and DefaultReadOnlyPermissions() (write gate update:config is still
  admin-only).
- migration 000088: SQL UPDATE grants view:config to Standard Users
  (00000000-...-0005) and Read-Only Users (00000000-...-0006) with an
  idempotent NOT EXISTS guard; down migration removes it via jsonb_agg.
- frontend/src/permissions.generated.ts: regenerated to include
  view:config in USER_PERMS and READONLY_PERMS.
- frontend/src/settings.ts: export isPermissionDeniedError so sibling
  modules can reuse it without duplicating the detection logic.
- frontend/src/riexchange.ts: graceful-degrade on permission denied
  in loadAutomationSettings (clears container, no error paragraph).

Tests:
- internal/auth/types_test.go + service_group_test.go: updated counts
  (11 -> 12 for users, 3 -> 4 for read-only) plus positive view:config
  and negative update:config assertions.
- internal/api/handler_config_test.go: new test asserts updateConfig
  returns 403 for a session that holds view:config but not update:config.
- internal/database/postgres/migrations/000088_..._test.go: integration
  tests cover grant, write-gate exclusion, sibling preservation, down
  migration, and idempotency.
- frontend/__tests__/permissions.test.ts: updated expected permission
  sets to include view:config; renamed readonly describe to reflect
  the 4-permission set.
- frontend/__tests__/settings-permissions.test.ts: 6 new tests verify
  the form is visible (not just the header) after getConfig succeeds
  for standard and read-only users, and that Purchasing Policies inputs
  are disabled.
- frontend/__tests__/riexchange-automation-settings.test.ts: new file
  with 5 tests covering permission-denied graceful degradation.

Closes #1401
Closes #1410
Closes #1413
@cristim
cristim force-pushed the fix/qa-nonadmin-settings-rbac branch from f0a9468 to 18674ca Compare July 17, 2026 13:09
@cristim

cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit 6862dea into main Jul 17, 2026
20 checks passed
cristim added a commit that referenced this pull request Jul 17, 2026
Expand-contract rename of all British-spelled 'cancelled'/'cancellable'
variants to US-spelled 'canceled'/'cancelable' across the codebase.

- Add migration 000089: adds canceled_by column alongside cancelled_by,
  widens CHECK constraints to accept both spellings, COALESCE reads both
  during the expand window (prev version 088)
- Rename field CancelledBy->CanceledBy in PurchaseExecution, update
  json tag to canceled_by; add IsImmediatelyCancelable() predicate
- Rename SetCancelledBy->SetCanceledBy in StoreInterface + all
  implementations and mocks
- Update handler_purchases, handler_purchases_revoke and all tests to
  use US spellings; replace em-dashes with double hyphens in comments
- Patch frontend history.ts, riexchange.ts and OpenAPI spec to use
  'canceled' status string

Rebased onto main (473f69b); migration renumbered from 000082 to
000089 to land after in-flight #808 (000087) and #1428 (000088).
cristim added a commit that referenced this pull request Jul 17, 2026
… spelling) (#1277)

* fix(db): rename cancelled->canceled (expand-contract, migration 000089)

Expand-contract rename of all British-spelled 'cancelled'/'cancellable'
variants to US-spelled 'canceled'/'cancelable' across the codebase.

- Add migration 000089: adds canceled_by column alongside cancelled_by,
  widens CHECK constraints to accept both spellings, COALESCE reads both
  during the expand window (prev version 088)
- Rename field CancelledBy->CanceledBy in PurchaseExecution, update
  json tag to canceled_by; add IsImmediatelyCancelable() predicate
- Rename SetCancelledBy->SetCanceledBy in StoreInterface + all
  implementations and mocks
- Update handler_purchases, handler_purchases_revoke and all tests to
  use US spellings; replace em-dashes with double hyphens in comments
- Patch frontend history.ts, riexchange.ts and OpenAPI spec to use
  'canceled' status string

Rebased onto main (473f69b); migration renumbered from 000082 to
000089 to land after in-flight #808 (000087) and #1428 (000088).

* fix(db): restore correct cancel error paths lost in rebase conflict

The rebase of 000089 onto current main incorrectly resolved two conflicts
in handler_purchases.go:

1. cancelOrRecoverExecution: reverted fmt.Errorf (router->500) back to
   NewClientError(409,...), misclassifying retriable backend faults as
   caller faults (feedback_http_status_classification).

2. cancelPurchaseViaSession: took main's broad !IsCancelable() guard
   instead of the PR's narrower guardImmediatelyCancelable, allowing
   "scheduled" executions through the pending/notified-only CAS path
   and producing a misleading "concurrent operation" 409 instead of the
   clear "use the revoke endpoint" message.

Fix: restore fmt.Errorf for the backend-failure branch; add
guardCancelableViaSession that explicitly routes "scheduled" to the
revoke endpoint before falling through to IsCancelable; rename residual
cancelledBy -> canceledBy; remove extra blank line in types.go.

Regression tests: TestHandler_deletePlannedPurchase_BackendErrorReturns5xx
and TestHandler_cancelPurchase_Session_ScheduledRoutedToRevoke now pass.
@cristim
cristim deleted the fix/qa-nonadmin-settings-rbac branch July 27, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment