> clickup-enterprise-rbac

Implement ClickUp Enterprise SSO, OAuth 2.0 multi-workspace access, role-based permissions, and organization management via API v2. Trigger: "clickup SSO", "clickup RBAC", "clickup enterprise", "clickup roles", "clickup permissions", "clickup OAuth app", "clickup multi-workspace".

fetch
$curl "https://skillshub.wtf/jeremylongshore/claude-code-plugins-plus-skills/clickup-enterprise-rbac?format=md"
SKILL.mdclickup-enterprise-rbac

ClickUp Enterprise RBAC

Overview

Enterprise access patterns for ClickUp API v2. ClickUp's role system is built into the workspace, and the API surfaces roles via member objects. OAuth 2.0 enables multi-workspace apps where each user authorizes their own workspaces.

ClickUp Role Model

ClickUp workspace members have role IDs in the API:

Role IDRolePermissions
1OwnerFull control, billing, workspace settings
2AdminManage members, spaces, integrations
3MemberCreate/edit tasks, spaces (per permission)
4GuestLimited access to shared items only
// Get workspace members with roles
async function getWorkspaceMembers(teamId: string) {
  const data = await clickupRequest(`/team/${teamId}`);

  return data.team.members.map((m: any) => ({
    userId: m.user.id,
    username: m.user.username,
    email: m.user.email,
    role: m.user.role,       // 1=owner, 2=admin, 3=member, 4=guest
    roleLabel: { 1: 'owner', 2: 'admin', 3: 'member', 4: 'guest' }[m.user.role],
  }));
}

// Check if user can perform admin operations
function canAdminister(member: { role: number }): boolean {
  return member.role <= 2; // Owner or Admin
}

OAuth 2.0 Multi-Workspace App

Build apps that access multiple ClickUp workspaces on behalf of users.

// Step 1: Redirect user to ClickUp authorization
function getOAuthUrl(state: string): string {
  return `https://app.clickup.com/api?client_id=${process.env.CLICKUP_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.CLICKUP_REDIRECT_URI!)}&state=${state}`;
}

// Step 2: Exchange code for token
async function handleOAuthCallback(code: string) {
  const response = await fetch('https://api.clickup.com/api/v2/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: process.env.CLICKUP_CLIENT_ID,
      client_secret: process.env.CLICKUP_CLIENT_SECRET,
      code,
    }),
  });

  const { access_token } = await response.json();

  // Step 3: Discover which workspaces user authorized
  const teamsResponse = await fetch('https://api.clickup.com/api/v2/team', {
    headers: { 'Authorization': access_token },
  });
  const { teams } = await teamsResponse.json();

  return {
    token: access_token,   // Doesn't expire (but can be revoked)
    workspaces: teams.map((t: any) => ({ id: t.id, name: t.name })),
  };
}

// Step 4: Store per-user tokens
interface UserClickUpAuth {
  userId: string;
  clickupToken: string;      // Encrypt at rest
  authorizedWorkspaces: string[];
  connectedAt: Date;
}

Permission Middleware

// Express middleware that checks ClickUp workspace access
function requireClickUpAccess(requiredRole: number = 3) {
  return async (req: any, res: any, next: any) => {
    const userToken = req.user.clickupToken;
    const teamId = req.params.teamId || req.body.teamId;

    if (!userToken) {
      return res.status(401).json({ error: 'ClickUp not connected' });
    }

    // Verify user still has access to this workspace
    const teamsRes = await fetch('https://api.clickup.com/api/v2/team', {
      headers: { 'Authorization': userToken },
    });

    if (!teamsRes.ok) {
      return res.status(401).json({ error: 'ClickUp token expired or revoked' });
    }

    const { teams } = await teamsRes.json();
    const workspace = teams.find((t: any) => t.id === teamId);

    if (!workspace) {
      return res.status(403).json({ error: 'No access to this ClickUp workspace' });
    }

    // Check role level
    const userMember = workspace.members.find(
      (m: any) => m.user.id === req.user.clickupUserId
    );

    if (!userMember || userMember.user.role > requiredRole) {
      return res.status(403).json({
        error: `Requires role ${requiredRole} or higher`,
      });
    }

    req.clickupWorkspace = workspace;
    next();
  };
}

// Usage
app.delete('/api/clickup/:teamId/space/:spaceId',
  requireClickUpAccess(2), // Admin required
  async (req, res) => { /* ... */ }
);

User Groups (API v2 "Teams")

GET    /api/v2/group                    Get User Groups
POST   /api/v2/team/{team_id}/group     Create User Group
PUT    /api/v2/group/{group_id}         Update User Group
DELETE /api/v2/group/{group_id}         Delete User Group
// Create a user group for engineering team
await clickupRequest(`/team/${teamId}/group`, {
  method: 'POST',
  body: JSON.stringify({
    name: 'Engineering',
    member_ids: [183, 456, 789],
  }),
});

Audit Trail

interface ClickUpAuditEntry {
  timestamp: string;
  userId: number;
  workspaceId: string;
  action: string;
  resource: string;
  resourceId: string;
  success: boolean;
}

function logClickUpAction(entry: Omit<ClickUpAuditEntry, 'timestamp'>): void {
  const log: ClickUpAuditEntry = {
    ...entry,
    timestamp: new Date().toISOString(),
  };
  console.log(JSON.stringify({ level: 'audit', service: 'clickup', ...log }));
}

Error Handling

IssueCauseSolution
OAUTH_023/027Workspace not authorizedUser must re-authorize via OAuth flow
Role check failsUser role changed in ClickUpRe-fetch member data from API
Token revokedUser disconnected appHandle 401, prompt re-auth
Guest access deniedEndpoint requires member+Check role field before API call

Resources

Next Steps

For major migrations, see clickup-migration-deep-dive.

┌ stats

installs/wk0
░░░░░░░░░░
github stars1.7K
██████████
first seenMar 23, 2026
└────────────

┌ repo

jeremylongshore/claude-code-plugins-plus-skills
by jeremylongshore
└────────────