返回 Skill 列表
extension
分类: 开发与工程无需 API Key

apollo-enterprise-rbac

Apollo.io的企业角色基础访问控制。在实现团队权限、限制数据访问或设置企业安全控制时使用。可以通过诸如“apollo rbac”、“apollo permissions”、“apollo roles”、“apollo team access”、“apollo enterprise security”等短语触发。

person作者: jakexiaohubgithub

Apollo Enterprise RBAC

Overview

Implement role-based access control for Apollo.io integrations with granular permissions, team isolation, and audit trails.

Role Hierarchy

Super Admin
    |
    +-- Admin
    |     |
    |     +-- Sales Manager
    |     |       |
    |     |       +-- Sales Rep
    |     |
    |     +-- Marketing Manager
    |             |
    |             +-- Marketing User
    |
    +-- Read Only

Permission Definitions

// src/lib/rbac/permissions.ts
export const PERMISSIONS = {
  // Contact permissions
  'contacts:read': 'View contact information',
  'contacts:create': 'Add new contacts',
  'contacts:update': 'Edit contact information',
  'contacts:delete': 'Delete contacts',
  'contacts:export': 'Export contact data',
  'contacts:reveal_email': 'Reveal email addresses (uses credits)',

  // Search permissions
  'search:basic': 'Basic people search',
  'search:advanced': 'Advanced search filters',
  'search:bulk': 'Bulk search operations',

  // Enrichment permissions
  'enrich:person': 'Enrich individual contacts',
  'enrich:company': 'Enrich company data',
  'enrich:bulk': 'Bulk enrichment operations',

  // Sequence permissions
  'sequences:read': 'View sequences',
  'sequences:create': 'Create new sequences',
  'sequences:edit': 'Edit sequences',
  'sequences:delete': 'Delete sequences',
  'sequences:enroll': 'Add contacts to sequences',
  'sequences:send': 'Send emails through sequences',

  // Admin permissions
  'admin:users': 'Manage users',
  'admin:roles': 'Manage roles',
  'admin:settings': 'Configure settings',
  'admin:billing': 'View/manage billing',
  'admin:audit': 'View audit logs',
  'admin:api_keys': 'Manage API keys',
} as const;

export type Permission = keyof typeof PERMISSIONS;

Role Definitions

// src/lib/rbac/roles.ts
import { Permission } from './permissions';

interface Role {
  name: string;
  description: string;
  permissions: Permission[];
  inherits?: string[];
}

export const ROLES: Record<string, Role> = {
  super_admin: {
    name: 'Super Admin',
    description: 'Full system access',
    permissions: Object.keys(PERMISSIONS) as Permission[],
  },

  admin: {
    name: 'Admin',
    description: 'Administrative access without billing',
    permissions: [
      'contacts:read', 'contacts:create', 'contacts:update', 'contacts:delete', 'contacts:export',
      'search:basic', 'search:advanced', 'search:bulk',
      'enrich:person', 'enrich:company', 'enrich:bulk',
      'sequences:read', 'sequences:create', 'sequences:edit', 'sequences:delete', 'sequences:enroll', 'sequences:send',
      'admin:users', 'admin:roles', 'admin:settings', 'admin:audit',
    ],
  },

  sales_manager: {
    name: 'Sales Manager',
    description: 'Manage sales team and sequences',
    permissions: [
      'contacts:read', 'contacts:create', 'contacts:update', 'contacts:export', 'contacts:reveal_email',
      'search:basic', 'search:advanced',
      'enrich:person', 'enrich:company',
      'sequences:read', 'sequences:create', 'sequences:edit', 'sequences:enroll', 'sequences:send',
    ],
  },

  sales_rep: {
    name: 'Sales Representative',
    description: 'Basic sales access',
    permissions: [
      'contacts:read', 'contacts:create', 'contacts:update', 'contacts:reveal_email',
      'search:basic',
      'enrich:person',
      'sequences:read', 'sequences:enroll',
    ],
  },

  marketing_manager: {
    name: 'Marketing Manager',
    description: 'Manage marketing campaigns',
    permissions: [
      'contacts:read', 'contacts:export',
      'search:basic', 'search:advanced', 'search:bulk',
      'enrich:person', 'enrich:company', 'enrich:bulk',
      'sequences:read', 'sequences:create', 'sequences:edit',
    ],
  },

  marketing_user: {
    name: 'Marketing User',
    description: 'Basic marketing access',
    permissions: [
      'contacts:read',
      'search:basic',
      'sequences:read',
    ],
  },

  read_only: {
    name: 'Read Only',
    description: 'View-only access',
    permissions: [
      'contacts:read',
      'search:basic',
      'sequences:read',
    ],
  },
};

