Skip to content

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

Description

@a-effort

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions