Skip to main content

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

  1. RBAC Principles
  2. Permission Model
  3. Role Hierarchy
  4. Product Namespaces
  5. Resource Scoping
  6. Cross-Product Policies
  7. Permission Evaluation
  8. API Authorization
  9. 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

  1. Least Privilege: Users get minimum permissions needed
  2. Separation of Duties: Product permissions are isolated
  3. Context-Aware: Permissions scoped to organization
  4. Resource-Based: Fine-grained control over specific resources
  5. Auditable: All permission checks logged
  6. 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.cashier
    ,
    payroll.viewer

2. Composite Roles

  • Combine multiple base roles
  • Inherit all permissions from child roles
  • Examples:
    pos.manager
    ,
    org.admin

3. System Roles

  • Pre-defined roles managed by platform
  • Cannot be deleted or modified
  • Examples:
    org.owner
    ,
    zewst.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

Was this page helpful?

Let us know how we can improve our documentation