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 permissions", "maintainx roles".
Configures enterprise role-based access control and SSO integration for MaintainX with permission management and audit logging.
/plugin marketplace add jeremylongshore/claude-code-plugins-plus-skills/plugin install maintainx-pack@claude-code-plugins-plusThis skill is limited to using the following tools:
Configure enterprise-grade role-based access control for MaintainX integrations, including SSO integration, permission management, and audit logging.
┌─────────────────────────────────────────────────────────────────────┐
│ 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 │ │
│ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
// 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 };
// 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('/');
}
);
// 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
);
// 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;
}
// 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 };
// 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;
}
}
For complete platform migration, see maintainx-migration-deep-dive.
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.