Agent Skills: HubSpot Enterprise RBAC

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/hubspot-enterprise-rbac

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/hubspot-pack/skills/hubspot-enterprise-rbac

Skill Files

Browse the full folder contents for hubspot-enterprise-rbac.

Download Skill

Loading file tree…

plugins/saas-packs/hubspot-pack/skills/hubspot-enterprise-rbac/SKILL.md

Skill Metadata

Name
hubspot-enterprise-rbac
Description
|

HubSpot Enterprise RBAC

Overview

Implement role-based access control for HubSpot integrations using OAuth scopes, multiple private apps with different permissions, and application-level authorization.

Prerequisites

  • HubSpot Enterprise subscription (for team-level permissions)
  • Understanding of HubSpot OAuth scopes
  • Multiple private apps or OAuth app configured

Instructions

Step 1: Scope-Based Access Model

HubSpot's permission model is scope-based. Create separate private apps for different access levels:

| Role | Private App | Scopes | Use Case | |------|-----------|--------|----------| | Reader | hubspot-readonly | crm.objects.contacts.read, crm.objects.deals.read, crm.objects.companies.read | Dashboards, reports | | Writer | hubspot-readwrite | Above + .write variants | CRM operations | | Admin | hubspot-admin | All CRM scopes + crm.schemas.*.read | Schema management | | Sync | hubspot-sync | crm.objects.contacts.read, crm.objects.contacts.write, crm.import | Data sync jobs | | Webhook | hubspot-webhook | automation | Event handling only |

Step 2: Multi-Token Client Factory

import * as hubspot from '@hubspot/api-client';

type AccessLevel = 'reader' | 'writer' | 'admin' | 'sync';

const TOKEN_MAP: Record<AccessLevel, string> = {
  reader: process.env.HUBSPOT_READER_TOKEN!,
  writer: process.env.HUBSPOT_WRITER_TOKEN!,
  admin: process.env.HUBSPOT_ADMIN_TOKEN!,
  sync: process.env.HUBSPOT_SYNC_TOKEN!,
};

const clientCache = new Map<AccessLevel, hubspot.Client>();

export function getClientForRole(role: AccessLevel): hubspot.Client {
  if (!clientCache.has(role)) {
    const token = TOKEN_MAP[role];
    if (!token) {
      throw new Error(`No token configured for role: ${role}`);
    }
    clientCache.set(role, new hubspot.Client({
      accessToken: token,
      numberOfApiCallRetries: 3,
    }));
  }
  return clientCache.get(role)!;
}

// Usage
const readClient = getClientForRole('reader'); // can only read
const writeClient = getClientForRole('writer'); // can read and write

Step 3: Application-Level Permission Middleware

import { Request, Response, NextFunction } from 'express';

interface AppPermissions {
  contacts: { read: boolean; write: boolean; delete: boolean };
  deals: { read: boolean; write: boolean; delete: boolean };
  companies: { read: boolean; write: boolean; delete: boolean };
}

const ROLE_PERMISSIONS: Record<string, AppPermissions> = {
  sales_rep: {
    contacts: { read: true, write: true, delete: false },
    deals: { read: true, write: true, delete: false },
    companies: { read: true, write: false, delete: false },
  },
  marketing: {
    contacts: { read: true, write: true, delete: false },
    deals: { read: true, write: false, delete: false },
    companies: { read: true, write: false, delete: false },
  },
  admin: {
    contacts: { read: true, write: true, delete: true },
    deals: { read: true, write: true, delete: true },
    companies: { read: true, write: true, delete: true },
  },
  readonly: {
    contacts: { read: true, write: false, delete: false },
    deals: { read: true, write: false, delete: false },
    companies: { read: true, write: false, delete: false },
  },
};

function requirePermission(
  objectType: keyof AppPermissions,
  action: 'read' | 'write' | 'delete'
) {
  return (req: Request, res: Response, next: NextFunction) => {
    const userRole = req.user?.role || 'readonly';
    const permissions = ROLE_PERMISSIONS[userRole];

    if (!permissions || !permissions[objectType]?.[action]) {
      return res.status(403).json({
        error: 'Forbidden',
        message: `Role "${userRole}" lacks ${action} permission for ${objectType}`,
      });
    }
    next();
  };
}

// Usage
app.get('/api/contacts', requirePermission('contacts', 'read'), listContacts);
app.post('/api/contacts', requirePermission('contacts', 'write'), createContact);
app.delete('/api/contacts/:id', requirePermission('contacts', 'delete'), deleteContact);

Step 4: OAuth 2.0 for Multi-Portal Access

// For public apps accessing multiple HubSpot portals
interface PortalCredentials {
  portalId: string;
  accessToken: string;
  refreshToken: string;
  expiresAt: Date;
}

class MultiPortalManager {
  private credentials = new Map<string, PortalCredentials>();

  async getClient(portalId: string): Promise<hubspot.Client> {
    let creds = this.credentials.get(portalId);

    if (!creds) {
      throw new Error(`No credentials for portal ${portalId}. User must authorize.`);
    }

    // Refresh token if expired
    if (new Date() >= creds.expiresAt) {
      creds = await this.refreshToken(creds);
      this.credentials.set(portalId, creds);
    }

    return new hubspot.Client({ accessToken: creds.accessToken });
  }

  private async refreshToken(creds: PortalCredentials): Promise<PortalCredentials> {
    const tempClient = new hubspot.Client();
    const response = await tempClient.oauth.tokensApi.create(
      'refresh_token',
      undefined, undefined,
      process.env.HUBSPOT_CLIENT_ID!,
      process.env.HUBSPOT_CLIENT_SECRET!,
      creds.refreshToken
    );

    return {
      ...creds,
      accessToken: response.accessToken,
      refreshToken: response.refreshToken,
      expiresAt: new Date(Date.now() + response.expiresIn * 1000),
    };
  }
}

Step 5: Audit Trail

interface HubSpotAuditEntry {
  timestamp: string;
  userId: string;
  role: string;
  action: string;
  objectType: string;
  objectId: string;
  success: boolean;
  hubspotCorrelationId?: string;
}

async function auditHubSpotAction(
  userId: string, role: string, action: string,
  objectType: string, objectId: string, success: boolean,
  correlationId?: string
): Promise<void> {
  const entry: HubSpotAuditEntry = {
    timestamp: new Date().toISOString(),
    userId, role, action, objectType, objectId, success,
    hubspotCorrelationId: correlationId,
  };

  // Store in your audit database
  await db.auditLog.insert(entry);

  // Alert on suspicious activity
  if (!success && action === 'delete') {
    console.warn('Failed delete attempt:', { userId, role, objectType, objectId });
  }
}

Output

  • Scope-based access model with separate private apps per role
  • Multi-token client factory for role-based HubSpot access
  • Application-level permission middleware
  • Multi-portal OAuth management for public apps
  • Audit trail for all HubSpot operations

Error Handling

| Issue | Cause | Solution | |-------|-------|----------| | 403 MISSING_SCOPES | Token lacks required scope | Use the correct role's token | | Permission denied in app | User role too restrictive | Check ROLE_PERMISSIONS mapping | | Token refresh fails | Client secret changed | Update client secret in env | | Audit gaps | Async logging failed | Add retry to audit log writes |

Resources

Next Steps

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