Skip to content

fix: gate the recent-activity fetch on audit:read - #69

Open
Altamimi-Dev wants to merge 1 commit into
contextforge-org:mainfrom
Altamimi-Dev:63-gate-recent-activity
Open

fix: gate the recent-activity fetch on audit:read#69
Altamimi-Dev wants to merge 1 commit into
contextforge-org:mainfrom
Altamimi-Dev:63-gate-recent-activity

Conversation

@Altamimi-Dev

Copy link
Copy Markdown
Contributor

Closes #63.

useMiniCardStatuses fetched /api/logs/activity unconditionally, so every caller without audit:read (every non-admin under the default backend roles, including team_admin) fired a guaranteed 403 on each home load. The failure is swallowed by the mini cards, so it only showed up as 403 noise in network and server logs.

This adds an enabled option to useRecentActivity, mirroring the flag useSystemHealth already has for the admin-only /version probe, and gates the home call on hasPermission("audit:read"). While disabled the hook makes no request and returns an empty feed; permissions load async, so the gate starts closed and the hook fetches when it opens. The gate checks audit:read only, not security:read, since the security-events half is additive server-side.

The gate lives in the hook rather than the call site so the upcoming ActivityView consumer gets it for free.

/api/logs/activity requires audit:read, which no default non-admin role
holds, so every non-admin home load fired a guaranteed 403. Add an
`enabled` option to useRecentActivity, mirroring useSystemHealth's flag
for the admin-only /version probe, and gate the home call on it.

Signed-off-by: Ahmad Al Tamimi <altamimi.dev@gmail.com>
@a-effort

a-effort commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Thanks for contributing! 🙌 No blockers; approving.

LLM-assisted feedback on improvements that could be included in this PR or added as followups:

Gate is correct, and the backend backs it up: log_search.py requires audit:read and folds security events in additively. One bug in the hook contract:

isLoading is never true for the fetch that happens: hasPermission fails closed while permissions load (AuthContext.tsx:256), so enabled starts false for every authenticated caller, not just non-admins. The disabled branch sets isLoading false; when the gate opens, the effect calls fetchOnce without setting it back. During the one real request the hook returns { isLoading: false, items: [] }.

Confirmed against this branch with a delayed handler:

rerender({ enabled: true });
IN FLIGHT -> { isLoading: false, items: 0 }

The mini cards only destructure items, so home is unaffected. It matters for the next consumer: ActivityView would render "no recent activity" and then pop into the list. Same on team switch: the permissions effect sets loading: true on selectedTeamId change, re-closing the gate.

     const controller = new AbortController();
+    setIsLoading(true);
     void fetchOnce(controller.signal);

Worth a comment on that line: it belongs in the effect body, not inside fetchOnce, or the 30s poll flashes a spinner on every tick.

Please add a test asserting isLoading is true between the rerender and the resolve; the current disabled-state test doesn't cover it

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

No blocking changes needed. 🚀

@a-effort a-effort mentioned this pull request Aug 22, 2026
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.

[BUG] Non-admins fire a guaranteed 403 on every home load: useRecentActivity isn't fetch-gated

2 participants