Skip to content

Plugin frontend tabs filtered out for admin users — gate uses user.permissions but role.permissions hold the perm #390

@alfonsokuen

Description

@alfonsokuen

Summary

PegaProx 0.9.9.3 dashboard's plugin frontend tab filter (added in #381) gates rendering on user.permissions.includes('plugins.view'), but for built-in admin users that personal permissions field is empty and the plugins.view permission lives only on the admin role definition. Result: admins never see plugin frontend tabs (or any other gated tab — site-recovery, plugins, compliance, plugin:*).

Reproduction

Tested on a clean PegaProx 0.9.9.3 install (LXC, archive update path).

  1. Install any plugin that declares has_frontend: true + frontend_route: \"ui\" in its manifest. I used pegaprox-docker-swarm v1.16.0 which exists specifically to be [Feature] Plugin system: support embedded frontend UIs (iframe tab) + expose has_frontend in API #381-ready.
  2. Enable the plugin (Settings → Plugins → Rescan → Enable, or INSERT INTO plugin_state VALUES('docker_swarm', 1, ...)).
  3. Restart PegaProx, log in as a built-in admin user.
  4. Expected: a "Docker Swarm Manager" tab appears between the Plugins tab and Settings tab.
  5. Actual: no tab appears.

Diagnosis (verified end-to-end via Playwright + API + source)

/api/plugins returns the plugin with the correct shape:
```json
{
"id": "docker_swarm",
"name": "Docker Swarm Manager",
"version": "1.16.0",
"enabled": true,
"loaded": true,
"error": "",
"has_frontend": true,
"frontend_route": "/api/plugins/docker_swarm/api/ui",
"routes": [...55 routes...]
}
```

The dashboard correctly synthesises the tab list — pluginFrontendTabs produces [{ id: 'plugin:docker_swarm', label: 'Docker Swarm Manager' }]. The plugin would render given any non-empty user permissions.

The blocking line lives in the tab .filter (paraphrased from the bundled JSX, comment block credits NS May 2026 / #381):

```js
.filter(tab => {
if (tab.id === 'site-recovery') return user?.permissions?.includes('site_recovery.view');
if (tab.id === 'plugins') return user?.permissions?.includes('plugins.view');
if (tab.id === 'compliance') return user?.permissions?.includes('admin.audit') || user?.permissions?.includes('node.maintenance');
if (typeof tab.id === 'string' && tab.id.startsWith('plugin:'))
return user?.permissions?.includes('plugins.view');
return true;
})
```

API state on a fresh install:

Endpoint Field Value
GET /api/users [username=alfonso].permissions []
GET /api/users [username=alfonso].role \"admin\"
GET /api/users [username=pegaprox].permissions []
GET /api/users [username=pegaprox].role \"admin\"
GET /api/roles [id=admin].permissions.length 123
GET /api/roles [id=admin].permissions ⊇ {plugins.view, plugins.manage, site_recovery.view, admin.audit, …} true

So user.permissions is empty for the two built-in admins, but the admin role they hold owns the gating permissions. The .filter checks the personal list directly and never consults the role, so every gated tab is hidden. This affects:

A reasonable fix is to compute an effective-permissions set client-side as user.permissions ∪ role.permissions[user.role], or have the server hydrate user.permissions from the role on /api/users / login. The latter is probably the more sustainable choice — every other place that reads user.permissions would benefit from the same fix.

Workarounds (for users hit by this today)

  1. SQL: UPDATE users SET permissions = json('[\"plugins.view\"]') WHERE username = 'alfonso'; (UI doesn't surface per-user permission edits without going outside the admin role).
  2. Navigate directly to https://<host>/api/plugins/<id>/api/ui — the plugin UI works, you just can't reach it from the tab strip.

Affected scope

  • Built-in admin role users with empty personal permissions. Fresh installs and SSO-provisioned admins are the typical case.
  • Custom roles do not show this — those users have non-empty permissions populated when their role was assigned.

Cross-link

This was found while validating pegaprox-docker-swarm v1.16.0, the plugin that #286 produced and #381 unblocked. v1.16.0 deletes ~1130 LOC of dashboard.js patcher infra in favour of the manifest-declared frontend hook. Backend integration verified, plugin UI verified end-to-end via direct route — only the tab strip discoverability is blocked by this gate.

— Alfonso (cc @MrMasterbay @mkellermann97 from #381)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions