Skip to main content

Organization Isolation

Organizations provide an additional layer of data isolation within a tenant. Users with organization-scoped roles can only access data within their organization and its children.

Scope Hierarchy

PLATFORM Scope
└── TENANT Scope
└── ORGANIZATION Scope (this level)

How Organization Isolation Works

Organization-Scoped Roles

When a user has a role scoped to a specific organization:

User: John Doe
Role: ORG_ADMIN
Scope: Engineering Organization

Permissions:
✓ Manage users in Engineering
✓ Manage users in Frontend Team (child of Engineering)
✓ Manage users in Backend Team (child of Engineering)
✗ Cannot access Sales organization
✗ Cannot access HR organization

Child Organization Access

Organization permissions cascade down the hierarchy:

Engineering (ORG_ADMIN)
├── Frontend Team (inherited access)
│ └── UI Team (inherited access)
└── Backend Team (inherited access)
└── API Team (inherited access)

Assigning Organization-Scoped Roles

Endpoint

POST /role-assignments

Request

{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"roleId": "org-admin-role-id",
"organizationId": "engineering-org-id"
}

The organizationId field scopes the role to that specific organization.

Access Control Examples

Example 1: List Users

When an ORG_ADMIN in Engineering lists users:

GET /users
Authorization: Bearer <engineering-admin-token>

Response only includes users in Engineering and its children.

Example 2: Create User

ORG_ADMIN can only create users within their organization:

POST /users
{
"email": "new.user@example.com",
"organizationId": "frontend-team-id" // Must be Engineering or child
}

Example 3: Cross-Organization Access

If an Engineering admin tries to access a Sales user:

GET /users/sales-user-id

Response: 404 Not Found (not 403, to prevent information leakage)

Organization Membership

Users can belong to multiple organizations with different roles:

interface OrganizationMembership {
userId: string;
organizationId: string;
roleAssignments: RoleAssignment[];
}

// Example: User in multiple orgs
const memberships = [
{ organizationId: 'engineering', role: 'ORG_ADMIN' },
{ organizationId: 'sales', role: 'VIEWER' },
];

Checking Organization Access

API Pattern

// Service layer
async canAccessOrganization(
userId: string,
targetOrgId: string
): Promise<boolean> {
// Get user's organization scopes
const roleAssignments = await this.getRoleAssignments(userId);

for (const assignment of roleAssignments) {
if (!assignment.organizationId) {
// Tenant-level role - can access all orgs
return true;
}

// Check if target is the assigned org or a descendant
if (await this.isOrgOrDescendant(targetOrgId, assignment.organizationId)) {
return true;
}
}

return false;
}

Best Practices

1. Use Organization Scoping for Departmental Access

{
"userId": "manager-id",
"roleId": "org-admin-id",
"organizationId": "their-department-id"
}

2. Use Tenant Scoping for Cross-Org Access

{
"userId": "hr-admin-id",
"roleId": "hr-admin-id",
"organizationId": null // Tenant-wide access
}

3. Audit Organization Access Changes

All organization membership changes are logged:

{
"action": "ROLE_ASSIGNED",
"resourceType": "ROLE_ASSIGNMENT",
"metadata": {
"userId": "user-id",
"roleId": "role-id",
"organizationId": "org-id"
}
}

Common Patterns

Department Managers

Each department manager gets ORG_ADMIN for their department:

Engineering Manager → ORG_ADMIN → Engineering Org
Sales Manager → ORG_ADMIN → Sales Org
HR Manager → ORG_ADMIN → HR Org

Project Teams

Create a project organization and assign team members:

Project Alpha (Organization)
├── Project Lead: ORG_ADMIN
├── Developer 1: MEMBER
├── Developer 2: MEMBER
└── QA: VIEWER

Matrix Organization

Users can have different roles in different organizations:

Alice:
├── Engineering: MEMBER
├── Project Alpha: ORG_ADMIN
└── Training Committee: VIEWER