ZewstID Role-Based Access Control (RBAC)
Overview
This document details the complete RBAC architecture for ZewstID, covering product-scoped permissions, resource-level access control, and cross-product authorization policies.
Table of Contents
- RBAC Principles
- Permission Model
- Role Hierarchy
- Product Namespaces
- Resource Scoping
- Cross-Product Policies
- Permission Evaluation
- API Authorization
- Best Practices
RBAC Principles
Core Concepts
Users → Organizations → Products → Roles → Permissions → Resources
User: john@zewst.com └─ Organization: merchant-abc ├─ Product: POS │ └─ Role: pos.manager │ ├─ Permissions: [pos.sales.*, pos.inventory.*] │ └─ Resources: [store-1, store-2] │ └─ Product: Payrollso └─ Role: payroll.viewer ├─ Permissions: [payroll.reports.view] └─ Resources: [department:kitchen]
Design Principles
- Least Privilege: Users get minimum permissions needed
- Separation of Duties: Product permissions are isolated
- Context-Aware: Permissions scoped to organization
- Resource-Based: Fine-grained control over specific resources
- Auditable: All permission checks logged
- Delegatable: Users can grant subset of their permissions
Permission Model
Permission Structure
Permissions follow the format:
<product>.<resource>.<action>product = pos | payrollso | online | payments resource = sales | inventory | employees | orders | menu action = view | create | update | delete | manage | *
Permission Examples
pos.sales.view → View sales data pos.sales.create → Process sales transactions pos.inventory.* → All inventory operations payroll.employees.view → View employee list payroll.reports.generate → Generate payroll reports online.menu.update → Update restaurant menu online.orders.* → All order operations payments.refunds.create → Issue refunds
Wildcard Permissions
Wildcards allow granting broad permissions:
pos.* → All POS permissions pos.sales.* → All sales permissions *.view → View across all products (not recommended)
Permission Registry
Each product defines its permissions in a registry:
{ "product_id": "pos", "permissions": [ { "id": "pos.sales.view", "resource": "sales", "action": "view", "description": "View sales transactions and history", "requires_resource_scope": true, "resource_types": ["location", "terminal"] }, { "id": "pos.sales.create", "resource": "sales", "action": "create", "description": "Process new sales transactions", "requires_resource_scope": true, "resource_types": ["location", "terminal"] }, { "id": "pos.inventory.update", "resource": "inventory", "action": "update", "description": "Update inventory quantities and details", "requires_resource_scope": true, "resource_types": ["location"] }, { "id": "pos.reports.generate", "resource": "reports", "action": "generate", "description": "Generate sales and inventory reports", "requires_resource_scope": false } ] }
Role Hierarchy
Role Types
1. Base Roles
- Single-purpose roles with specific permissions
- Not composed of other roles
- Examples: ,
pos.cashierpayroll.viewer
2. Composite Roles
- Combine multiple base roles
- Inherit all permissions from child roles
- Examples: ,
pos.managerorg.admin
3. System Roles
- Pre-defined roles managed by platform
- Cannot be deleted or modified
- Examples: ,
org.ownerzewst.admin
4. Custom Roles
- Organization-specific roles
- Created by org admins
- Scoped to organization
Role Hierarchy Examples
POS Product Hierarchy
pos.admin (Composite, System) ├─ pos.manager (Composite, System) │ ├─ pos.cashier (Base, System) │ │ ├─ pos.sales.view │ │ ├─ pos.sales.create │ │ └─ pos.orders.view │ │ │ ├─ pos.inventory.manager (Base, System) │ │ ├─ pos.inventory.view │ │ ├─ pos.inventory.update │ │ └─ pos.inventory.adjust │ │ │ └─ pos.reports.viewer (Base, System) │ ├─ pos.reports.view │ └─ pos.reports.generate │ ├─ pos.settings.admin (Base, System) │ ├─ pos.settings.view │ ├─ pos.settings.update │ └─ pos.integrations.manage │ └─ pos.api.admin (Base, System) └─ pos.api.*
Payrollso Product Hierarchy
payroll.admin (Composite, System) ├─ payroll.manager (Composite, System) │ ├─ payroll.viewer (Base, System) │ │ ├─ payroll.employees.list │ │ ├─ payroll.reports.view │ │ └─ payroll.timesheets.view │ │ │ └─ payroll.processor (Base, System) │ ├─ payroll.timesheets.approve │ ├─ payroll.payments.process │ └─ payroll.adjustments.create │ └─ payroll.compliance.officer (Base, System) ├─ payroll.reports.view ├─ payroll.audit.access └─ payroll.compliance.generate
Organization-Level Hierarchy
org.owner (Composite, System) ├─ org.admin (Composite, System) │ ├─ org.user.manager (Base, System) │ │ ├─ org.users.invite │ │ ├─ org.users.remove │ │ └─ org.users.assign_roles │ │ │ ├─ org.settings.admin (Base, System) │ │ ├─ org.settings.view │ │ ├─ org.settings.update │ │ └─ org.billing.manage │ │ │ └─ org.service_accounts.manager (Base, System) │ ├─ org.service_accounts.create │ ├─ org.service_accounts.revoke │ └─ org.service_accounts.view │ └─ All product admin roles (pos.admin, payroll.admin, etc.)
Creating and Managing Roles
Use the ZewstID Admin API to create and manage roles:
import { ZewstID } from '@zewstid/nextjs'; const zewstid = new ZewstID({ clientId: process.env.ZEWSTID_CLIENT_ID!, apiUrl: 'https://api.zewstid.com', apiKey: process.env.ZEWSTID_API_KEY! }); // Create a base role await zewstid.roles.create({ name: 'pos.cashier', description: 'POS Cashier for sales transactions', permissions: [ 'pos.sales.view', 'pos.sales.create', 'pos.orders.view' ], type: 'base', productId: 'pos' }); // Create a composite role await zewstid.roles.create({ name: 'pos.manager', description: 'POS Manager with operational permissions', permissions: [ 'pos.sales.*', 'pos.inventory.*', 'pos.reports.*' ], type: 'composite', composedOf: ['pos.cashier', 'pos.inventory.manager'], productId: 'pos' }); // Assign role to user await zewstid.roles.assign({ userId: 'user-id', organizationId: 'org-id', role: 'pos.manager', productId: 'pos', resourceScope: { locations: ['store-1', 'store-2'], terminals: ['terminal-001'] } });
Product Namespaces
Product Scoping
Each product has a dedicated scope that's included in JWT tokens:
{ "sub": "user-id", "email": "john@example.com", "org_context": "merchant-abc", "products": { "pos": { "enabled": true, "permissions": ["pos.sales.*", "pos.inventory.*"], "roles": ["pos.manager"] }, "payrollso": { "enabled": true, "permissions": ["payroll.reports.view"], "roles": ["payroll.viewer"] } } }
Product Registration
Products register their permissions via the Admin API:
// Register a new product await zewstid.products.register({ id: 'pos', name: 'Point of Sale', baseUrl: 'https://pos.zewst.com', permissions: [ { id: 'pos.sales.view', resource: 'sales', action: 'view', description: 'View sales transactions' }, { id: 'pos.sales.create', resource: 'sales', action: 'create', description: 'Create sales transactions' }, { id: 'pos.inventory.update', resource: 'inventory', action: 'update', description: 'Update inventory' } ], defaultRoles: [ { name: 'pos.cashier', permissions: ['pos.sales.view', 'pos.sales.create'] }, { name: 'pos.manager', permissions: ['pos.sales.*', 'pos.inventory.*'] } ] });
OAuth Client Configuration
When creating an OAuth client for your product, specify which product scopes to include:
await zewstid.clients.create({ clientId: 'pos-application', name: 'POS Application', redirectUris: ['https://pos.example.com/callback'], defaultScopes: [ 'openid', 'email', 'profile', 'pos-scope', 'organization-scope' ], optionalScopes: [ 'payrollso-scope', 'online-scope', 'payments-scope' ], metadata: { product_id: 'pos' } });
Resource Scoping
Resource Types
Resources represent entities that permissions apply to:
Locations → Physical stores, restaurants Terminals → POS terminals, kiosks Departments → Kitchen, service, management Employees → Individual staff members Custom → Product-specific resources
Resource Scope Structure
{ "locations": ["store-1", "store-2", "*"], "terminals": ["terminal-001", "terminal-002"], "departments": ["kitchen", "service"], "employees": ["emp-123", "emp-456"], "custom": { "menu_sections": ["appetizers", "entrees"], "payment_methods": ["cash", "card"] } }
Managing Resource Access
// Create a resource definition await zewstid.resources.create({ organizationId: 'org-id', productId: 'pos', type: 'location', resourceId: 'store-1', name: 'Downtown Store', metadata: { address: '123 Main St', timezone: 'America/New_York' } }); // Assign resource access to a user await zewstid.resources.grantAccess({ userId: 'user-id', organizationId: 'org-id', productId: 'pos', resourceType: 'location', resourceId: 'store-1', permissions: ['pos.sales.view', 'pos.sales.create'] }); // Check resource access const hasAccess = await zewstid.resources.checkAccess({ userId: 'user-id', organizationId: 'org-id', productId: 'pos', resourceType: 'location', resourceId: 'store-1', permission: 'pos.sales.create' });
Resource Scope in JWT
Resource scopes are automatically included in JWT tokens:
{ "products": { "pos": { "permissions": ["pos.sales.*", "pos.inventory.view"], "resources": { "locations": ["store-1", "store-2"], "terminals": ["terminal-001"], "scope_expression": "location IN ['store-1', 'store-2'] AND terminal='terminal-001'" } } } }
Cross-Product Policies
Policy Definition
Cross-product policies grant permissions across product boundaries:
interface CrossProductPolicy { id: string; sourceRole: string; targetProduct: string; grantedPermissions: string[]; conditions?: Record<string, any>; enabled: boolean; }
Example Policies
// Create cross-product policies await zewstid.policies.create({ sourceRole: 'pos.manager', targetProduct: 'payrollso', grantedPermissions: [ 'payroll.reports.view', 'payroll.employees.list' ], description: 'POS managers can view employee payroll data' }); await zewstid.policies.create({ sourceRole: 'payments.admin', targetProduct: 'pos', grantedPermissions: [ 'pos.refunds.create', 'pos.transactions.view' ], description: 'Payment admins can process refunds' }); await zewstid.policies.create({ sourceRole: 'online.admin', targetProduct: 'pos', grantedPermissions: [ 'pos.menu.view', 'pos.inventory.view' ], description: 'Online admins can view menu and inventory' }); await zewstid.policies.create({ sourceRole: 'org.owner', targetProduct: '*', grantedPermissions: ['*'], description: 'Organization owners have full access' });
Policy Evaluation
// Get inherited permissions for a user const inheritedPermissions = await zewstid.policies.getInheritedPermissions({ userId: 'user-id', organizationId: 'org-id', targetProduct: 'payrollso' }); // Example response: // { // "inheritedFrom": { // "pos.manager": ["payroll.reports.view", "payroll.employees.list"] // }, // "permissions": ["payroll.reports.view", "payroll.employees.list"] // } // Check if policy applies const policyApplies = await zewstid.policies.evaluate({ userId: 'user-id', organizationId: 'org-id', policyId: 'policy-id', conditions: { subscription_tier: 'enterprise', user_verified: true } });
Permission Evaluation
Checking Permissions
import { ZewstID } from '@zewstid/nextjs'; const zewstid = new ZewstID({ clientId: process.env.ZEWSTID_CLIENT_ID!, apiUrl: 'https://api.zewstid.com' }); // Check if user has permission const hasPermission = await zewstid.permissions.check({ userId: 'user-id', organizationId: 'org-id', productId: 'pos', permission: 'pos.sales.create' }); // Check permission with resource scope const hasResourceAccess = await zewstid.permissions.check({ userId: 'user-id', organizationId: 'org-id', productId: 'pos', permission: 'pos.sales.create', resourceType: 'location', resourceId: 'store-1' }); // Get all user permissions for a product const userPermissions = await zewstid.permissions.list({ userId: 'user-id', organizationId: 'org-id', productId: 'pos' }); // Example response: // { // "permissions": [ // "pos.sales.view", // "pos.sales.create", // "pos.inventory.view" // ], // "inheritedPermissions": [ // "payroll.reports.view" // ], // "roles": ["pos.manager"], // "resourceScope": { // "locations": ["store-1", "store-2"] // } // }
Wildcard Matching
// The SDK handles wildcard matching automatically const hasAccess = await zewstid.permissions.check({ userId: 'user-id', organizationId: 'org-id', productId: 'pos', permission: 'pos.sales.create' }); // This will return true if user has any of: // - pos.sales.create (exact match) // - pos.sales.* (resource wildcard) // - pos.* (product wildcard) // - * (global wildcard)
Client-Side Permission Checks
'use client'; import { useSession } from '@zewstid/nextjs/client'; export default function SalesPage() { const { session, hasPermission } = useSession(); // Check permission from JWT claims const canCreateSales = hasPermission('pos.sales.create'); const canViewReports = hasPermission('pos.reports.view'); return ( <div> {canCreateSales && ( <button>Create Sale</button> )} {canViewReports && ( <a href="/reports">View Reports</a> )} </div> ); }
API Authorization
Protecting API Routes
import { withAuth, requirePermission } from '@zewstid/nextjs/server'; import { NextRequest } from 'next/server'; // Protect route with authentication export const GET = withAuth(async (req: NextRequest, { session }) => { // User is authenticated, session available return Response.json({ user: session.user }); }); // Require specific permission export const POST = requirePermission('pos.sales.create')( async (req: NextRequest, { session }) => { const body = await req.json(); // User has pos.sales.create permission // Create sale... return Response.json({ success: true }); } ); // Require permission with resource check export const PUT = requirePermission('pos.inventory.update', { resourceType: 'location', resourceIdFromParam: 'locationId' })( async (req: NextRequest, { session, params }) => { // User has pos.inventory.update permission for this location // Update inventory... return Response.json({ success: true }); } );
Middleware-Based Authorization
// middleware.ts import { withAuth } from '@zewstid/nextjs/middleware'; import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export default withAuth( function middleware(req: NextRequest) { const token = req.nextauth.token; const products = token?.products as Record<string, any>; // Check if user has access to POS product if (req.nextUrl.pathname.startsWith('/pos')) { if (!products?.pos?.enabled) { return NextResponse.redirect(new URL('/unauthorized', req.url)); } } // Check specific permission for admin routes if (req.nextUrl.pathname.startsWith('/admin')) { const permissions = products?.pos?.permissions || []; if (!permissions.includes('pos.admin') && !permissions.includes('pos.*')) { return NextResponse.redirect(new URL('/unauthorized', req.url)); } } return NextResponse.next(); } ); export const config = { matcher: ['/pos/:path*', '/admin/:path*'] };
Server Actions with Permissions
'use server'; import { requirePermission } from '@zewstid/nextjs/server'; // Server action with permission check export const createSale = requirePermission('pos.sales.create')( async (data: CreateSaleInput) => { // User has permission, proceed with creating sale const sale = await db.sale.create({ data: { ...data, organizationId: session.orgContext } }); return sale; } ); // Server action with resource scope check export const updateInventory = requirePermission('pos.inventory.update', { resourceType: 'location', resourceIdFromInput: 'locationId' })( async (data: UpdateInventoryInput) => { // User has permission for this specific location const inventory = await db.inventory.update({ where: { id: data.inventoryId, locationId: data.locationId }, data: data.updates }); return inventory; } );
React Components with Permission Guards
'use client'; import { PermissionGuard } from '@zewstid/nextjs/client'; export default function SalesPage() { return ( <div> <h1>Sales Dashboard</h1> {/* Only render if user has permission */} <PermissionGuard permission="pos.sales.create"> <CreateSaleButton /> </PermissionGuard> {/* Show different content based on permissions */} <PermissionGuard permission="pos.reports.view" fallback={<p>You don't have access to reports</p>} > <SalesReports /> </PermissionGuard> {/* Multiple permission check */} <PermissionGuard permissions={['pos.refunds.create', 'pos.manager']} requireAll={false} // User needs at least one > <RefundButton /> </PermissionGuard> </div> ); }
Best Practices
1. Principle of Least Privilege
Assign minimal permissions needed:
pos.cashier ├─ pos.sales.view ✓ ├─ pos.sales.create ✓ ├─ pos.orders.view ✓ └─ pos.inventory.* ✗ (too broad, cashier doesn't need inventory)
2. Use Composite Roles
Group related permissions:
pos.manager (Composite) ├─ pos.cashier → for sales operations ├─ pos.inventory.admin → for stock management └─ pos.reports.viewer → for reporting
3. Resource Scoping
Always scope to specific resources when possible:
// Good: Specific resources { resources: { locations: ["store-1"], terminals: ["terminal-001"] } } // Avoid: Wildcard resources (unless necessary) { resources: { locations: ["*"] } }
4. Regular Permission Audits
// Audit unused permissions periodically const unusedPermissions = await zewstid.permissions.audit({ organizationId: 'org-id', daysInactive: 90 }); // Remove stale permissions for (const audit of unusedPermissions) { if (audit.daysSinceLastUse > 90) { await zewstid.roles.revoke({ userId: audit.userId, organizationId: 'org-id', role: audit.role }); } }
5. Permission Delegation
When users create service accounts or assign roles:
// Check if user can delegate permission const canDelegate = await zewstid.permissions.canDelegate({ userId: 'admin-id', organizationId: 'org-id', targetPermission: 'pos.sales.create' }); if (canDelegate) { await zewstid.roles.assign({ userId: 'target-user-id', organizationId: 'org-id', role: 'pos.cashier' }); }
6. Graceful Permission Degradation
export default async function ReportsPage() { const { session } = await getServerSession(); const { hasPermission } = session; const reports = { sales: hasPermission('pos.reports.sales') ? await generateSalesReport() : null, inventory: hasPermission('pos.reports.inventory') ? await generateInventoryReport() : null, payroll: hasPermission('payroll.reports.summary') ? await generatePayrollSummary() : null }; return ( <div> {reports.sales && <SalesReport data={reports.sales} />} {reports.inventory && <InventoryReport data={reports.inventory} />} {reports.payroll && <PayrollReport data={reports.payroll} />} {!reports.sales && !reports.inventory && !reports.payroll && ( <p>No reports available with your current permissions</p> )} </div> ); }
7. Clear Error Messages
try { await requirePermission('pos.sales.create'); } catch (error) { if (error instanceof PermissionDeniedError) { // Error includes helpful details: // - Required permission // - User's current roles // - Organization context console.error(error.message); // "Permission denied: pos.sales.create // User: user-id // Organization: org-id // Your roles: [pos.viewer] // Contact your administrator to request access" } }
8. Cache Permission Checks
// The SDK automatically caches permission checks // Cache is invalidated on role assignment/revocation // Manual cache control if needed await zewstid.permissions.clearCache({ userId: 'user-id', organizationId: 'org-id' });
Summary
The ZewstID RBAC system provides:
- Product-scoped permissions via OAuth scopes and JWT claims
- Hierarchical roles using composite role inheritance
- Resource-level access control through resource scoping
- Cross-product policies for workflow integration
- Dynamic permission resolution at runtime
- Complete audit trail for compliance
- TypeScript-first SDK with full type safety
This architecture scales to thousands of organizations while maintaining fine-grained access control and excellent performance.
Next Steps
- SDK Documentation - Explore the full SDK API reference
- Onboarding Guide - Learn how to onboard new applications
- Customization - Customize the authentication experience
Was this page helpful?
Let us know how we can improve our documentation