Role-Based Access Control (RBAC)
Priority: 🔵 LONG-TERM Estimated Time: 8 hours Risk Level: LOW Impact: Fine-grained access control for MCP tools
Issue Description
Current access control:
- ✅ JWT-based authentication
- ✅ User context extraction
- ❌ No role-based access control
- ❌ All authenticated users have same permissions
- ❌ No tool-level permissions
Proposed Solution
Implement RBAC for MCP tools:
- Extract roles from JWT
groupsclaim - Define permissions per tool
- Add permission checking middleware
- Support role hierarchies
Implementation
Note: Redis is included in the local development environment when using ./scripts/local (part of the Docker stack).
Define Role System
Create src/types/rbac.ts:
typescript
export type Role = "admin" | "developer" | "operator" | "viewer";
export type Permission =
| "tools:execute"
| "tools:system_status"
| "tools:user_info"
| "resources:read"
| "prompts:use";
export interface RolePermissions {
[role: string]: Permission[];
}
export const rolePermissions: RolePermissions = {
admin: [
"tools:execute",
"tools:system_status",
"tools:user_info",
"resources:read",
"prompts:use",
],
developer: [
"tools:execute",
"tools:user_info",
"resources:read",
"prompts:use",
],
operator: [
"tools:system_status",
"resources:read",
],
viewer: [
"resources:read",
],
};Permission Checking
Update src/mcp/tools/index.ts:
typescript
import { hasPermission } from "../rbac.js";
export function registerTools(server: McpServer): void {
// Register healthcheck (public, no auth required)
registerHealthcheck(server);
// Register with permission checks
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const userContext = extra.userContext as UserContext | undefined;
if (!userContext) {
throw new Error("Authentication required");
}
// Check tool-specific permission
const toolName = request.params.name;
const requiredPermission = getToolPermission(toolName);
if (requiredPermission && !hasPermission(userContext, requiredPermission)) {
throw new Error(`Permission denied: ${requiredPermission} required`);
}
// Execute tool
return executeToolWithPermissions(toolName, request.params.arguments, userContext);
});
}
function getToolPermission(toolName: string): Permission | null {
const toolPermissions: Record<string, Permission> = {
system_status: "tools:system_status",
user_info: "tools:user_info",
// ... other tools
};
return toolPermissions[toolName] ?? "tools:execute";
}RBAC Utility
Create src/services/rbac.ts:
typescript
import type { UserContext } from "../types/index.js";
import type { Permission } from "../types/rbac.js";
import { rolePermissions } from "../types/rbac.js";
export function hasPermission(user: UserContext, permission: Permission): boolean {
// Extract roles from groups claim
const userRoles = user.groups ?? [];
// Check if any user role has the required permission
return userRoles.some((role) => {
const permissions = rolePermissions[role];
return permissions?.includes(permission) ?? false;
});
}
export function hasAnyPermission(user: UserContext, permissions: Permission[]): boolean {
return permissions.some((permission) => hasPermission(user, permission));
}
export function hasAllPermissions(user: UserContext, permissions: Permission[]): boolean {
return permissions.every((permission) => hasPermission(user, permission));
}Configuration
Create src/config/rbac.ts:
typescript
export const rbacConfig = {
enabled: process.env.RBAC_ENABLED !== "false",
// Map OIDC groups to internal roles
groupRoleMapping: {
"admins": "admin",
"developers": "developer",
"ops": "operator",
"users": "viewer",
},
// Default role if no groups match
defaultRole: process.env.RBAC_DEFAULT_ROLE ?? "viewer",
} as const;Add to .env.example:
env
# RBAC Configuration
RBAC_ENABLED=true
RBAC_DEFAULT_ROLE=viewerRole Definitions
Admin
Full System Access
Permissions:
- Execute all tools
- View system status
- Access user information
- Read all resources
- Use all prompts
Use Cases:
- System administrators
- Platform operators
- Security team members
Developer
Development Access
Permissions:
- Execute custom tools
- View user information
- Read resources
- Use prompts
Use Cases:
- Application developers
- Integration engineers
- API consumers
Operator
Operations Access
Permissions:
- View system status
- Read resources (monitoring data)
Use Cases:
- SRE team members
- Operations staff
- Monitoring systems
Viewer
Read-Only Access
Permissions:
- Read resources only
Use Cases:
- Auditors
- Stakeholders
- Reporting systems
Testing
Create comprehensive tests in src/services/rbac.test.ts:
typescript
describe("RBAC", () => {
it("should allow admin to execute all tools", async () => {
const user: UserContext = {
sub: "user-123",
groups: ["admins"],
};
expect(hasPermission(user, "tools:execute")).toBe(true);
expect(hasPermission(user, "tools:system_status")).toBe(true);
});
it("should deny viewer from executing system_status", async () => {
const user: UserContext = {
sub: "user-456",
groups: ["users"],
};
expect(hasPermission(user, "tools:system_status")).toBe(false);
expect(hasPermission(user, "resources:read")).toBe(true);
});
it("should handle multiple roles correctly", async () => {
const user: UserContext = {
sub: "user-789",
groups: ["developers", "ops"],
};
// Should have permissions from both roles
expect(hasPermission(user, "tools:execute")).toBe(true);
expect(hasPermission(user, "tools:system_status")).toBe(true);
});
it("should use default role for users with no groups", async () => {
const user: UserContext = {
sub: "user-000",
groups: [],
};
// Should have viewer permissions
expect(hasPermission(user, "resources:read")).toBe(true);
expect(hasPermission(user, "tools:execute")).toBe(false);
});
});IdP Configuration
Configure Groups in Your IdP
Authentik
- Navigate to Directory → Groups
- Create groups:
admins,developers,ops,users - Assign users to appropriate groups
- In Application settings, ensure groups are included in token claims
Auth0
- Navigate to User Management → Roles
- Create roles matching your group names
- Assign permissions to roles
- Add "Add user roles to tokens" rule
Keycloak
- Navigate to Groups
- Create groups with matching names
- In Client Scopes, map groups to JWT
groupsclaim - Assign users to groups
Monitoring
Add Prometheus metrics:
typescript
export const rbacDenials = new promClient.Counter({
name: "rbac_denials_total",
help: "Total RBAC permission denials",
labelNames: ["role", "permission", "tool"],
});
export const rbacChecks = new promClient.Counter({
name: "rbac_checks_total",
help: "Total RBAC permission checks",
labelNames: ["result"],
});Set up alerts:
promql
# High rate of permission denials
rate(rbac_denials_total[5m]) > 50
# Users with no valid roles
rate(rbac_checks_total{result="no_role"}[5m]) > 10Tool-Specific Permissions
Define granular permissions per tool:
typescript
const toolPermissions: Record<string, Permission> = {
// System tools (admin only)
system_status: "tools:system_status",
server_metrics: "tools:system_status",
// User tools (developer+)
user_info: "tools:user_info",
// General tools (all authenticated)
healthcheck: null, // public
// Custom tools (developer+)
execute_query: "tools:execute",
run_analysis: "tools:execute",
};Dynamic Permission Loading
For advanced scenarios, load permissions from database:
typescript
export async function loadDynamicPermissions(): Promise<RolePermissions> {
const redis = getRedisClient();
const permissions = await redis.get("rbac:permissions");
if (permissions) {
return JSON.parse(permissions);
}
return rolePermissions; // fallback to static
}Migration Path
Phase 1: Add Roles (No Enforcement)
- Add role extraction
- Log role information
- Monitor for missing roles
Phase 2: Soft Enforcement (Warn Only)
- Check permissions
- Log denials
- Allow all requests (backwards compatible)
Phase 3: Full Enforcement
- Enforce permissions
- Return 403 for unauthorized requests
- Monitor denial rates
Related Enhancements
- Session IP Binding - Additional security layer
- Audit Logging - Track permission denials