feat: RBAC multi-user role-based access control
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 1m14s

- Add rbac.py with User model, LDAP group-to-role mapping, page/write dependencies
- Proxy auth reads Remote-Groups header to resolve role from LDAP groups
- JWT tokens carry role claim, /me returns role + allowed pages
- Per-router page access control and viewer write protection
- Terminal WebSocket RBAC check
- Frontend filters sidebar links and routes by allowed pages
- Admin-only Settings page and RBAC override endpoints
- Mount rbac.json in docker-compose for page config persistence
This commit is contained in:
Gan, Jimmy
2026-03-01 14:13:43 +08:00
parent 15ad628425
commit 9992105b49
10 changed files with 342 additions and 65 deletions
+34 -13
View File
@@ -11,13 +11,31 @@
import Security from "./routes/Security.svelte";
import Login from "./routes/Login.svelte";
import { onMount } from "svelte";
import { getToken, checkAuth, setToken, setRefreshToken } from "./lib/api.js";
import { getToken, checkAuth, setToken, setRefreshToken, currentUser, setCurrentUser } from "./lib/api.js";
let page = $state("dashboard");
let dark = $state(false);
let sidebarOpen = $state(false);
let authorized = $state(false);
let loading = $state(true);
let userRole = $state("");
let allowedPages = $state([]);
async function fetchUserInfo() {
try {
const data = await checkAuth();
setCurrentUser(data);
userRole = data.role || "admin";
allowedPages = data.pages || "*";
} catch (e) {
console.error("Failed to fetch user info:", e);
}
}
function hasPageAccess(pageId) {
if (allowedPages === "*") return true;
return Array.isArray(allowedPages) && allowedPages.includes(pageId);
}
onMount(async () => {
// Theme init
@@ -37,6 +55,7 @@
setToken(data.access_token);
setRefreshToken(data.refresh_token);
authorized = true;
await fetchUserInfo();
loading = false;
return;
}
@@ -51,9 +70,9 @@
authorized = false;
return;
}
try {
await checkAuth();
await fetchUserInfo();
authorized = true;
} catch (e) {
console.error("Auth check failed:", e);
@@ -90,31 +109,33 @@
<Login />
{:else}
<div class="flex h-screen overflow-hidden bg-surface-50 {dark ? 'dark bg-surface-900' : ''}">
<Sidebar bind:page {dark} {toggleTheme} {logout} bind:sidebarOpen />
<Sidebar bind:page {dark} {toggleTheme} {logout} bind:sidebarOpen {allowedPages} {userRole} />
<main class="flex-1 overflow-auto">
<div class="max-w-[1400px] mx-auto p-6 lg:p-8">
<!-- Mobile hamburger -->
<button class="md:hidden mb-4 p-2 rounded-lg hover:bg-surface-200 {dark ? 'hover:bg-surface-700 text-surface-300' : 'text-surface-600'}" onclick={() => sidebarOpen = true} aria-label="Open menu">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /></svg>
</button>
{#if page === "dashboard"}
{#if page === "dashboard" && hasPageAccess("dashboard")}
<Dashboard />
{:else if page === "docker"}
{:else if page === "docker" && hasPageAccess("docker")}
<Docker />
{:else if page === "gitea"}
{:else if page === "gitea" && hasPageAccess("gitea")}
<Gitea />
{:else if page === "files"}
{:else if page === "files" && hasPageAccess("files")}
<Files />
{:else if page === "terminal"}
{:else if page === "terminal" && hasPageAccess("terminal")}
<Terminal />
{:else if page === "openclaw"}
{:else if page === "openclaw" && hasPageAccess("openclaw")}
<OpenClaw />
{:else if page === "chat-digest"}
{:else if page === "chat-digest" && hasPageAccess("chat-digest")}
<ChatSummary />
{:else if page === "settings"}
{:else if page === "settings" && userRole === "admin"}
<Settings />
{:else if page === "security"}
{:else if page === "security" && hasPageAccess("security")}
<Security />
{:else}
<Dashboard />
{/if}
</div>
</main>