Skip to content
maintainx-enterprise-rbac logo

MaintainX Enterprise RBAC

maintainx-enterprise-rbac

Configure enterprise role-based access control for MaintainX integrations. Use when implementing SSO, managing organization-level permissions, or setting up enterprise access controls with MaintainX. Trigger with phrases like "maintainx rbac", "maintainx sso", "maintainx enterprise", "maintainx p...

SKILL.md

Full skill instructions

MaintainX Enterprise RBAC

Overview

Configure enterprise-grade role-based access control for MaintainX integrations, including SSO integration, permission management, and audit logging.

Prerequisites

  • MaintainX Enterprise plan
  • Identity Provider (IdP) with SAML/​OIDC
  • Understanding of RBAC concepts

MaintainX Role Hierarchy

┌─────────────────────────────────────────────────────────────────────┐
│                    MaintainX Role Hierarchy                          │
│                                                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │                      ORGANIZATION ADMIN                        │  │
│  │  Full access to all features, users, and settings             │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                               │                                      │
│              ┌────────────────┼────────────────┐                    │
│              ▼                ▼                ▼                    │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐           │
│  │    ADMIN      │  │  SUPERVISOR   │  │   REQUESTER   │           │
│  │               │  │               │  │               │           │
│  │ Manage users  │  │ Manage work   │  │ Submit        │           │
│  │ Manage assets │  │ Assign tasks  │  │ requests only │           │
│  │ Full WO access│  │ View reports  │  │               │           │
│  └───────────────┘  └───────┬───────┘  └───────────────┘           │
│                             │                                       │
│              ┌──────────────┴──────────────┐                       │
│              ▼                             ▼                       │
│  ┌───────────────┐              ┌───────────────┐                  │
│  │  TECHNICIAN   │              │    VIEWER     │                  │
│  │               │              │               │                  │
│  │ Execute work  │              │ Read-only     │                  │
│  │ Update status │              │ View reports  │                  │
│  └───────────────┘              └───────────────┘                  │
└─────────────────────────────────────────────────────────────────────┘

Instructions

Step 1: Role Definition

// src/​rbac/​roles.ts

enum MaintainXRole {
  OrganizationAdmin = 'ORGANIZATION_ADMIN',
  Admin = 'ADMIN',
  Supervisor = 'SUPERVISOR',
  Technician = 'TECHNICIAN',
  Requester = 'REQUESTER',
  Viewer = 'VIEWER',
}

interface Permission {
  resource: string;
  actions: ('create' | 'read' | 'update' | 'delete' | 'assign')[];
}

const rolePermissions: Record<MaintainXRole, Permission[]> = {
  [MaintainXRole.OrganizationAdmin]: [
    { resource: '*', actions: ['create', 'read', 'update', 'delete', 'assign'] },
  ],
  [MaintainXRole.Admin]: [
    { resource: 'workorders', actions: ['create', 'read', 'update', 'delete', 'assign'] },
    { resource: 'assets', actions: ['create', 'read', 'update', 'delete'] },
    { resource: 'locations', actions: ['create', 'read', 'update', 'delete'] },
    { resource: 'users', actions: ['create', 'read', 'update'] },
  ],
  [MaintainXRole.Supervisor]: [
    { resource: 'workorders', actions: ['create', 'read', 'update', 'assign'] },
    { resource: 'assets', actions: ['read'] },
    { resource: 'locations', actions: ['read'] },
    { resource: 'users', actions: ['read'] },
    { resource: 'reports', actions: ['read'] },
  ],
  [MaintainXRole.Technician]: [
    { resource: 'workorders', actions: ['read', 'update'] },
    { resource: 'assets', actions: ['read'] },
    { resource: 'locations', actions: ['read'] },
  ],
  [MaintainXRole.Requester]: [
    { resource: 'workrequests', actions: ['create', 'read'] },
    { resource: 'workorders', actions: ['read'] },
  ],
  [MaintainXRole.Viewer]: [
    { resource: 'workorders', actions: ['read'] },
    { resource: 'assets', actions: ['read'] },
    { resource: 'locations', actions: ['read'] },
    { resource: 'reports', actions: ['read'] },
  ],
};

