Entity Classification

Overview

Entities in the multi-dimensional security system are classified into five distinct security types. Each type determines how security is applied and which dimensions are checked during authorization.

Classification Types

A. Org-Scoped Only

Entities that belong to an organization and have no personal data component.

Characteristics

  • Contains an orgId field (direct)

  • Implements OrganisationScoped interface

  • Security checked via organizational dimension only

  • Person dimension is not applicable

Examples

Entity Description

Event

A sporting event organized by a club or federation

Organization

The organization entity itself

Venue

A location owned/managed by an organization

Competition

A competition series run by an organization

Security Rule

User can access entity IF:
  user.accessibleOrgIds.contains(entity.orgId)
  AND user has required access level (READ or READ_WRITE)

Implementation

@Entity
public class Event implements OrganisationScoped {
    @Id
    private Long id;

    @Column(nullable = false)
    private Long orgId;  // Direct org ownership

    private String name;
    // ...
}

B. Person-Scoped Only

Entities containing personal data with no organizational component.

Characteristics

  • Contains a personId field (direct)

  • Implements PersonScoped interface

  • Security checked via personal dimension only

  • Org dimension is not applicable

Examples

Entity Description

PersonProfile

Personal profile information (emergency contacts, medical info)

PersonalBest

Individual athlete’s personal best records

MedicalInfo

Medical history and information

EmergencyContact

Emergency contact details

Security Rule

User can access entity IF:
  user.accessiblePersonIds.contains(entity.personId)
  AND user has required access level (READ or READ_WRITE)

Implementation

@Entity
public class PersonProfile implements PersonScoped {
    @Id
    private Long id;

    @Column(nullable = false)
    private Long personId;  // Direct person ownership

    private String emergencyContact;
    // ...
}

C. Composite-Scoped

Entities that implement both organizational and personal security interfaces. Each dimension is evaluated independently.

Characteristics

  • Contains both orgId and personId (direct or transitive)

  • Implements both OrganisationScoped AND PersonScoped interfaces

  • Each dimension is checked separately using its respective methods

  • Access requires passing BOTH dimension checks independently (AND logic)

  • Most restrictive security type

Examples

Entity Description

EventEntry

A person’s entry into an event (event→org, person→personal)

RaceResult

A person’s result in a race (race→event→org, person→personal)

Payment

Payment for an entry (event→org, person→payer)

TeamMembership

Person joining a team (team→org, person→member)

Security Rule

User can access entity IF:
  (user.accessibleOrgIds.contains(entity.orgId)
   AND user has required org access level)
  AND
  (user.accessiblePersonIds.contains(entity.personId)
   AND user has required person access level)

Implementation

@Entity
public class EventEntry implements OrganisationScoped, PersonScoped {
    @Id
    private Long id;

    @ManyToOne(optional = false)
    private Event event;  // Provides orgId (transitive)

    @Column(nullable = false)
    private Long personId;  // Direct person

    @Override
    public Long getOrgId() {
        return event != null ? event.getOrgId() : null;
    }

    @Override
    public void setOrgId(Long orgId) {
        throw new UnsupportedOperationException("OrgId derived from event");
    }
}

D. Transitive Org-Scoped

Child entities that inherit organizational access through a parent entity.

Characteristics

  • No direct orgId field

  • Access to org is via parent relationship

  • Implements helper method to get orgId from parent

  • Org dimension checked via join to parent

Examples

Entity Parent Entity Relationship

Race

Event

Race belongs to Event, Event has orgId

Heat

Race

Heat belongs to Race, Race→Event→orgId

TeamMember

Team

Member belongs to Team, Team has orgId

Security Rule

User can access entity IF:
  user.accessibleOrgIds.contains(entity.parent.orgId)
  AND user has required access level

Implementation

@Entity
public class Race {
    @Id
    private Long id;

    @ManyToOne(optional = false)
    private Event event;  // Parent with orgId

    private String name;

