Skip to content

fix(settings): suppress permission-denied banner on Purchasing policies for non-admins - #981

Merged
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
fix/553-purchasing-policies-perm-error
Jun 9, 2026
Merged

fix(settings): suppress permission-denied banner on Purchasing policies for non-admins#981
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
fix/553-purchasing-policies-perm-error

Conversation

@cristim

@cristim cristim commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

  • On Admin > Purchasing policies, non-admin users saw a red error banner: Failed to load settings: permission denied: requires view on config whenever GET /api/config returned 403.
  • Since parts of the page (e.g. Exchange Automation) are already hidden for non-admins, the correct behaviour is graceful degradation: silently hide the section, no error banner.
  • This PR makes loadGlobalSettings detect 403 + "permission denied" errors and return early, leaving the section hidden. Unrelated 403s, 5xx, and network errors still surface the banner.

Closes #979

Changes

  • frontend/src/settings.ts: in the catch block of loadGlobalSettings, detect permission-denied errors (HTTP 403 AND message starts with "permission denied") and return early without showing #settings-error.
  • frontend/src/__tests__/settings-permissions.test.ts: 4 new tests covering the 403+permission path (no banner), unrelated 403s (banner shown), 500s (banner shown), and network errors (banner shown).

Test plan

  • npx jest --testPathPattern="settings-permissions" - 12 tests pass (4 new)
  • npx tsc --noEmit - no type errors
  • SKIP=terraform_validate pre-commit run --all-files - all hooks pass
  • Manual: navigate to Settings > Purchasing policies as a Standard (non-admin) user - no red error banner, section is hidden/read-only
  • Manual: verify real errors (e.g. disconnect backend) still show the error banner

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling on the Purchasing policies page so permission-denied responses hide loading indicators and avoid showing generic error banners, enabling graceful UI degradation for non-admin users.
  • Tests

    • Added comprehensive tests to verify UI behavior for permission-denied, other authorization, server, and network error scenarios.