RBAC Service

// src/services/rbac/rbac.service.ts
import { User } from '../../models/user.model';
import { ROLES } from '../../lib/rbac/roles';
import { Permission } from '../../lib/rbac/permissions';

export class RBACService {
  async getUserPermissions(userId: string): Promise<Permission[]> {
    const user = await prisma.user.findUnique({
      where: { id: userId },
      include: {
        roles: true,
        customPermissions: true,
      },
    });

    if (!user) {
      return [];
    }

    const permissions = new Set<Permission>();

    // Add role permissions
    for (const role of user.roles) {
      const roleDef = ROLES[role.name];
      if (roleDef) {
        roleDef.permissions.forEach(p => permissions.add(p));
      }
    }

    // Add custom permissions
    user.customPermissions.forEach(p => {
      if (p.granted) {
        permissions.add(p.permission as Permission);
      } else {
        permissions.delete(p.permission as Permission);
      }
    });

    return Array.from(permissions);
  }

  async hasPermission(userId: string, permission: Permission): Promise<boolean> {
    const permissions = await this.getUserPermissions(userId);
    return permissions.includes(permission);
  }

  async hasAnyPermission(userId: string, permissions: Permission[]): Promise<boolean> {
    const userPermissions = await this.getUserPermissions(userId);
    return permissions.some(p => userPermissions.includes(p));
  }

  async hasAllPermissions(userId: string, permissions: Permission[]): Promise<boolean> {
    const userPermissions = await this.getUserPermissions(userId);
    return permissions.every(p => userPermissions.includes(p));
  }
}

export const rbac = new RBACService();

Permission Middleware

// src/middleware/rbac.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { rbac } from '../services/rbac/rbac.service';
import { Permission } from '../lib/rbac/permissions';

export function requirePermission(...permissions: Permission[]) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const userId = req.user?.id;

    if (!userId) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    const hasPermission = await rbac.hasAnyPermission(userId, permissions);

    if (!hasPermission) {
      // Audit failed access attempt
      await auditLog.create({
        action: 'PERMISSION_DENIED',
        actor: userId,
        resource: req.path,
        metadata: { requiredPermissions: permissions },
      });

      return res.status(403).json({
        error: 'Permission denied',
        required: permissions,
      });
    }

    next();
  };
}

// Usage in routes
router.get('/contacts',
  requirePermission('contacts:read'),
  contactController.list
);

router.post('/contacts',
  requirePermission('contacts:create'),
  contactController.create
);

router.delete('/contacts/:id',
  requirePermission('contacts:delete'),
  contactController.delete
);

router.post('/search/bulk',
  requirePermission('search:bulk', 'search:advanced'),
  searchController.bulkSearch
);

Team-Based Access Control

// src/services/rbac/team-access.ts
export class TeamAccessService {
  async getAccessibleContacts(
    userId: string,
    teamId: string
  ): Promise<string[]> {
    // Check user's team membership
    const membership = await prisma.teamMember.findFirst({
      where: { userId, teamId },
      include: { team: true },
    });

    if (!membership) {
      return [];
    }

    // Build access scope
    const accessScope = await this.buildAccessScope(userId, membership);

    // Return contact IDs user can access
    const contacts = await prisma.contact.findMany({
      where: accessScope,
      select: { id: true },
    });

    return contacts.map(c => c.id);
  }

  private async buildAccessScope(
    userId: string,
    membership: TeamMembership
  ): Promise<any> {
    // Team admins can see all team contacts
    if (membership.role === 'admin' || membership.role === 'manager') {
      return { teamId: membership.teamId };
    }

    // Regular members see own contacts + shared
    return {
      OR: [
        { ownerId: userId },
        { sharedWith: { some: { userId } } },
        { teamId: membership.teamId, isPublic: true },
      ],
    };
  }
}