    // Helper for security
    public Long getOrgId() {
        return event != null ? event.getOrgId() : null;
    }
}

E. Transitive Person-Scoped

Child entities that inherit personal access through a parent entity.

Characteristics

  • No direct personId field

  • Access to person is via parent relationship

  • Implements helper method to get personId from parent

  • Person dimension checked via join to parent

Examples

Entity Parent Entity Relationship

PersonAddress

PersonProfile

Address belongs to Profile, Profile has personId

TrainingLog

Athlete

Log belongs to Athlete, Athlete has personId

MedicalRecord

Person

Record belongs to Person

Security Rule

User can access entity IF:
  user.accessiblePersonIds.contains(entity.parent.personId)
  AND user has required access level

Implementation

@Entity
public class PersonAddress {
    @Id
    private Long id;

    @ManyToOne(optional = false)
    private PersonProfile profile;  // Parent with personId

    private String street;

    // Helper for security
    public Long getPersonId() {
        return profile != null ? profile.getPersonId() : null;
    }
}

Classification Decision Tree

Use this decision tree to classify your entities:

Does the entity belong to an organization?
│
├─ YES → Does it also relate to a specific person?
│        │
│        ├─ YES → DUAL-SCOPED (C)
│        │
│        └─ NO → Is orgId direct or via parent?
│                 │
│                 ├─ Direct → ORG-SCOPED ONLY (A)
│                 │
│                 └─ Via parent → TRANSITIVE ORG-SCOPED (D)
│
└─ NO → Does it relate to a specific person?
         │
         ├─ YES → Is personId direct or via parent?
         │        │
         │        ├─ Direct → PERSON-SCOPED ONLY (B)
         │        │
         │        └─ Via parent → TRANSITIVE PERSON-SCOPED (E)
         │
         └─ NO → Not security-scoped (rare, admin-only entities)

Summary Table

Type Org Check Person Check Example

A - Org-Scoped

Direct orgId

None

Event, Venue

B - Person-Scoped

None

Direct personId

PersonProfile, MedicalInfo

C - Composite-Scoped

Direct or transitive

Direct or transitive

EventEntry, Payment

D - Transitive Org

Via parent

None

Race (via Event)

E - Transitive Person

None

Via parent

PersonAddress (via Profile)

Multi-Level Transitive Access

Some entities may require traversing multiple levels:

Example: Race Result

RaceResult
  → Race (parent 1)
    → Event (parent 2)
      → orgId (final)

Security check must join through multiple levels:

user.accessibleOrgIds.contains(result.race.event.orgId)

Example with Composite: Heat Result

HeatResult (composite-scoped, multi-level org)
  → Heat
    → Race
      → Event
        → orgId
  → personId (direct)

Security requires:

  1. Org access via 3-level join (Heat→Race→Event→orgId)

  2. Person access via direct personId

Classification Best Practices

Keep It Simple

  • Prefer direct orgId/personId fields when possible

  • Limit transitive depth to 2-3 levels maximum

  • Consider denormalizing for frequently accessed paths

Consistent Patterns

  • Always use the same parent relationship for org/person derivation

  • Document the access path clearly in entity comments

  • Use consistent naming (orgId, personId, not organizationId, etc.)

Performance Considerations

  • Transitive access requires JOINs - ensure proper indexing

  • Consider materialized views for complex multi-level access

  • Cache frequently accessed access paths

Testing Each Type

Each classification type should have dedicated tests:

// A - Org-Scoped
@Test
public void userCanAccessEventsInAccessibleOrgs()

// B - Person-Scoped
@Test
public void userCanAccessLinkedPersonProfiles()

// C - Composite-Scoped
@Test
public void userCanAccessEntriesWithBothOrgAndPersonAccess()

// D - Transitive Org
@Test
public void userCanAccessRacesViaEventOrg()

// E - Transitive Person
@Test
public void userCanAccessAddressesViaProfilePerson()

Next Steps