Skillshub canva-enterprise-rbac
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/canva-enterprise-rbac" ~/.claude/skills/comeonoliver-skillshub-canva-enterprise-rbac && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/canva-enterprise-rbac/SKILL.mdsource content
Canva Enterprise RBAC
Overview
Manage access control for Canva Connect API integrations at the organization level. The Canva API uses OAuth scopes (not roles) — your application layer implements RBAC on top of Canva's scope system.
Canva Enterprise Requirements
| Feature | Canva Free/Pro | Canva Enterprise |
|---|---|---|
| Design Create/Read | Yes | Yes |
| Export Designs | Yes | Yes |
| Asset Upload | Yes | Yes |
| Brand Templates | No | Yes |
| Autofill API | No | Yes |
| Folders (advanced) | Limited | Yes |
| Comments API | Yes | Yes |
Key: Autofill and brand template APIs require the user to be a member of a Canva Enterprise organization.
Application-Level RBAC
// Your application controls what each user role can do with Canva interface CanvaRole { name: string; scopes: string[]; // OAuth scopes to request allowedOperations: string[]; // Application-level operations } const CANVA_ROLES: Record<string, CanvaRole> = { viewer: { name: 'Viewer', scopes: ['design:meta:read'], allowedOperations: ['listDesigns', 'getDesign'], }, creator: { name: 'Creator', scopes: ['design:meta:read', 'design:content:write', 'design:content:read', 'asset:write', 'asset:read'], allowedOperations: ['listDesigns', 'getDesign', 'createDesign', 'exportDesign', 'uploadAsset'], }, admin: { name: 'Admin', scopes: [ 'design:meta:read', 'design:content:write', 'design:content:read', 'asset:write', 'asset:read', 'brandtemplate:meta:read', 'brandtemplate:content:read', 'folder:read', 'folder:write', 'comment:read', 'comment:write', 'collaboration:event', ], allowedOperations: ['*'], }, }; // Request only the scopes needed for the user's role function getScopesForRole(role: string): string[] { return CANVA_ROLES[role]?.scopes || CANVA_ROLES.viewer.scopes; }
Permission Middleware
function requireCanvaOperation(operation: string) { return async (req: Request, res: Response, next: NextFunction) => { const userRole = req.user?.canvaRole || 'viewer'; const role = CANVA_ROLES[userRole]; if (!role) { return res.status(403).json({ error: 'Unknown role' }); } if (!role.allowedOperations.includes('*') && !role.allowedOperations.includes(operation)) { return res.status(403).json({ error: 'Forbidden', message: `Role '${userRole}' cannot perform '${operation}'`, requiredRole: Object.entries(CANVA_ROLES) .find(([, r]) => r.allowedOperations.includes(operation) || r.allowedOperations.includes('*')) ?.[0], }); } next(); }; } // Usage app.post('/api/designs', requireCanvaOperation('createDesign'), async (req, res) => { const result = await req.canva.createDesign(req.body); res.json(result); } ); app.post('/api/autofill', requireCanvaOperation('autofillTemplate'), async (req, res) => { // Only admins can autofill — requires Enterprise + admin role const result = await req.canva.createAutofill(req.body); res.json(result); } );
User Capabilities Check
// GET https://api.canva.com/rest/v1/users/me/capabilities // Check what the authenticated user can do async function checkUserCapabilities(token: string): Promise<{ canAutofill: boolean; isEnterprise: boolean; }> { try { const data = await canvaAPI('/users/me/capabilities', token); return { canAutofill: data.capabilities?.includes('autofill') || false, isEnterprise: data.capabilities?.includes('brand_template') || false, }; } catch { return { canAutofill: false, isEnterprise: false }; } }
Scope-Based Access Control
// Track which scopes each user authorized interface UserCanvaAuth { userId: string; grantedScopes: string[]; // Scopes the user consented to role: string; // Application-assigned role connectedAt: Date; } // Check if a specific API call is authorized function canPerformAction( userAuth: UserCanvaAuth, requiredScope: string ): boolean { // 1. Check application role allows the operation const role = CANVA_ROLES[userAuth.role]; if (!role) return false; // 2. Check the required OAuth scope was granted by the user if (!userAuth.grantedScopes.includes(requiredScope)) { console.warn(`User ${userAuth.userId} missing scope: ${requiredScope}`); return false; } return true; } // If user needs additional scopes, redirect to re-authorize function buildReAuthUrl(userId: string, additionalScopes: string[]): string { const existingScopes = userAuth.grantedScopes; const allScopes = [...new Set([...existingScopes, ...additionalScopes])]; return getAuthorizationUrl({ clientId: process.env.CANVA_CLIENT_ID!, redirectUri: process.env.CANVA_REDIRECT_URI!, scopes: allScopes, codeChallenge: generatePKCE().challenge, state: `reauth:${userId}`, }); }
Audit Logging
async function auditCanvaAction(entry: { userId: string; role: string; action: string; endpoint: string; success: boolean; designId?: string; }): Promise<void> { await db.auditLog.insert({ ...entry, service: 'canva-connect-api', timestamp: new Date(), }); // Alert on permission escalation attempts if (!entry.success && entry.action === 'autofillTemplate') { console.warn(`Permission denied: user ${entry.userId} (role: ${entry.role}) attempted ${entry.action}`); } }
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| 403 on autofill | Not Enterprise user | Check user capabilities first |
| Scope not granted | User rejected consent | Show scope explanation, re-auth |
| Role mismatch | Wrong role assigned | Update user role in your DB |
| New scope needed | Feature added | Trigger re-authorization flow |
Resources
Next Steps
For major migrations, see
canva-migration-deep-dive.