Summary
useMiniCardStatuses calls useRecentActivity unconditionally, so every non-admin user fires a request to GET /api/logs/activity on home page load that is guaranteed to 403.
The fix for this already exists four lines above it in the same file; it just wasn't applied to the activity call.
Current code
src/hooks/useMiniCardStatuses.ts:73-78:
const canViewSystem = hasPermission("admin.system_config");
const systemHealth = useSystemHealth(undefined, canViewSystem); // ← fetch-gated
const { data: mcpServers, error: mcpServersError } = useQuery<ServersResponse>(MCP_REACH_PATH);
const { data: a2aAgents, error: a2aError } = useQuery<Activatable[]>(A2A_REACH_PATH);
const { items } = useRecentActivity({ pollIntervalMs: 0 }); // ← NOT gated
useSystemHealth takes an enabled flag precisely for this. Its doc comment:
enabled — When false, no request is made at all. Callers use this to fetch-gate /version (admin-only) so non-admins never spam it with 403s.
useRecentActivity has no equivalent parameter.
Why it 403s
GET /api/logs/activity requires audit:read (mcpgateway/routers/log_search.py:834). Checked against the backend's default roles in bootstrap_db.py:350:
| role |
scope |
perms |
has audit:read? |
platform_admin |
global |
["*"] |
yes, via wildcard |
team_admin |
team |
42 |
no |
developer |
team |
38 |
no |
viewer |
team |
17 |
no |
platform_viewer |
global |
17 |
no |
Neither audit:read nor security:read appears in any non-admin role. The only other way in is the is_admin bypass at permission_service.py:130. Every non-admin 403s, including team_admin.
Impact
- A guaranteed-failing request on every home load for the majority of users.
- The error is swallowed (the mini-card counts just stay 0), so it is invisible in the UI and only shows up as 403 noise in network/server logs.
Fix
Add an enabled option to useRecentActivity, mirroring useSystemHealth, and gate the call:
const canViewActivity = hasPermission("audit:read");
const { items } = useRecentActivity({ pollIntervalMs: 0, enabled: canViewActivity });
hasPermission (src/auth/AuthContext.tsx:251-259) is an exact string match plus *, so the colon form audit:read needs no special handling despite differing from dot-form admin.system_config.
Gate on audit:read only, not also security:read. The security-events half is additive server-side, so a caller with just audit:read gets a valid, narrower feed rather than an error.
Why in the hook, not the caller
ActivityView (not yet built) will be the second consumer of this hook and needs the same gate. Putting enabled in useRecentActivity fixes both call sites at once.
Related
- Blocks / overlaps the
ActivityView build
- See also the
is_admin vs /rbac/my/permissions asymmetry issue, which describes a known edge in this gating approach
Summary
useMiniCardStatusescallsuseRecentActivityunconditionally, so every non-admin user fires a request toGET /api/logs/activityon home page load that is guaranteed to 403.The fix for this already exists four lines above it in the same file; it just wasn't applied to the activity call.
Current code
src/hooks/useMiniCardStatuses.ts:73-78:useSystemHealthtakes anenabledflag precisely for this. Its doc comment:useRecentActivityhas no equivalent parameter.Why it 403s
GET /api/logs/activityrequiresaudit:read(mcpgateway/routers/log_search.py:834). Checked against the backend's default roles inbootstrap_db.py:350:audit:read?platform_admin["*"]team_admindeveloperviewerplatform_viewerNeither
audit:readnorsecurity:readappears in any non-admin role. The only other way in is theis_adminbypass atpermission_service.py:130. Every non-admin 403s, includingteam_admin.Impact
Fix
Add an
enabledoption touseRecentActivity, mirroringuseSystemHealth, and gate the call:hasPermission(src/auth/AuthContext.tsx:251-259) is an exact string match plus*, so the colon formaudit:readneeds no special handling despite differing from dot-formadmin.system_config.Gate on
audit:readonly, not alsosecurity:read. The security-events half is additive server-side, so a caller with justaudit:readgets a valid, narrower feed rather than an error.Why in the hook, not the caller
ActivityView(not yet built) will be the second consumer of this hook and needs the same gate. PuttingenabledinuseRecentActivityfixes both call sites at once.Related
ActivityViewbuildis_adminvs/rbac/my/permissionsasymmetry issue, which describes a known edge in this gating approach