function hasPermission(
  role: MaintainXRole,
  resource: string,
  action: string
): boolean {
  const permissions = rolePermissions[role];

  return permissions.some(p => {
    const resourceMatch = p.resource === '*' || p.resource === resource;
    const actionMatch = p.actions.includes(action as any);
    return resourceMatch && actionMatch;
  });
}

export { MaintainXRole, hasPermission, rolePermissions };

Step 2: SAML SSO Integration

// src/​auth/​saml-sso.ts
import { Strategy as SamlStrategy } from 'passport-saml';
import passport from 'passport';

interface SamlConfig {
  entryPoint: string;
  issuer: string;
  cert: string;
  callbackUrl: string;
}

// SAML configuration
const samlConfig: SamlConfig = {
  entryPoint: process.env.SAML_ENTRY_POINT!,  // IdP SSO URL
  issuer: process.env.SAML_ISSUER!,           // SP Entity ID
  cert: process.env.SAML_CERT!,               // IdP Certificate
  callbackUrl: `${process.env.APP_URL}/​auth/​saml/​callback`,
};

// Map IdP groups to MaintainX roles
const groupRoleMapping: Record<string, MaintainXRole> = {
  'MaintainX-Admins': MaintainXRole.Admin,
  'MaintainX-Supervisors': MaintainXRole.Supervisor,
  'MaintainX-Technicians': MaintainXRole.Technician,
  'MaintainX-Requesters': MaintainXRole.Requester,
  'MaintainX-Viewers': MaintainXRole.Viewer,
};

// Configure Passport SAML strategy
passport.use(new SamlStrategy(
  {
    ...samlConfig,
    passReqToCallback: true,
  },
  async (req, profile, done) => {
    try {
      // Extract user info from SAML assertion
      const email = profile.email || profile.nameID;
      const groups = profile.groups || [];

      // Determine role from groups
      let role = MaintainXRole.Viewer;  // Default role
      for (const [group, mappedRole] of Object.entries(groupRoleMapping)) {
        if (groups.includes(group)) {
          role = mappedRole;
          break;
        }
      }

      // Find or create user
      const user = await findOrCreateUser({
        email,
        firstName: profile.firstName,
        lastName: profile.lastName,
        role,
        ssoId: profile.nameID,
      });

      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// Routes
app.get('/​auth/​saml', passport.authenticate('saml'));

app.post('/​auth/​saml/​callback',
  passport.authenticate('saml', { failureRedirect: '/​login' }),
  (req, res) => {
    res.redirect('/');
  }
);

Step 3: Permission Middleware

// src/​middleware/​authorization.ts
import { Request, Response, NextFunction } from 'express';

interface AuthenticatedRequest extends Request {
  user?: {
    id: string;
    email: string;
    role: MaintainXRole;
  };
}

// Check permission middleware
function requirePermission(resource: string, action: string) {
  return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Unauthorized' });
    }

    if (!hasPermission(req.user.role, resource, action)) {
      auditLogger.log({
        type: 'ACCESS_DENIED',
        userId: req.user.id,
        resource,
        action,
        ip: req.ip,
      });

      return res.status(403).json({
        error: 'Forbidden',
        message: `You don't have permission to ${action} ${resource}`,
      });
    }

    next();
  };
}

// Check role middleware
function requireRole(...allowedRoles: MaintainXRole[]) {
  return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Unauthorized' });
    }

    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        error: 'Forbidden',
        message: `Required role: ${allowedRoles.join(' or ')}`,
      });
    }

    next();
  };
}

// Usage
app.get('/​api/​workorders',
  requirePermission('workorders', 'read'),
  getWorkOrders
);

app.post('/​api/​workorders',
  requirePermission('workorders', 'create'),
  createWorkOrder
);

app.delete('/​api/​workorders/:id',
  requirePermission('workorders', 'delete'),
  deleteWorkOrder
);

// Admin-only endpoint
app.get('/​api/​admin/​users',
  requireRole(MaintainXRole.OrganizationAdmin, MaintainXRole.Admin),
  getUsers
);

Step 4: Location-Based Access Control

// src/​rbac/​location-access.ts

interface LocationAccess {
  userId: string;
  locationIds: string[];
  includeChildren: boolean;
}

class LocationAccessControl {
  private accessRules: Map<string, LocationAccess> = new Map();

  setAccess(userId: string, locationIds: string[], includeChildren = true) {
    this.accessRules.set(userId, {
      userId,
      locationIds,
      includeChildren,
    });
  }

