veylant/web/src/pages/docs/guides/RbacGuide.tsx
2026-03-13 12:43:20 +01:00

183 lines
8.1 KiB
TypeScript

import { Callout } from "../components/Callout";
import { CodeBlock } from "../components/CodeBlock";
export function RbacGuide() {
return (
<div>
<h1 id="rbac">RBAC & Permissions</h1>
<p>
Veylant IA enforces Role-Based Access Control on every request. Roles are stored in the{" "}
<code>users</code> table and embedded in the HS256 JWT at login time. A role cannot be
elevated at runtime a new token must be issued after a role change.
</p>
<h2 id="roles">Roles</h2>
<div className="space-y-4 my-4">
{[
{
role: "admin",
color: "bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300",
description: "Full access. Can manage routing policies, users, providers, feature flags, and read all compliance/audit data. Has unrestricted model access.",
},
{
role: "manager",
color: "bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300",
description: "Read-write access to routing policies and user profiles. Can run AI inference with any model. Cannot manage feature flags or access compliance reports.",
},
{
role: "user",
color: "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300",
description: "Inference only. Restricted to the model list in rbac.user_allowed_models (default: gpt-4o-mini, mistral-medium). No admin API access.",
},
{
role: "auditor",
color: "bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300",
description: "Read-only access to audit logs, costs, and compliance data. Cannot call /v1/chat/completions. Intended for compliance officers and DPOs.",
},
].map((item) => (
<div key={item.role} className="rounded-lg border bg-card p-4 flex items-start gap-3">
<span className={`text-xs font-bold px-2 py-0.5 rounded font-mono shrink-0 ${item.color}`}>
{item.role}
</span>
<p className="text-sm text-muted-foreground leading-relaxed">{item.description}</p>
</div>
))}
</div>
<h2 id="permission-matrix">Permission Matrix</h2>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Endpoint</th>
<th className="text-center px-3 py-2.5 font-semibold">admin</th>
<th className="text-center px-3 py-2.5 font-semibold">manager</th>
<th className="text-center px-3 py-2.5 font-semibold">user</th>
<th className="text-center px-3 py-2.5 font-semibold">auditor</th>
</tr>
</thead>
<tbody>
{[
{ ep: "POST /v1/auth/login", admin: "✓", manager: "✓", user: "✓", auditor: "✓" },
{ ep: "POST /v1/chat/completions", admin: "✓", manager: "✓", user: "✓ (limited models)", auditor: "✗" },
{ ep: "POST /v1/pii/analyze", admin: "✓", manager: "✓", user: "✓", auditor: "✓" },
{ ep: "GET /v1/admin/policies", admin: "✓", manager: "✓", user: "✗", auditor: "✗" },
{ ep: "POST/PUT/DELETE /v1/admin/policies", admin: "✓", manager: "✓", user: "✗", auditor: "✗" },
{ ep: "GET/POST/PUT/DELETE /v1/admin/users", admin: "✓", manager: "read only", user: "✗", auditor: "✗" },
{ ep: "GET /v1/admin/logs", admin: "✓", manager: "✓", user: "✗", auditor: "✓" },
{ ep: "GET /v1/admin/costs", admin: "✓", manager: "✓", user: "✗", auditor: "✓" },
{ ep: "GET /v1/admin/compliance/*", admin: "✓", manager: "✗", user: "✗", auditor: "✓" },
{ ep: "POST/PUT/DELETE /v1/admin/compliance/*", admin: "✓", manager: "✗", user: "✗", auditor: "✗" },
{ ep: "GET/PUT/DELETE /v1/admin/flags", admin: "✓", manager: "✗", user: "✗", auditor: "✗" },
{ ep: "GET /v1/admin/providers/status", admin: "✓", manager: "✓", user: "✗", auditor: "✗" },
{ ep: "GET/POST/PUT/DELETE /v1/admin/providers", admin: "✓", manager: "✗", user: "✗", auditor: "✗" },
{ ep: "GET/PUT/DELETE /v1/admin/rate-limits", admin: "✓", manager: "✗", user: "✗", auditor: "✗" },
].map((row) => (
<tr key={row.ep} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.ep}</td>
{[row.admin, row.manager, row.user, row.auditor].map((v, i) => (
<td key={i} className={`px-3 py-2.5 text-xs text-center ${v === "✗" ? "text-muted-foreground" : v === "✓" ? "text-green-600 dark:text-green-400 font-medium" : "text-amber-600 dark:text-amber-400"}`}>
{v}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<h2 id="model-restrictions">Model Restrictions</h2>
<p>
Users with the <code>user</code> role can only access models listed in{" "}
<code>rbac.user_allowed_models</code>. Requests to other models are rejected with 403:
</p>
<CodeBlock
language="yaml"
code={`# config.yaml
rbac:
user_allowed_models:
- "gpt-4o-mini"
- "mistral-medium-latest"
- "claude-3-haiku-20240307"`}
/>
<CodeBlock
language="json"
code={`// 403 response when a 'user' requests gpt-4o:
{
"error": {
"type": "permission_error",
"message": "role 'user' does not have access to model 'gpt-4o'. Allowed: gpt-4o-mini, mistral-medium-latest, claude-3-haiku-20240307",
"code": "permission_denied"
}
}`}
/>
<Callout type="tip" title="admin and manager bypass model restrictions">
The <code>admin</code> and <code>manager</code> roles have unrestricted model access {" "}
<code>user_allowed_models</code> does not apply to them.
</Callout>
<h2 id="managing-roles">Managing User Roles via the Admin API</h2>
<p>
Roles are managed through the <code>/v1/admin/users</code> endpoints. After updating a
user's role, they must log in again to receive a new token with the updated claims.
</p>
<CodeBlock
language="bash"
code={`# List all users
curl "http://localhost:8090/v1/admin/users" \\
-H "Authorization: Bearer $ADMIN_TOKEN"
# Create a new user with a specific role
curl -X POST "http://localhost:8090/v1/admin/users" \\
-H "Authorization: Bearer $ADMIN_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"email": "alice@acme.com",
"password": "SecurePass123!",
"name": "Alice Martin",
"role": "auditor",
"department": "Legal"
}'
# Promote a user to manager
curl -X PUT "http://localhost:8090/v1/admin/users/user-uuid" \\
-H "Authorization: Bearer $ADMIN_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"role": "manager",
"department": "Finance"
}'
# Deactivate a user (GDPR soft-delete)
curl -X DELETE "http://localhost:8090/v1/admin/users/user-uuid" \\
-H "Authorization: Bearer $ADMIN_TOKEN"`}
/>
<Callout type="warning" title="Role changes require a new token">
JWT tokens embed the role at login time. After changing a user's role via the API, the user
must log out and log in again. The old token remains valid until its <code>exp</code> claim
(controlled by <code>auth.jwt_ttl_hours</code>).
</Callout>
<h2 id="bulk-import">Bulk User Import</h2>
<p>
For tenant onboarding, use the provided script to bulk-import users from a CSV file:
</p>
<CodeBlock
language="bash"
code={`# CSV format: email,first_name,last_name,department,role
cat > users.csv << 'EOF'
alice@acme.com,Alice,Martin,Legal,user
bob@acme.com,Bob,Dupont,Finance,manager
carol@acme.com,Carol,Lefebvre,IT,auditor
EOF
# Import (requires make dev to be running)
./deploy/onboarding/import-users.sh users.csv`}
/>
</div>
);
}