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 permissions", "maintainx roles". allowed-tools: Read, Write, Edit, Bash(npm:*) version: 1.0.0 license: MIT author: Jeremy Longshore <jeremy@intentsolutions.io>
Allowed Tools
No tools specified
Provided by Plugin
maintainx-pack
Claude Code skill pack for MaintainX CMMS (24 skills)
Installation
This skill is included in the maintainx-pack plugin:
/plugin install maintainx-pack@claude-code-plugins-plus
Click to copy
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
```typescript
// 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.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
```typescript
// 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 = {
'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
```typescript
// 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
```typescript
// src/rbac/location-access.ts
interface LocationAccess {
userId: string;
locationIds: string[];
includeChildren: boolean;
}
class LocationAccessControl {
private accessRules: Map = new Map();
setAccess(userId: string, locationIds: string[], includeChildren = true) {
this.accessRules.set(userId, {
userId,
locationIds,
includeChildren,
});
}
async canAccessWorkOrder(userId: string, workOrder: WorkOrder): Promise {
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 {
// 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 {
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
```typescript
// 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): Promise {
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 {
// 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 {
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
```typescript
// 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 {
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
- [MaintainX Enterprise](https://www.getmaintainx.com/enterprise)
- [SAML 2.0 Specification](https://wiki.oasis-open.org/security/FrontPage)
- [OWASP Access Control](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/)
## Next Steps
For complete platform migration, see `maintainx-migration-deep-dive`.