  async canAccessWorkOrder(userId: string, workOrder: WorkOrder): Promise<boolean> {
    const access = this.accessRules.get(userId);

    // No restrictions = full access
    if (!access || access.locationIds.length === 0) {
      return true;
    }

    // Check if work order's location is accessible
    if (!workOrder.locationId) {
      return true;  // No location = accessible
    }

    if (access.locationIds.includes(workOrder.locationId)) {
      return true;
    }

    // Check child locations if enabled
    if (access.includeChildren) {
      const childLocations = await this.getChildLocations(access.locationIds);
      return childLocations.includes(workOrder.locationId);
    }

    return false;
  }

  private async getChildLocations(parentIds: string[]): Promise<string[]> {
    // Recursively get all child location IDs
    const children: string[] = [];
    const locations = await maintainxClient.getLocations();

    function findChildren(parentId: string) {
      locations.locations
        .filter(l => l.parentId === parentId)
        .forEach(l => {
          children.push(l.id);
          findChildren(l.id);
        });
    }

    parentIds.forEach(findChildren);
    return children;
  }
}

// Apply location filter to queries
async function filterByLocationAccess(
  userId: string,
  workOrders: WorkOrder[]
): Promise<WorkOrder[]> {
  const lac = new LocationAccessControl();
  const filtered = [];

  for (const wo of workOrders) {
    if (await lac.canAccessWorkOrder(userId, wo)) {
      filtered.push(wo);
    }
  }

  return filtered;
}

Step 5: Audit Logging

// src/​rbac/​audit.ts

interface AuditEntry {
  timestamp: Date;
  type: 'ACCESS_GRANTED' | 'ACCESS_DENIED' | 'DATA_MODIFIED' | 'LOGIN' | 'LOGOUT';
  userId: string;
  userEmail?: string;
  userRole?: MaintainXRole;
  resource?: string;
  resourceId?: string;
  action?: string;
  ip: string;
  userAgent?: string;
  details?: any;
}

class AuditLogger {
  private store: AuditStore;

  async log(entry: Omit<AuditEntry, 'timestamp'>): Promise<void> {
    const fullEntry: AuditEntry = {
      ...entry,
      timestamp: new Date(),
    };

    // Store in database
    await this.store.insert(fullEntry);

    // Log to console for immediate visibility
    console.log(`[AUDIT] ${entry.type}: ${entry.userId} - ${entry.resource}/​${entry.action}`);

    // Alert on suspicious activity
    if (entry.type === 'ACCESS_DENIED') {
      await this.checkForSuspiciousActivity(entry.userId);
    }
  }

  private async checkForSuspiciousActivity(userId: string): Promise<void> {
    // Check for repeated access denials
    const recentDenials = await this.store.count({
      userId,
      type: 'ACCESS_DENIED',
      timestamp: { $gte: new Date(Date.now() - 5 * 60 * 1000) },  // Last 5 minutes
    });

    if (recentDenials > 10) {
      await alertSecurityTeam({
        type: 'SUSPICIOUS_ACCESS_PATTERN',
        userId,
        message: `User ${userId} has ${recentDenials} access denials in the last 5 minutes`,
      });
    }
  }

  // Generate compliance report
  async generateComplianceReport(startDate: Date, endDate: Date): Promise<any> {
    const entries = await this.store.find({
      timestamp: { $gte: startDate, $lte: endDate },
    });

    return {
      period: { start: startDate, end: endDate },
      totalEvents: entries.length,
      byType: this.groupBy(entries, 'type'),
      byUser: this.groupBy(entries, 'userId'),
      accessDenials: entries.filter(e => e.type === 'ACCESS_DENIED'),
      loginEvents: entries.filter(e => e.type === 'LOGIN'),
    };
  }
}

const auditLogger = new AuditLogger();
export { auditLogger };

Step 6: API Key Scoping

// src/​rbac/​api-keys.ts

interface ScopedApiKey {
  id: string;
  name: string;
  keyHash: string;
  permissions: Permission[];
  locationRestrictions?: string[];
  createdBy: string;
  createdAt: Date;
  expiresAt?: Date;
  lastUsedAt?: Date;
}