// Middleware for team-scoped access
export function requireTeamAccess(resourceType: string) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const userId = req.user?.id;
    const resourceId = req.params.id;

    const hasAccess = await teamAccess.canAccessResource(
      userId,
      resourceType,
      resourceId
    );

    if (!hasAccess) {
      return res.status(403).json({
        error: 'You do not have access to this resource',
      });
    }

    next();
  };
}

API Key Scoping

// src/lib/rbac/api-key-scope.ts
interface ApiKeyScope {
  permissions: Permission[];
  rateLimit: number;
  ipAllowlist?: string[];
  expiresAt?: Date;
}

export async function createScopedApiKey(
  userId: string,
  scope: ApiKeyScope
): Promise<string> {
  // Validate user has the permissions they're granting
  const userPermissions = await rbac.getUserPermissions(userId);
  const invalidPermissions = scope.permissions.filter(
    p => !userPermissions.includes(p)
  );

  if (invalidPermissions.length > 0) {
    throw new Error(`Cannot grant permissions you don't have: ${invalidPermissions.join(', ')}`);
  }

  // Generate key
  const apiKey = generateSecureKey();

  // Store with scope
  await prisma.apiKey.create({
    data: {
      key: hashApiKey(apiKey),
      userId,
      permissions: scope.permissions,
      rateLimit: scope.rateLimit,
      ipAllowlist: scope.ipAllowlist,
      expiresAt: scope.expiresAt,
    },
  });

  return apiKey;
}

// Middleware to enforce API key scope
export function enforceApiKeyScope(requiredPermissions: Permission[]) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const apiKey = req.headers['x-api-key'];

    if (!apiKey) {
      return res.status(401).json({ error: 'API key required' });
    }

    const keyRecord = await prisma.apiKey.findFirst({
      where: { key: hashApiKey(apiKey as string) },
    });

    if (!keyRecord) {
      return res.status(401).json({ error: 'Invalid API key' });
    }

    // Check expiration
    if (keyRecord.expiresAt && new Date() > keyRecord.expiresAt) {
      return res.status(401).json({ error: 'API key expired' });
    }

    // Check IP allowlist
    if (keyRecord.ipAllowlist?.length > 0) {
      const clientIp = req.ip;
      if (!keyRecord.ipAllowlist.includes(clientIp)) {
        return res.status(403).json({ error: 'IP not allowed' });
      }
    }

    // Check permissions
    const hasPermission = requiredPermissions.every(
      p => keyRecord.permissions.includes(p)
    );

    if (!hasPermission) {
      return res.status(403).json({
        error: 'API key lacks required permissions',
        required: requiredPermissions,
      });
    }

    next();
  };
}

Admin Dashboard

// src/routes/admin/rbac.ts
router.get('/users/:id/permissions',
  requirePermission('admin:users'),
  async (req, res) => {
    const permissions = await rbac.getUserPermissions(req.params.id);
    res.json({ permissions });
  }
);

router.post('/users/:id/roles',
  requirePermission('admin:roles'),
  async (req, res) => {
    const { roles } = req.body;
    await rbac.assignRoles(req.params.id, roles);
    res.json({ success: true });
  }
);

router.get('/audit/access-denied',
  requirePermission('admin:audit'),
  async (req, res) => {
    const logs = await prisma.auditLog.findMany({
      where: { action: 'PERMISSION_DENIED' },
      orderBy: { createdAt: 'desc' },
      take: 100,
    });
    res.json({ logs });
  }
);

Output

  • Role-based permission system
  • Team-based access control
  • API key scoping
  • Permission middleware
  • Admin dashboard endpoints

Error Handling

| Issue | Resolution | |-------|------------| | Missing permissions | Request role upgrade | | Team access denied | Check team membership | | API key scope error | Regenerate with correct scope | | Role conflict | Higher role takes precedence |

Resources

Next Steps

Proceed to apollo-migration-deep-dive for migration strategies.