fix(web): restore admin nav menu items (#122)

v13 RBAC migration nulled users.role and moved admin authority onto user_group_members. Header still gated on session.user.role == 'admin', so admin menu was hidden for everyone. Inject user['is_admin'] via is_user_admin in get_current_user; header reads session.user.is_admin.
This commit is contained in:
ZdenekSrotyr 2026-04-29 09:09:23 +02:00 committed by GitHub
parent 33b318e491
commit 6752c4a53e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 29 additions and 3 deletions

View file

@ -14,6 +14,7 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
- Corporate memory pages (`/corporate-memory`, `/corporate-memory/admin`) now render the shared app header at full viewport width, matching the dashboard. Previously the `_app_header.html` include sat inside `.container-memory` (max-width: 1000px) and was cropped on wide viewports.
- `release.yml` now publishes a `:dev-<slug>` + `:dev-<prefix>-latest` image when a fresh branch is pushed off `main` with no extra commits. Pre-fix, `paths-ignore` on the `push` event diffed the new ref against the default branch — a same-SHA branch had zero diff, every file matched paths-ignore, and the workflow was skipped, so a developer creating a personal branch off main to deploy main's exact state to their dev VM (which pins to `:dev-<user>-latest`) had to either commit something or trigger the workflow manually. The `build-and-push` job's `if` was also tightened to `main || workflow_dispatch` only, which prevented branch-push images regardless. Both fixed: added `create:` trigger (filtered to branch refs at the job level so tag creates don't double-build with `keboola-deploy.yml`), and broadened `build-and-push.if` to also publish on non-main branch pushes / branch creates.
- Web header admin nav (All tokens, Marketplaces, Admin → Users / Groups / Resource access / Server config) is now visible to admin users again. Pre-fix, `_app_header.html` gated the admin block on `session.user.role == 'admin'`, but the v13 RBAC migration nulled `users.role` and moved admin authority onto `user_group_members` (Admin system group) — so the gate evaluated to false for everyone, including actual admins. `get_current_user` now injects `user["is_admin"]` (computed via `app.auth.access.is_user_admin`, the same call all server-side admin gates use), and the header reads `session.user.is_admin`. The role badge in the user-menu dropdown now reads "Admin" or hides — `users.role` is no longer surfaced in the UI.
## [0.15.0] — 2026-04-29

View file

@ -158,6 +158,7 @@ async def get_current_user(
if is_local_dev_mode():
user = _get_local_dev_user(conn)
if user:
_attach_admin_flag(user, conn)
return user
# Fall through to normal auth if seed missing — surfaces the bug
# instead of hiding it.
@ -181,6 +182,7 @@ async def get_current_user(
from app.auth.pat_resolver import resolve_token_to_user
user, reason = resolve_token_to_user(conn, token, request)
if user:
_attach_admin_flag(user, conn)
return user
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@ -188,6 +190,29 @@ async def get_current_user(
)
def _attach_admin_flag(user: dict, conn: duckdb.DuckDBPyConnection) -> None:
"""Inject ``user["is_admin"]`` so templates and route handlers can gate
admin-only UI without touching the legacy ``users.role`` column.
v13 nulled out ``users.role`` and moved admin authority onto
``user_group_members`` (Admin system group). The web header used to
gate its admin nav on ``session.user.role == 'admin'``, which silently
became false for every user so no admin saw any admin menu items
after the v13 migration. Computing the flag once per request here
keeps every consumer in sync with ``app.auth.access.is_user_admin``
(the same call all server-side admin gates use).
"""
from app.auth.access import is_user_admin
user_id = user.get("id")
if user_id:
try:
user["is_admin"] = is_user_admin(user_id, conn)
except Exception:
user["is_admin"] = False
else:
user["is_admin"] = False
async def get_optional_user(
request: Request = None,
authorization: Optional[str] = Header(None),

View file

@ -12,7 +12,7 @@
{% set _path = request.url.path %}
<a class="app-nav-link {% if _path == '/dashboard' or _path == '/' %}is-active{% endif %}" href="/dashboard">Dashboard</a>
<a class="app-nav-link {% if _path.startswith('/install') %}is-active{% endif %}" href="/install">Install CLI</a>
{% if session.user.role == 'admin' %}
{% if session.user.is_admin %}
<a class="app-nav-link {% if _path.startswith('/admin/tokens') %}is-active{% endif %}" href="/admin/tokens">All tokens</a>
<a class="app-nav-link {% if _path.startswith('/admin/marketplaces') %}is-active{% endif %}" href="/admin/marketplaces">Marketplaces</a>
{% set _admin_active = _path.startswith('/admin/users') or _path.startswith('/admin/groups') or _path.startswith('/admin/access') or _path.startswith('/admin/server-config') %}
@ -50,8 +50,8 @@
<div class="app-user-menu-panel" id="userMenuPanel" role="menu" hidden>
<div class="app-user-menu-header">
<div class="app-user-menu-email">{{ session.user.email }}</div>
{% if session.user.role %}
<div class="app-user-menu-role">{{ session.user.role | capitalize }}</div>
{% if session.user.is_admin %}
<div class="app-user-menu-role">Admin</div>
{% endif %}
</div>
<a class="app-user-menu-item {% if _path.startswith('/profile') %}is-active{% endif %}" role="menuitem" href="/profile">Profile</a>