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