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());
    }
}

3.3 Check Organization Access

boolean orgCheckPassed = userOrgIds.contains(entityOrgId);

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());
    }
}

4.3 Check Person Access

boolean personCheckPassed = userPersonIds.contains(entityPersonId);

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
    }
}

Examples

User Has Required Satisfies?

READ_WRITE

READ

✓ Yes

READ_WRITE

READ_WRITE

✓ Yes

READ

READ

✓ Yes

READ

READ_WRITE

✗ No

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 1: Identify Type

Entity type: DUAL_SCOPED
Need to check: BOTH org and person dimensions

Step 2: Admin Check

isSystemAdmin() → false
Continue to dimension checks...

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 ✓

Step 4: Person Dimension

entityPersonId = eventEntry.getPersonId() = 25
userPersonIds = [20, 25]  // Self + Emma

personCheckPassed = 25 in [20, 25] → true
userAccessLevel = READ_WRITE (FAMILY link)
satisfies(READ_WRITE) → true

Person check: PASSED ✓

Step 5: Combine

accessGranted = orgCheckPassed && personCheckPassed
              = true && true
              = true

Result: GRANTED ✓

Sarah can update Emma’s entry in the Summer Marathon.

Scenario: Access Denied Example

Same user (Sarah) trying to access:

Entity: EventEntry for Michael (personId = 30) in Summer Marathon

Org Check

entityOrgId = 10
userOrgIds = [10]

orgCheckPassed = true ✓

Person Check

entityPersonId = 30  // Michael
userPersonIds = [20, 25]  // Sarah, Emma

personCheckPassed = 30 in [20, 25] → false ✗

Result

accessGranted = true && false = false
Result: DENIED ✗

Sarah cannot access Michael’s entry even though the event is in her organization, because she doesn’t have access to Michael’s person data.

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);

Cache Invalidation

Caches are evicted when permissions change:

  • User’s org links are modified → Evict userOrgAccess

  • User’s person links are modified → Evict userPersonAccess

  • User is deleted → Evict all caches for that user

Performance Optimization

Early Exit

The algorithm can exit early in several cases:

  1. Admin bypass → Immediate grant

  2. First dimension fails (composite-scoped) → Immediate deny (skip second check)

  3. Empty accessible set → Immediate deny

Batch Processing

For bulk operations, accessible IDs are computed once:

Set<Long> userOrgIds = getUserOrgIds(username, READ);
List<Event> events = eventRepository.findByOrgIdIn(userOrgIds);
// All events returned are already authorized

Query-Level Filtering

JPA Specifications apply security at the database level:

// Security filter in WHERE clause
SELECT e FROM Event e WHERE e.orgId IN (:orgIds)

// Instead of fetching all and filtering in memory

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

Inactive links are excluded:

for (LinkedOrg link : user.getLinkedOrganisations()) {
    if (link.isActive()) {  // Only active links
        userOrgIds.add(link.getOrganisation().getId());
    }
}

Time-Bound Access

Links can have temporal restrictions:

Instant now = Instant.now();
if (link.getValidFrom() != null && now.isBefore(link.getValidFrom())) {
    continue;  // Not yet valid
}
if (link.getValidTo() != null && now.isAfter(link.getValidTo())) {
    continue;  // Expired
}

Next Steps