openclaw/src/security/rbac.ts
ronitchidara 1546fb5c04 enterprise: add Phase 5 observability and enterprise features
Observability:
- HTTP health endpoints (/health, /ready, /health/deep)
- Prometheus metrics endpoint (/metrics) with prom-client
- W3C traceparent request tracing with AsyncLocalStorage
- Trace ID propagation to logs and diagnostic events

Enterprise:
- JSONL audit logging with daily rotation and 7-day retention
- RBAC permission engine with flat roles (admin, operator, user, viewer)
- Tool and agent access restrictions per role
- Audit integration with pairing, auth, and exec approval flows

Documentation:
- Enterprise deployment guide (single/multi-tenant, K8s, Docker)
- Security hardening guide (TLS, RBAC, rate limiting)
- Observability guide (Prometheus, Grafana, alerting)
- Self-healing behaviors documentation

Load Testing:
- Connection stress test (WebSocket saturation)
- Chat throughput test (sustained message load)
- Auth stress test (rate limit verification)
- Configurable scenarios with p50/p95/p99 latency metrics

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:01:41 +05:30

346 lines
8.9 KiB
TypeScript

/**
* RBAC (Role-Based Access Control) engine for enterprise deployments.
*
* Provides permission checking based on user role assignments.
* When RBAC is disabled, all permissions are granted (backwards compatible).
*/
import type { ClawdbotConfig } from "../config/types.js";
import type { RbacConfig, RbacPermission, RbacRoleDefinition } from "../config/types.rbac.js";
import { auditRbacDenied } from "./audit-log.js";
/** Result of a permission check. */
export type RbacCheckResult = {
allowed: boolean;
role?: string;
reason?: string;
};
/** Built-in role definitions for when no custom roles are configured. */
const DEFAULT_ROLES: Record<string, RbacRoleDefinition> = {
admin: {
name: "Administrator",
description: "Full access to all features",
permissions: ["exec", "exec.elevated", "exec.approve", "admin"],
},
operator: {
name: "Operator",
description: "Can execute commands and approve requests",
permissions: ["exec", "exec.approve"],
},
user: {
name: "User",
description: "Can execute basic commands",
permissions: ["exec"],
},
viewer: {
name: "Viewer",
description: "Read-only access",
permissions: ["read-only"],
},
};
/**
* Check if RBAC is enabled in the config.
*/
export function isRbacEnabled(config?: ClawdbotConfig): boolean {
return config?.rbac?.enabled === true;
}
/**
* Get the RBAC config with defaults applied.
*/
function getRbacConfig(config?: ClawdbotConfig): RbacConfig {
return {
enabled: false,
roles: DEFAULT_ROLES,
assignments: {},
...config?.rbac,
};
}
/**
* Get all role definitions (custom roles merged with defaults).
*/
function getRoles(rbacConfig: RbacConfig): Record<string, RbacRoleDefinition> {
return {
...DEFAULT_ROLES,
...rbacConfig.roles,
};
}
/**
* Get the role definition for a user.
* Returns the assigned role, default role, or undefined if no role applies.
*/
export function getRoleForUser(
senderId: string,
config?: ClawdbotConfig,
): { roleId: string; role: RbacRoleDefinition } | null {
const rbacConfig = getRbacConfig(config);
const roles = getRoles(rbacConfig);
// Check explicit assignment first
const assignedRoleId = rbacConfig.assignments?.[senderId];
if (assignedRoleId && roles[assignedRoleId]) {
return { roleId: assignedRoleId, role: roles[assignedRoleId] };
}
// Fall back to default role
const defaultRoleId = rbacConfig.defaultRole;
if (defaultRoleId && roles[defaultRoleId]) {
return { roleId: defaultRoleId, role: roles[defaultRoleId] };
}
return null;
}
/**
* Check if a user has a specific permission.
*/
export function hasPermission(
senderId: string,
permission: RbacPermission,
config?: ClawdbotConfig,
): RbacCheckResult {
// If RBAC is disabled, allow everything
if (!isRbacEnabled(config)) {
return { allowed: true, reason: "rbac-disabled" };
}
const userRole = getRoleForUser(senderId, config);
if (!userRole) {
return { allowed: false, reason: "no-role-assigned" };
}
const { roleId, role } = userRole;
// Admin permission grants all other permissions
if (role.permissions.includes("admin")) {
return { allowed: true, role: roleId, reason: "admin-role" };
}
// Check for the specific permission
if (role.permissions.includes(permission)) {
return { allowed: true, role: roleId };
}
// Special case: exec.elevated requires explicit permission, but exec allows basic commands
if (permission === "exec" && role.permissions.includes("exec.elevated")) {
return { allowed: true, role: roleId, reason: "elevated-includes-exec" };
}
return { allowed: false, role: roleId, reason: "permission-denied" };
}
/**
* Check if a user can execute a command.
* Elevated commands (sudo, etc.) require exec.elevated permission.
*/
export function canExecuteCommand(
senderId: string,
command: string,
config?: ClawdbotConfig,
): RbacCheckResult {
// If RBAC is disabled, allow everything
if (!isRbacEnabled(config)) {
return { allowed: true, reason: "rbac-disabled" };
}
const isElevated = isElevatedCommand(command);
const requiredPermission: RbacPermission = isElevated ? "exec.elevated" : "exec";
const result = hasPermission(senderId, requiredPermission, config);
if (!result.allowed) {
// Audit the denial
auditRbacDenied({
actor: { type: "user", id: senderId },
action: requiredPermission,
resource: command,
reason: result.reason,
});
}
return result;
}
/**
* Check if a command requires elevated permissions.
*/
function isElevatedCommand(command: string): boolean {
const trimmed = command.trim().toLowerCase();
// Check for sudo prefix
if (trimmed.startsWith("sudo ")) return true;
// Check for common elevated operations
const elevatedPatterns = [
/^su\s/,
/^doas\s/,
/^pkexec\s/,
/^runas\s/,
/\|\s*sudo\s/,
/;\s*sudo\s/,
/&&\s*sudo\s/,
];
return elevatedPatterns.some((pattern) => pattern.test(trimmed));
}
/**
* Check if a user can access a specific agent.
*/
export function canAccessAgent(
senderId: string,
agentId: string,
config?: ClawdbotConfig,
): RbacCheckResult {
// If RBAC is disabled, allow everything
if (!isRbacEnabled(config)) {
return { allowed: true, reason: "rbac-disabled" };
}
const userRole = getRoleForUser(senderId, config);
if (!userRole) {
return { allowed: false, reason: "no-role-assigned" };
}
const { roleId, role } = userRole;
// Admin can access all agents
if (role.permissions.includes("admin")) {
return { allowed: true, role: roleId, reason: "admin-role" };
}
// If no agent restrictions, allow all
if (!role.agents || role.agents.length === 0) {
return { allowed: true, role: roleId, reason: "no-agent-restrictions" };
}
// Check if agent is in the allowed list
if (role.agents.includes(agentId)) {
return { allowed: true, role: roleId };
}
// Audit the denial
auditRbacDenied({
actor: { type: "user", id: senderId },
action: "agent.access",
resource: agentId,
reason: "agent-not-allowed",
});
return { allowed: false, role: roleId, reason: "agent-not-allowed" };
}
/**
* Check if a user can use a specific tool.
*/
export function canUseTool(
senderId: string,
toolName: string,
config?: ClawdbotConfig,
): RbacCheckResult {
// If RBAC is disabled, allow everything
if (!isRbacEnabled(config)) {
return { allowed: true, reason: "rbac-disabled" };
}
const userRole = getRoleForUser(senderId, config);
if (!userRole) {
return { allowed: false, reason: "no-role-assigned" };
}
const { roleId, role } = userRole;
// Admin can use all tools
if (role.permissions.includes("admin")) {
return { allowed: true, role: roleId, reason: "admin-role" };
}
// Read-only users cannot use tools that modify state
if (role.permissions.includes("read-only") && !role.permissions.includes("exec")) {
auditRbacDenied({
actor: { type: "user", id: senderId },
action: "tool.use",
resource: toolName,
reason: "read-only-role",
});
return { allowed: false, role: roleId, reason: "read-only-role" };
}
const toolAccess = role.tools;
if (!toolAccess) {
// No tool restrictions
return { allowed: true, role: roleId, reason: "no-tool-restrictions" };
}
// Check deny list first (takes precedence)
if (toolAccess.deny?.includes(toolName)) {
auditRbacDenied({
actor: { type: "user", id: senderId },
action: "tool.use",
resource: toolName,
reason: "tool-denied",
});
return { allowed: false, role: roleId, reason: "tool-denied" };
}
// Check allow list (if specified)
if (toolAccess.allow && toolAccess.allow.length > 0) {
if (!toolAccess.allow.includes(toolName)) {
auditRbacDenied({
actor: { type: "user", id: senderId },
action: "tool.use",
resource: toolName,
reason: "tool-not-allowed",
});
return { allowed: false, role: roleId, reason: "tool-not-allowed" };
}
}
return { allowed: true, role: roleId };
}
/**
* Check if a user can approve exec requests.
*/
export function canApproveExec(senderId: string, config?: ClawdbotConfig): RbacCheckResult {
return hasPermission(senderId, "exec.approve", config);
}
/**
* Get a summary of a user's permissions for debugging/display.
*/
export function getUserPermissionSummary(
senderId: string,
config?: ClawdbotConfig,
): {
enabled: boolean;
roleId?: string;
roleName?: string;
permissions?: RbacPermission[];
agents?: string[];
tools?: { allow?: string[]; deny?: string[] };
} {
if (!isRbacEnabled(config)) {
return { enabled: false };
}
const userRole = getRoleForUser(senderId, config);
if (!userRole) {
return { enabled: true };
}
const { roleId, role } = userRole;
return {
enabled: true,
roleId,
roleName: role.name,
permissions: role.permissions,
agents: role.agents,
tools: role.tools,
};
}