export type Capabilities = (typeof Capabilities)[keyof typeof Capabilities]; export const Capabilities = { // Can access the admin console ui_access: 1 << 0, // Read tailnet policy file (unimplemented) read_policy: 1 << 1, // Write tailnet policy file (unimplemented) write_policy: 1 << 2, // Read network configurations read_network: 1 << 3, // Write network configurations, for example, enable MagicDNS, split DNS, // make subnet, or allow a node to be an exit node, enable HTTPS write_network: 1 << 4, // Read feature configuration (unimplemented) read_feature: 1 << 5, // Write feature configuration, for example, enable Taildrop (unimplemented) write_feature: 1 << 6, // Configure user & group provisioning configure_iam: 1 << 7, // Read machines, for example, see machine names and status read_machines: 1 << 8, // Write machines, for example, approve, rename, and remove machines write_machines: 1 << 9, // Read users and user roles read_users: 1 << 10, // Write users and user roles, for example, remove users, // approve users, make Admin write_users: 1 << 11, // Can generate authkeys (unimplemented) generate_authkeys: 1 << 12, // Can use any tag (without being tag owner) (unimplemented) use_tags: 1 << 13, // Write tailnet name (unimplemented) write_tailnet: 1 << 14, // Owner flag owner: 1 << 15, } as const; export type Roles = [keyof typeof Roles]; export const Roles = { owner: Capabilities.ui_access | Capabilities.read_policy | Capabilities.write_policy | Capabilities.read_network | Capabilities.write_network | Capabilities.read_feature | Capabilities.write_feature | Capabilities.configure_iam | Capabilities.read_machines | Capabilities.write_machines | Capabilities.read_users | Capabilities.write_users | Capabilities.generate_authkeys | Capabilities.use_tags | Capabilities.write_tailnet | Capabilities.owner, admin: Capabilities.ui_access | Capabilities.read_policy | Capabilities.write_policy | Capabilities.read_network | Capabilities.write_network | Capabilities.read_feature | Capabilities.write_feature | Capabilities.configure_iam | Capabilities.read_machines | Capabilities.write_machines | Capabilities.read_users | Capabilities.write_users | Capabilities.generate_authkeys | Capabilities.use_tags | Capabilities.write_tailnet, network_admin: Capabilities.ui_access | Capabilities.read_policy | Capabilities.write_policy | Capabilities.read_network | Capabilities.write_network | Capabilities.read_feature | Capabilities.read_machines | Capabilities.read_users | Capabilities.generate_authkeys | Capabilities.use_tags | Capabilities.write_tailnet, it_admin: Capabilities.ui_access | Capabilities.read_policy | Capabilities.read_network | Capabilities.read_feature | Capabilities.write_feature | Capabilities.configure_iam | Capabilities.read_machines | Capabilities.write_machines | Capabilities.read_users | Capabilities.write_users | Capabilities.generate_authkeys, auditor: Capabilities.ui_access | Capabilities.read_policy | Capabilities.read_network | Capabilities.read_feature | Capabilities.read_machines | Capabilities.read_users, // Default role for new users with 0 capabilities on the UI side of things member: 0, } as const; export type Role = keyof typeof Roles; export type Capability = keyof typeof Capabilities; export function hasCapability(role: Role, capability: Capability): boolean { return (Roles[role] & Capabilities[capability]) !== 0; } export function getRoleFromCapabilities(capabilities: Capabilities): Role { const iterable = Roles as Record; for (const role in iterable) { if (iterable[role] === capabilities) { return role as Role; } } return 'member'; } /** * Maps OIDC groups to Headplane roles using configurable group-to-role mapping. * Groups are matched using exact string matching or prefix patterns. * * Default mapping (can be overridden via configuration): * - Groups containing "owner" or "admin" -> admin role * - Groups containing "network" -> network_admin role * - Groups containing "audit" -> auditor role * - Groups containing "it" -> it_admin role * - All other groups -> member role */ export function mapOidcGroupsToRole(groups: string[]): Role { if (!groups || groups.length === 0) { return 'member'; } // Load role mapping from environment variables (highest priority) const envMapping = loadRoleMappingFromEnv(); // Use environment mapping if any roles are configured const hasEnvMapping = Object.values(envMapping).some( (groups) => groups.length > 0, ); if (hasEnvMapping) { const role = findRoleInMapping(groups, envMapping); if (role !== 'member') return role; } // Fall back to intelligent convention-based matching return mapByConvention(groups); } function loadRoleMappingFromEnv(): Record { return { owner: parseEnvGroups(process.env.HEADPLANE_OWNER_GROUPS), admin: parseEnvGroups(process.env.HEADPLANE_ADMIN_GROUPS), network_admin: parseEnvGroups(process.env.HEADPLANE_NETWORK_ADMIN_GROUPS), it_admin: parseEnvGroups(process.env.HEADPLANE_IT_ADMIN_GROUPS), auditor: parseEnvGroups(process.env.HEADPLANE_AUDITOR_GROUPS), member: [], }; } function parseEnvGroups(envVar?: string): string[] { if (!envVar) return []; return envVar .split(',') .map((s) => s.trim()) .filter(Boolean); } function findRoleInMapping( userGroups: string[], mapping: Record, ): Role { const normalizedUserGroups = userGroups.map((g) => g.toLowerCase().trim()); const roleHierarchy: Role[] = [ 'owner', 'admin', 'network_admin', 'it_admin', 'auditor', ]; for (const role of roleHierarchy) { const roleGroups = mapping[role] || []; if ( roleGroups.some((mappedGroup) => normalizedUserGroups.includes(mappedGroup.toLowerCase()), ) ) { return role; } } return 'member'; } function mapByConvention(groups: string[]): Role { const normalizedGroups = groups.map((g) => g.toLowerCase().trim()); // Owner patterns - most specific first if ( normalizedGroups.some( (g) => g === 'owner' || g === 'ceo' || g === 'cto' || g === 'founder' || g.endsWith('-owner') || g.endsWith('-owners') || g.includes('founder') || g.startsWith('owner-') || g === 'executives', ) ) { return 'owner'; } // Admin patterns if ( normalizedGroups.some( (g) => g === 'admin' || g === 'administrator' || g === 'it-admin' || g.endsWith('-admin') || g.endsWith('-admins') || g.endsWith('-administrator') || g.startsWith('admin-') || g.includes('platform') || g.includes('sysadmin') || g === 'administrators' || g === 'managers', ) ) { return 'admin'; } // Network admin patterns if ( normalizedGroups.some( (g) => g === 'network' || g === 'devops' || g === 'sre' || g === 'netadmin' || g.includes('network') || g.includes('infrastructure') || g.includes('devops') || g.includes('sre') || g.includes('ops'), ) ) { return 'network_admin'; } // IT admin patterns if ( normalizedGroups.some( (g) => g === 'helpdesk' || g === 'support' || g === 'it' || g.includes('helpdesk') || g.includes('support') || g.includes('it-') || g.startsWith('it') || g === 'tech', ) ) { return 'it_admin'; } // Auditor patterns if ( normalizedGroups.some( (g) => g === 'auditor' || g === 'audit' || g === 'compliance' || g === 'security' || g.includes('audit') || g.includes('compliance') || g.includes('security'), ) ) { return 'auditor'; } return 'member'; }