…es for non-admins (closes #979)

When GET /api/config returns HTTP 403 with a "permission denied" error,
loadGlobalSettings now returns early instead of rendering the red error
banner. The section stays hidden and the page degrades gracefully,
matching the existing Exchange Automation hide-for-non-admins pattern.
Non-permission 403s, 5xx, and network failures still surface the banner.

Four new tests in settings-permissions.test.ts cover: the 403+permission
path (no banner), unrelated 403s (banner shown), 500s (banner shown),
and network errors (banner shown).
@cristim cristim added triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/this-quarter Within the quarter impact/few Limited audience type/bug Defect labels Jun 5, 2026
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Need an answer fast? Review this PR in Change Stack to ask focused questions about the PR or a changed range.

Review Change Stack

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 4 minutes and 7 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4812315-3249-4e99-9011-f92d3414af15

📥 Commits

Reviewing files that changed from the base of the PR and between 9a9f047 and ee2ac50.

📒 Files selected for processing (1)
  • frontend/src/settings.ts
📝 Walkthrough

Walkthrough

Detects backend 403 "permission denied" responses during global settings load and hides the loading UI without showing the generic error banner; other failures (other 403s, 500s, network errors) continue to show the error banner.

Changes

Graceful permission-denied error handling

Layer / File(s) Summary
Implementation: permission-denied error handler
frontend/src/settings.ts
Adds isPermissionDeniedError(error) and updates loadGlobalSettings() to hide #settings-loading and return early when a 403 with a "permission denied" message is detected; other errors still log and show #settings-error.
Test coverage: graceful degradation scenarios
frontend/src/__tests__/settings-permissions.test.ts
Adds a describe suite for issue #979 with a helper to create status-bearing errors, a beforeEach that resets mocks/DOM and calls resetConfigCache, and tests asserting UI outcomes for permission-denied 403, other 403s, 500s, and network errors.

Sequence Diagram(s)

sequenceDiagram
  participant loadGlobalSettings
  participant BackendAPI
  participant DOM
  loadGlobalSettings->>BackendAPI: GET /api/config
  BackendAPI-->>loadGlobalSettings: 403 "permission denied" or other error
  alt permission denied (403)
    loadGlobalSettings->>DOM: hide `#settings-loading`
    loadGlobalSettings-->>DOM: do not show `#settings-error`
  else other error (500, network, 403 other)
    loadGlobalSettings->>DOM: hide `#settings-loading`
    loadGlobalSettings->>DOM: show `#settings-error` with message
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • LeanerCloud/CUDly#580: Also modifies frontend/src/settings.ts's loadGlobalSettings() to change how config data is processed and populated.

Suggested labels

effort/s

Poem

🐰 A hop, a sniff, a quiet mend,
When access stops, the banners end.
The spinner fades, no alarmed cry —
Non-admins pass the page nearby.
Small, polite, the UI sighs.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% 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 The title clearly and concisely describes the main change: suppressing the permission-denied error banner on the Purchasing policies page for non-admin users, which matches the core fix in the changeset.
Linked Issues check ✅ Passed The changes fully satisfy issue #979 requirements: HTTP 403 errors with 'permission denied' messages are detected and the error banner is suppressed, while other errors (500s, network) continue to show the banner.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #979: the helper function and settings logic address permission-denied error handling, and tests verify the behavior for both permission and non-permission errors.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/553-purchasing-policies-perm-error

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

@cristim

cristim commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 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 commented Jun 6, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 6, 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.

🧹 Nitpick comments (1)
frontend/src/settings.ts (1)

2984-2986: 💤 Low value

Consider extracting the permission-denied check to a helper function.

The inline boolean expression is correct, but extracting it would improve readability and make the intent clearer. For example:

function isPermissionDeniedError(error: unknown): boolean {
  return (error as { status?: number }).status === 403
    && error instanceof Error
    && error.message.startsWith('permission denied');
}

Then the catch block would read:

if (isPermissionDeniedError(error)) {
  if (loadingEl) loadingEl.classList.add('hidden');
  return;
}
🤖 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 2984 - 2986, Extract the inline
permission-check into a small helper named isPermissionDeniedError(error:
unknown): boolean that implements the current logic ((error as { status?: number
}).status === 403 && error instanceof Error &&
error.message.startsWith('permission denied')), then replace the inline boolean
in the catch block with a call to isPermissionDeniedError(error) so the catch
reads: if (isPermissionDeniedError(error)) { if (loadingEl)
loadingEl.classList.add('hidden'); return; } to improve readability and intent.
🤖 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 `@frontend/src/settings.ts`:
- Around line 2984-2986: Extract the inline permission-check into a small helper
named isPermissionDeniedError(error: unknown): boolean that implements the
current logic ((error as { status?: number }).status === 403 && error instanceof
Error && error.message.startsWith('permission denied')), then replace the inline
boolean in the catch block with a call to isPermissionDeniedError(error) so the
catch reads: if (isPermissionDeniedError(error)) { if (loadingEl)
loadingEl.classList.add('hidden'); return; } to improve readability and intent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 325a5689-83b6-4c69-88c1-aaa24772a880

📥 Commits

Reviewing files that changed from the base of the PR and between 20ce559 and e89a55f.

📒 Files selected for processing (2)
  • frontend/src/__tests__/settings-permissions.test.ts
  • frontend/src/settings.ts

Pull the inline 403 + "permission denied" check out of the catch block
into a small helper so the catch reads as a single intent-revealing
line. Behavior unchanged; existing settings-permissions tests cover
the contract.
@cristim

cristim commented Jun 6, 2026

Copy link
Copy Markdown
Member Author

Addressed CR pass-1 finding:

  • Extracted inline 403 + "permission denied" check into isPermissionDeniedError helper in frontend/src/settings.ts. Behavior unchanged; existing settings-permissions tests cover the contract.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the current state of settings.ts to verify the isPermissionDeniedError helper
cat frontend/src/settings.ts

Length of output: 145107



@cristim The refactor looks clean — isPermissionDeniedError is well-documented, properly scoped (module-private), and the behavior is unchanged from the inline version. One trivial style observation: the type assertion (error as { status?: number }).status === 403 precedes the instanceof Error narrowing, which is the reverse of the conventional TypeScript idiom (narrow the type first, then access properties). It works correctly due to short-circuit evaluation, but swapping the operand order would be more idiomatic:

function isPermissionDeniedError(error: unknown): boolean {
  return error instanceof Error
    && (error as { status?: number }).status === 403
    && error.message.startsWith('permission denied');
}

That said, this is purely cosmetic — happy to trigger the review as-is.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.
✅ 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.

…nDeniedError (#981 CR)

Reorder the guard so the `instanceof Error` narrowing precedes the
`.status` type assertion, matching the conventional TypeScript idiom
(narrow first, then access properties). Behavior is unchanged thanks to
short-circuit evaluation; this addresses the CodeRabbit style nitpick.
@cristim

cristim commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

Addressed the remaining CR style nitpick: reordered the guard in isPermissionDeniedError so the instanceof Error narrowing precedes the .status type assertion (idiomatic narrow-first order). Behavior is unchanged. Pushed as ee2ac50.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@cristim Perfect — that's exactly the idiomatic ordering suggested. The isPermissionDeniedError helper is now cleanly structured: narrow the type first with instanceof Error, then access the narrowed properties. Triggering a fresh review pass on the updated commit.

✅ 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 31d83e7 into feat/multicloud-web-frontend Jun 9, 2026
5 checks passed
@cristim
cristim deleted the fix/553-purchasing-policies-perm-error branch July 27, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

impact/few Limited audience priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant