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.md
source 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

RoleScopePermissions
Organization AdminOrg-wideFull access, billing, SSO
Organization MemberOrg-wideAccess granted teams
Team AdminPer-teamManage team settings
Team MemberPer-teamCreate/edit issues
GuestPer-teamLimited view access

API Key Scopes

ScopeAccess Level
read
Read-only access
write
Create and update
issues:create
Create issues only
admin
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

ErrorCauseSolution
ForbiddenError
Permission deniedCheck user role and team access
Invalid SSO token
Token expiredRe-authenticate user
Role not found
Unknown roleMap to default role

Resources

Next Steps

Complete your Linear knowledge with

linear-migration-deep-dive
.