Claude-skill-registry linear-enterprise-rbac
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/linear-enterprise-rbac" ~/.claude/skills/majiayu000-claude-skill-registry-linear-enterprise-rbac && rm -rf "$T"
manifest:
skills/data/linear-enterprise-rbac/SKILL.mdsource content
Linear Enterprise RBAC
Overview
Implement enterprise-grade role-based access control for Linear integrations.
Prerequisites
- Linear organization admin access
- Understanding of Linear's permission model
- SSO provider (Okta, Azure AD, Google Workspace)
Linear Permission Model
Built-in Roles
| Role | Scope | Permissions |
|---|---|---|
| Organization Admin | Org-wide | Full access, billing, SSO |
| Organization Member | Org-wide | Access granted teams |
| Team Admin | Per-team | Manage team settings |
| Team Member | Per-team | Create/edit issues |
| Guest | Per-team | Limited view access |
API Key Scopes
| Scope | Access Level |
|---|---|
| Read-only access |
| Create and update |
| Create issues only |
| Administrative actions |
Instructions
Step 1: Define Application Roles
// lib/rbac/roles.ts export enum AppRole { ADMIN = "admin", MANAGER = "manager", DEVELOPER = "developer", VIEWER = "viewer", } export interface RolePermissions { canCreateIssues: boolean; canUpdateIssues: boolean; canDeleteIssues: boolean; canManageProjects: boolean; canManageCycles: boolean; canManageTeam: boolean; canViewMetrics: boolean; allowedTeams: string[] | "*"; issueStateTransitions: string[]; } export const ROLE_PERMISSIONS: Record<AppRole, RolePermissions> = { [AppRole.ADMIN]: { canCreateIssues: true, canUpdateIssues: true, canDeleteIssues: true, canManageProjects: true, canManageCycles: true, canManageTeam: true, canViewMetrics: true, allowedTeams: "*", issueStateTransitions: ["*"], }, [AppRole.MANAGER]: { canCreateIssues: true, canUpdateIssues: true, canDeleteIssues: false, canManageProjects: true, canManageCycles: true, canManageTeam: false, canViewMetrics: true, allowedTeams: "*", issueStateTransitions: ["*"], }, [AppRole.DEVELOPER]: { canCreateIssues: true, canUpdateIssues: true, canDeleteIssues: false, canManageProjects: false, canManageCycles: false, canManageTeam: false, canViewMetrics: false, allowedTeams: [], // Set per-user issueStateTransitions: ["Todo->InProgress", "InProgress->InReview", "InReview->Done"], }, [AppRole.VIEWER]: { canCreateIssues: false, canUpdateIssues: false, canDeleteIssues: false, canManageProjects: false, canManageCycles: false, canManageTeam: false, canViewMetrics: false, allowedTeams: [], issueStateTransitions: [], }, };
Step 2: Permission Guard Implementation
// lib/rbac/guards.ts import { LinearClient } from "@linear/sdk"; import { AppRole, ROLE_PERMISSIONS, RolePermissions } from "./roles"; interface UserContext { userId: string; email: string; role: AppRole; teamAccess: string[]; } export class PermissionGuard { private permissions: RolePermissions; private userContext: UserContext; private linearClient: LinearClient; constructor(client: LinearClient, context: UserContext) { this.linearClient = client; this.userContext = context; this.permissions = { ...ROLE_PERMISSIONS[context.role], allowedTeams: context.teamAccess.length > 0 ? context.teamAccess : ROLE_PERMISSIONS[context.role].allowedTeams, }; } canAccessTeam(teamKey: string): boolean { if (this.permissions.allowedTeams === "*") return true; return this.permissions.allowedTeams.includes(teamKey); } canCreateIssue(teamKey: string): boolean { return this.permissions.canCreateIssues && this.canAccessTeam(teamKey); } canUpdateIssue(teamKey: string): boolean { return this.permissions.canUpdateIssues && this.canAccessTeam(teamKey); } canTransitionState(fromState: string, toState: string): boolean { const transitions = this.permissions.issueStateTransitions; if (transitions.includes("*")) return true; return transitions.includes(`${fromState}->${toState}`); } async assertCanCreateIssue(teamKey: string): Promise<void> { if (!this.canCreateIssue(teamKey)) { throw new ForbiddenError( `User ${this.userContext.email} cannot create issues in team ${teamKey}` ); } } async assertCanUpdateIssue(issueId: string): Promise<void> { const issue = await this.linearClient.issue(issueId); const team = await issue.team; if (!this.canUpdateIssue(team?.key ?? "")) { throw new ForbiddenError( `User ${this.userContext.email} cannot update issues in team ${team?.key}` ); } } }
Step 3: Secure Linear Client Factory
// lib/rbac/secure-client.ts import { LinearClient } from "@linear/sdk"; import { PermissionGuard } from "./guards"; import { UserContext } from "./types"; export class SecureLinearClient { private client: LinearClient; private guard: PermissionGuard; constructor(client: LinearClient, context: UserContext) { this.client = client; this.guard = new PermissionGuard(client, context); } async createIssue(input: { teamId: string; teamKey: string; title: string; description?: string; }) { await this.guard.assertCanCreateIssue(input.teamKey); return this.client.createIssue({ teamId: input.teamId, title: input.title, description: input.description, }); } async updateIssue(issueId: string, input: Record<string, unknown>) { await this.guard.assertCanUpdateIssue(issueId); return this.client.updateIssue(issueId, input); } async transitionIssue(issueId: string, newStateId: string) { const issue = await this.client.issue(issueId); const currentState = await issue.state; const newState = await this.client.workflowState(newStateId); if (!this.guard.canTransitionState(currentState?.name ?? "", newState.name)) { throw new ForbiddenError( `Cannot transition from ${currentState?.name} to ${newState.name}` ); } return this.client.updateIssue(issueId, { stateId: newStateId }); } // Filter issues by accessible teams async getAccessibleIssues(filter?: Record<string, unknown>) { const teams = await this.getAccessibleTeams(); const teamKeys = teams.map(t => t.key); return this.client.issues({ filter: { ...filter, team: { key: { in: teamKeys } }, }, }); } private async getAccessibleTeams() { const allTeams = await this.client.teams(); if (this.guard["permissions"].allowedTeams === "*") { return allTeams.nodes; } return allTeams.nodes.filter(t => (this.guard["permissions"].allowedTeams as string[]).includes(t.key) ); } }
Step 4: SSO Integration
// lib/auth/sso.ts import { OAuth2Client } from "google-auth-library"; interface SSOConfig { provider: "google" | "okta" | "azure"; clientId: string; clientSecret: string; domain?: string; } interface SSOUser { email: string; name: string; groups: string[]; } export async function verifySSOToken( token: string, config: SSOConfig ): Promise<SSOUser> { switch (config.provider) { case "google": return verifyGoogleToken(token, config); case "okta": return verifyOktaToken(token, config); case "azure": return verifyAzureToken(token, config); default: throw new Error(`Unknown SSO provider: ${config.provider}`); } } async function verifyGoogleToken(token: string, config: SSOConfig): Promise<SSOUser> { const client = new OAuth2Client(config.clientId); const ticket = await client.verifyIdToken({ idToken: token, audience: config.clientId, }); const payload = ticket.getPayload()!; return { email: payload.email!, name: payload.name!, groups: [], // Would come from Google Workspace groups API }; } // Map SSO groups to app roles export function mapGroupsToRole(groups: string[]): AppRole { if (groups.includes("linear-admins")) return AppRole.ADMIN; if (groups.includes("linear-managers")) return AppRole.MANAGER; if (groups.includes("linear-developers")) return AppRole.DEVELOPER; return AppRole.VIEWER; } // Map SSO groups to team access export function mapGroupsToTeams(groups: string[]): string[] { const teamMapping: Record<string, string[]> = { "engineering": ["ENG", "PLATFORM", "INFRA"], "product": ["PROD", "DESIGN"], "all-teams": ["*"], }; const teams = new Set<string>(); for (const group of groups) { const mappedTeams = teamMapping[group]; if (mappedTeams) { mappedTeams.forEach(t => teams.add(t)); } } return Array.from(teams); }
Step 5: Audit Logging
// lib/rbac/audit.ts interface AuditEntry { timestamp: Date; userId: string; userEmail: string; action: string; resource: string; resourceId: string; teamKey: string; allowed: boolean; reason?: string; } export class AuditLogger { async log(entry: Omit<AuditEntry, "timestamp">): Promise<void> { const fullEntry: AuditEntry = { ...entry, timestamp: new Date(), }; // Log to structured logging logger.info({ event: "rbac_audit", ...fullEntry, }); // Store in database for compliance await db.insert(auditLog).values(fullEntry); } async logAccess( user: UserContext, action: string, resource: string, resourceId: string, teamKey: string, allowed: boolean ): Promise<void> { await this.log({ userId: user.userId, userEmail: user.email, action, resource, resourceId, teamKey, allowed, reason: allowed ? undefined : "Permission denied", }); } } export const auditLogger = new AuditLogger();
Step 6: API Middleware
// middleware/rbac.ts import { SecureLinearClient } from "../lib/rbac/secure-client"; export async function rbacMiddleware(req: Request, res: Response, next: NextFunction) { try { // Get user from session/JWT const user = await getUserFromRequest(req); // Create permission-aware client const linearClient = new LinearClient({ apiKey: process.env.LINEAR_API_KEY!, }); const secureClient = new SecureLinearClient(linearClient, { userId: user.id, email: user.email, role: user.role, teamAccess: user.teams, }); // Attach to request req.linearClient = secureClient; next(); } catch (error) { res.status(403).json({ error: "Access denied" }); } }
Error Handling
| Error | Cause | Solution |
|---|---|---|
| Permission denied | Check user role and team access |
| Token expired | Re-authenticate user |
| Unknown role | Map to default role |
Resources
Next Steps
Complete your Linear knowledge with
linear-migration-deep-dive.