183 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
}
|