Claude-code-plugins-plus-skills clickup-enterprise-rbac
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/clickup-pack/skills/clickup-enterprise-rbac" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-clickup-enterprise-rbac && rm -rf "$T"
manifest:
plugins/saas-packs/clickup-pack/skills/clickup-enterprise-rbac/SKILL.mdsource content
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 ID | Role | Permissions |
|---|---|---|
| 1 | Owner | Full control, billing, workspace settings |
| 2 | Admin | Manage members, spaces, integrations |
| 3 | Member | Create/edit tasks, spaces (per permission) |
| 4 | Guest | Limited 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
| Issue | Cause | Solution |
|---|---|---|
| OAUTH_023/027 | Workspace not authorized | User must re-authorize via OAuth flow |
| Role check fails | User role changed in ClickUp | Re-fetch member data from API |
| Token revoked | User disconnected app | Handle 401, prompt re-auth |
| Guest access denied | Endpoint requires member+ | Check field before API call |
Resources
Next Steps
For major migrations, see
clickup-migration-deep-dive.