Security Resolution Algorithm
Overview
The security resolution algorithm determines whether a user can access a specific entity based on the entity’s classification and the user’s permissions across both security dimensions.
High-Level Flow
FOR EACH ENTITY ACCESS REQUEST:
┌─────────────────────────────────────────────────────────────┐
│ 1. Identify Entity Security Type │
│ (Org-Scoped, Person-Scoped, Dual-Scoped, etc.) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. Check for Admin Bypass │
│ Is user SYSTEM_ADMIN? → Grant full access │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. Evaluate Organizational Dimension (if applicable) │
│ - Determine entity's orgId (direct or transitive) │
│ - Check if orgId in user's accessible organizations │
│ - Verify required access level (READ or READ_WRITE) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. Evaluate Personal Dimension (if applicable) │
│ - Determine entity's personId (direct or transitive) │
│ - Check if personId in user's accessible persons │
│ - Verify required access level (READ or READ_WRITE) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. Combine Results │
│ - Org-only: Return org check result │
│ - Person-only: Return person check result │
│ - Composite: Return (org check AND person check) │
└─────────────────────────────────────────────────────────────┘
↓
┌──────────┴──────────┐
│ │
GRANTED DENIED
Detailed Algorithm
Step 1: Identify Entity Security Type
Determine which security checks are needed:
SecurityType type = determineSecurityType(entity);
switch (type) {
case ORG_SCOPED:
// Check org dimension only
break;
case PERSON_SCOPED:
// Check person dimension only
break;
case DUAL_SCOPED:
// Check both dimensions (AND logic)
break;
case TRANSITIVE_ORG:
// Check org via parent
break;
case TRANSITIVE_PERSON:
// Check person via parent
break;
}
Step 2: Admin Bypass Check
Check if user has administrative privileges that bypass all restrictions:
if (isSystemAdmin()) {
return GRANTED; // Full access to everything
}
if (hasGlobalOrgAccess() && requiredLevel == READ) {
// Global viewers can read all orgs
orgCheckPassed = true;
}
| Role | Bypass Behavior |
|---|---|
ROLE_ADMIN |
Full bypass - can access all organizations and all persons |
ROLE_GLOBAL_VIEWER |
Org dimension bypass for READ only |
ROLE_AUDITOR |
Org dimension bypass for READ only |
Step 3: Organizational Dimension Check
3.1 Determine Entity’s Organization
Long entityOrgId = null;
if (entity instanceof OrganisationScoped) {
entityOrgId = ((OrganisationScoped) entity).getOrgId();
} else if (type == TRANSITIVE_ORG) {
// Navigate to parent
entityOrgId = getTransitiveOrgId(entity);
}
3.2 Get User’s Accessible Organizations
Set<Long> userOrgIds = new HashSet<>();
// Add primary organization (always READ_WRITE)
userOrgIds.add(user.getPrimaryOrganisation().getId());
// Add linked organizations with sufficient access level
for (LinkedOrg link : user.getLinkedOrganisations()) {
if (link.isActive() &&
link.getAccessLevel().satisfies(requiredLevel)) {
userOrgIds.add(link.getOrganisation().getId());
}
}
Step 4: Personal Dimension Check
4.1 Determine Entity’s Person
Long entityPersonId = null;
if (entity instanceof PersonScoped) {
entityPersonId = ((PersonScoped) entity).getPersonId();
} else if (type == TRANSITIVE_PERSON) {
// Navigate to parent
entityPersonId = getTransitivePersonId(entity);
}
4.2 Get User’s Accessible Persons
Set<Long> userPersonIds = new HashSet<>();
// Add self (principal person - always READ_WRITE)
userPersonIds.add(user.getPrincipalPerson().getId());
// Add linked persons with sufficient access level
for (LinkedPerson link : user.getPrincipalPerson().getLinkedPersons()) {
if (link.isActive() &&
link.getAccessLevel().satisfies(requiredLevel)) {
userPersonIds.add(link.getToPersonId());
}
}
Step 5: Combine Results
Final authorization decision based on entity type:
boolean accessGranted;
switch (type) {
case ORG_SCOPED:
case TRANSITIVE_ORG:
accessGranted = orgCheckPassed;
break;
case PERSON_SCOPED:
case TRANSITIVE_PERSON:
accessGranted = personCheckPassed;
break;
case DUAL_SCOPED:
case DUAL_TRANSITIVE_ORG:
accessGranted = orgCheckPassed && personCheckPassed; // Both required
break;
default:
accessGranted = false;
}
return accessGranted ? GRANTED : DENIED;
Access Level Resolution
The AccessLevel enum has built-in hierarchy logic:
public enum AccessLevel {
READ,
READ_WRITE;
public boolean satisfies(AccessLevel required) {
if (required == READ) {
return true; // Both READ and READ_WRITE satisfy READ
}
return this == READ_WRITE; // Only READ_WRITE satisfies READ_WRITE
}
}
Complete Example Walkthrough
Scenario: EventEntry Access
Entity: EventEntry (Dual-Scoped)
-
Event: Summer Marathon (orgId = 10, belongs to "Running Club A")
-
Person: Emma (personId = 25)
User: Sarah
-
Primary Org: Running Club A (orgId = 10)
-
Linked Orgs: Running Club B (orgId = 11, READ)
-
Principal Person: Sarah (personId = 20)
-
Linked Persons: Emma (personId = 25, READ_WRITE via FAMILY)
Required Level: READ_WRITE (for updating entry)
Algorithm Execution
Step 3: Org Dimension
entityOrgId = eventEntry.getEvent().getOrgId() = 10
userOrgIds = [10] // Primary org
orgCheckPassed = 10 in [10] → true
userAccessLevel = READ_WRITE (primary org)
satisfies(READ_WRITE) → true
Org check: PASSED ✓
Caching Strategy
The algorithm uses caching to improve performance:
Cached Data
// Cache key: "username-READ" or "username-READ_WRITE"
@Cacheable(value = "userOrgAccess", key = "#username + '-' + #accessLevel")
Set<Long> getUserOrgIds(String username, AccessLevel accessLevel);
@Cacheable(value = "userPersonAccess", key = "#username + '-' + #accessLevel")
Set<Long> getUserPersonIds(String username, AccessLevel accessLevel);
Performance Optimization
Early Exit
The algorithm can exit early in several cases:
-
Admin bypass → Immediate grant
-
First dimension fails (composite-scoped) → Immediate deny (skip second check)
-
Empty accessible set → Immediate deny
Edge Cases
Empty Accessible Sets
If user has no org/person access:
userOrgIds = Collections.emptySet()
// Query: WHERE orgId IN () → Returns no results (SQL optimization)
Null Organization/Person
If entity has null orgId or personId:
if (entityOrgId == null) {
throw new BadRequestException("Entity must have organization");
}
// Prevents access to malformed data
Next Steps
-
Review the Domain Model for entity definitions
-
Explore Security Test Scenarios