class ApiKeyManager {
  async createScopedKey(
    name: string,
    permissions: Permission[],
    options?: {
      locationRestrictions?: string[];
      expiresIn?: number;  // days
    }
  ): Promise<{ key: string; id: string }> {
    const rawKey = generateSecureToken(32);
    const keyHash = hashApiKey(rawKey);

    const apiKey: ScopedApiKey = {
      id: generateId(),
      name,
      keyHash,
      permissions,
      locationRestrictions: options?.locationRestrictions,
      createdBy: getCurrentUserId(),
      createdAt: new Date(),
      expiresAt: options?.expiresIn
        ? new Date(Date.now() + options.expiresIn * 24 * 60 * 60 * 1000)
        : undefined,
    };

    await this.store.insert(apiKey);

    return {
      key: rawKey,  // Only returned once
      id: apiKey.id,
    };
  }

  async validateKey(rawKey: string): Promise<ScopedApiKey | null> {
    const keyHash = hashApiKey(rawKey);
    const apiKey = await this.store.findOne({ keyHash });

    if (!apiKey) return null;

    // Check expiration
    if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
      return null;
    }

    // Update last used
    await this.store.update(
      { id: apiKey.id },
      { $set: { lastUsedAt: new Date() } }
    );

    return apiKey;
  }
}

Output

  • Role definitions implemented
  • SAML SSO integration
  • Permission middleware
  • Location-based access control
  • Audit logging
  • Scoped API keys

Enterprise Security Checklist

  • SSO configured with IdP
  • Role mappings defined
  • Permission checks on all endpoints
  • Audit logging enabled
  • Location restrictions configured
  • API key rotation policy
  • Compliance reports automated

Resources

Next Steps

For complete platform migration, see maintainx-migration-deep-dive.

More Security skills

supabase logo
Security

supabase

Handles the full Supabase workflow from schema changes to deployment, with built-in security guardrails that catch common traps like RLS...

2.7K 308.9K
View

Comprehensive guides and best practices for Neon Serverless Postgres, covering setup, connection methods, authentication, and platform APIs.

98 216.2K
View

Guide for setting up and using Firebase Authentication. Use this skill when the user's app requires user sign-in, user management, or secure data access using auth rules.

462 163.8K
View

A skill to evaluate how secure Firestore security rules are. Use this when Firestore security rules are updated to ensure that the generated rules are extremely secure and robust.

462 127.7K
View

Official skill for integrating Firebase AI Logic (Gemini API) into web applications. Covers setup, multimodal inference, structured output, and security.

462 125.1K
View

Complete Better Auth server and client setup with database adapters, session management, plugins, and security configuration.

222 118.2K
View

Deploy and manage projects on Vercel using token-based authentication. Use when working with Vercel CLI using access tokens rather than interactive login — e.g. "deploy to vercel", "set up vercel", "add environment variables to vercel".

31.9K 116.1K
View
cloudflare logo
Security

cloudflare

Complete Cloudflare platform integration with decision trees for compute, storage, AI, networking, security, and infrastructure-as-code.

3K 110.7K
View

Run Azure compliance and security audits with azqr plus Key Vault expiration checks. Covers best-practice assessment, resource review, policy/compliance validation, and security posture checks. WHEN: compliance scan, security audit, BEFORE running azqr (compliance cli tool), Azure best practices, Key Vault expiration check, expired certificates, expiring secrets, orphaned resources, compliance assessment.

253 103K
View
gws-gmail logo
Security

gws-gmail

Send, read, and manage Gmail messages, drafts, labels, and account settings.

31.2K 78.9K
View

Comprehensive website auditing across 230+ rules in 21 categories including SEO, performance, security, and accessibility.

94 72.3K
View
gws-shared logo
Security

gws-shared

Shared authentication, CLI syntax, and output formatting patterns for gws Google Workspace commands.

31.2K 63.4K
View

Security AI tools

StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
GeoSpy logo
Security

GeoSpy

GeoSpy: Pricing, Features, FAQs, and Alternatives for AI Teams

Free
View

Detect AI-generated voices to protect against audio fraud.

Paid
View

Ensure Your Content's Originality with AI Plagiarism Checker

Freemium
View
Img Upscaler logo
Security

Img Upscaler

Upscale images by 400% without quality loss

Freemium
View
AICheatCheck logo
Security

AICheatCheck

Accurately Detect AI-Generated Content with TheChecker.AI

Free
View
D
Security

Detect GPT

Chrome extension that detects and flags AI-generated content

Free
View