Files
Enterprise-Knowledge-Base/internal
wizardchen 1a46fcf8ca fix(rbac): role-aware UI gates leaked write affordances after tenant switch
When an Owner of tenant A invited user B as a Viewer, B could log in,
switch the active tenant from B's home to A via the user menu, and
still see every write button — Create KB, Upload Document, Edit/Delete
agent — because the role-gate plumbing on the client was wired to the
wrong tenant. The server's RBAC enforcement (PR 2 / PR 5) still blocked
the actual mutations with 403, so no data leaked, but the UI presented
authority the user did not have, which is a confusing and embarrassing
defect.

Five distinct root causes, all on the client / server boundary:

1. `currentTenantRole` (frontend/src/stores/auth.ts) read
   `tenant.value.id` — the user's *home* tenant — instead of the active
   tenant id. After switching to A, the helper still returned B's home
   role (Owner), so every `hasRole(...)` gate in the codebase silently
   short-circuited. Fix: read `selectedTenantId` first, fall back to
   `tenant.id`.

2. `setMemberships` was only called once, on /auth/login. Hard reloads
   (which the tenant switcher triggers) re-ran `getCurrentUser()` via
   `App.vue` and `router/index.ts`, but those callers discarded the
   `memberships` field of the response, so the auth store kept the
   login-time snapshot forever. Role changes between sessions stayed
   invisible. Fix: extract memberships in both /auth/me consumers and
   feed them into `setMemberships`.

3. `KnowledgeBase.vue`'s `isOwner` compared `kb.tenant_id` to the
   active tenant id, so any KB inside the current tenant was treated
   as "owned by the caller" — bypassing every role gate below. Fix:
   compare against `kb.creator_id` (the field PR 5 introduced) and
   require an actual creator match. `canEdit` / `canManage` now layer
   `hasRole('admin')` on top so a non-creator with sufficient role can
   still manage.

4. `KnowledgeBaseList.vue` and `AgentList.vue` rendered the per-card
   more-menu (Settings, Delete, Edit) unconditionally. With the
   ownership column from PR 5 this is straightforward to gate on the
   client: Settings/Delete now require creator-match or `hasRole('admin')`
   per the server's `OwnedKBOrAdmin` / `OwnedAgentOrAdmin` matrix.

5. The axios interceptor (frontend/src/utils/request.ts) had a
   "selectedTenantId === defaultTenantId → don't attach X-Tenant-ID"
   short-circuit. Any code that wrote the active tenant into
   `weknora_tenant` (UserMenu's loadUserInfo, OIDC sync, router
   hydrate) made the two values equal, after which the header silently
   stopped attaching and subsequent navigation requests landed on the
   home tenant. Fix: drop the short-circuit; always attach when
   selectedTenantId is set.

Server-side, /auth/me read `user.TenantID` (the immutable home tenant
id stamped at signup) when fetching the tenant for the response, so
the `tenant` field always pointed at home even after the auth
middleware had already resolved an active tenant from X-Tenant-ID.
The frontend then re-keyed `authStore.tenant` to the home tenant on
every refresh, undoing the switch. Fix: read the active tenant id from
context (`types.TenantIDFromContext(ctx)`) and only fall back to
user.TenantID when context is unset.

The server-side enforcement was correct throughout — every fix here is
purely about not lying to the user with the rendered UI.
2026-05-18 17:28:58 +08:00
..