Core Entities

Overview

This page describes the core domain entities that form the foundation of the multi-dimensional security system.

Marker Interfaces

The security system uses marker interfaces to identify entity security classifications.

OrganisationScoped

Entities belonging to an organization implement this interface.

/**
 * Marker interface for org-scoped entities
 */
public interface OrganisationScoped {
    Long getOrgId();
    void setOrgId(Long orgId);
}

PersonScoped

Entities containing person-specific data implement this interface.

/**
 * Marker interface for person-scoped entities
 */
public interface PersonScoped {
    Long getPersonId();
    void setPersonId(Long personId);
}

Composite-Scoped Entities

Entities requiring both organizational and personal authorization implement both OrganisationScoped and PersonScoped interfaces directly. This compositional approach is more flexible and scalable than creating a combined interface. Each dimension is checked independently.

/**
 * Example: Composite-scoped entity implementing both interfaces
 */
@Entity
public class EventEntry implements OrganisationScoped, PersonScoped {
    // Implements both getOrgId/setOrgId and getPersonId/setPersonId
    // Each security dimension is checked separately - both must pass
}

This approach allows each security dimension to be evaluated independently and makes it easier to add additional security facets in the future without creating multiple interface combinations.

Enumerations

AccessLevel

Defines the level of access a user has.

package com.example.security.domain;

/ * Access level enumeration for multi-dimensional security. * Defines the level of access a user has to organizations or persons. */ public enum AccessLevel { / * Read-only access. Can view/query data but cannot create, update, or delete. */ READ, / * Full access. Can view, create, update, and delete data. */ READ_WRITE; / * Check if this access level satisfies a required access level. * * @param required The required access level * @return true if this access level is sufficient */ public boolean satisfies(AccessLevel required) { if (required == READ) { // Both READ and READ_WRITE satisfy READ requirement return true; } // Only READ_WRITE satisfies READ_WRITE requirement return this == READ_WRITE; } }

Key features:

  • Two levels: READ and READ_WRITE

  • satisfies() method implements hierarchy logic

  • READ_WRITE satisfies READ requirements

LinkType

Categorizes the type of person-to-person relationship.

package com.example.security.domain;

/ * Type of person-to-person relationship link. * Categorizes the nature of delegated access between principals. */ public enum LinkType { / * Family relationship (parent-child, spouse, etc.). * Typically grants READ_WRITE access. */ FAMILY, / * Team manager to team members relationship. * Typically grants READ access to view team roster. */ TEAM_MANAGER, / * Coach to athletes relationship. * Typically grants READ_WRITE access to manage athlete data. */ COACH, / * Legal guardian relationship. * Typically grants READ_WRITE access. */ GUARDIAN, / * General delegation for temporary or special access. * Access level varies based on delegation purpose. */ DELEGATE }

Common use cases:

  • FAMILY: Parent-child relationships

  • TEAM_MANAGER: Team manager to team members

  • COACH: Coach to athletes

  • GUARDIAN: Legal guardian relationships

  • DELEGATE: General delegation

SecurityType

Classifies entities by their security requirements.

package com.example.security.domain;

/ * Security classification for entities. * Determines which security dimensions apply and how authorization is performed. */ public enum SecurityType { / * Entity has direct orgId field. * Security checked via organizational dimension only. * Example: Event, Organization */ ORG_SCOPED, / * Entity has direct personId field. * Security checked via personal dimension only. * Example: PersonProfile, MedicalInfo */ PERSON_SCOPED, / * Entity has both orgId and personId fields (direct or transitive). * Security must pass BOTH dimensional checks (AND logic). * Example: EventEntry, Payment */ DUAL_SCOPED, / * Entity inherits orgId through parent relationship. * Security checked via join to parent’s orgId. * Example: Race (via Event) */ TRANSITIVE_ORG, / * Entity inherits personId through parent relationship. * Security checked via join to parent’s personId. * Example: PersonAddress (via PersonProfile) */ TRANSITIVE_PERSON, / * Entity has direct personId but transitive orgId. * Common for dual-scoped entities where org comes from a parent. * Example: EventEntry (personId direct, orgId via Event) */ DUAL_TRANSITIVE_ORG, / * No security filtering applied. * Rare - typically for admin-only or public reference data. */ NONE }

Used by QueryService implementations to determine which security specifications to apply.

User and Authentication

OrgUser

The authenticated user account entity.

@Entity
public class OrgUser {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false, length = 50)
    private String login;

    @Column(nullable = false)
    private String passwordHash;

    @Column(nullable = false)
    private boolean activated = true;

    @ManyToOne(optional = false)
    @JoinColumn(name = "primary_org_id")
    private Organisation primaryOrganisation;

    @OneToMany(mappedBy = "orgUser", fetch = FetchType.EAGER)
    private Set<LinkedOrg> linkedOrganisations = new HashSet<>();

    @ManyToOne(optional = false)
    @JoinColumn(name = "principal_person_id")
    private Principal principalPerson;

    @OneToMany(mappedBy = "fromPrincipal", fetch = FetchType.EAGER)
    private Set<LinkedPerson> linkedPersons = new HashSet<>();

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
        name = "org_user_authority",
        joinColumns = {@JoinColumn(name = "user_id")},
        inverseJoinColumns = {@JoinColumn(name = "authority_name")}
    )
    private Set<Authority> authorities = new HashSet<>();

    // Standard getters and setters
}

Key relationships:

  • One primary organization (mandatory, READ_WRITE access)

  • Multiple linked organizations (optional, with explicit access levels)

  • One principal person (mandatory, the user’s identity)

  • Multiple linked persons (optional, delegated access)

  • Multiple authorities/roles

Authority

Role/authority entity for Spring Security.

@Entity
public class Authority implements Serializable {

    @Id
    @Column(length = 50)
    private String name;

    // getters and setters
}

Common authorities:

  • ROLE_ADMIN: System administrator

  • ROLE_USER: Regular user

  • ROLE_GLOBAL_VIEWER: Can read all organizations

  • ROLE_AUDITOR: Can audit all data

Organization Dimension

Organisation

The organization entity.

@Entity
public class Organisation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(length = 50)
    private String shortName;

    @Column(nullable = false)
    private boolean active = true;

    @Enumerated(EnumType.STRING)
    @Column(length = 50)
    private OrganisationType type;

    // Contact information
    private String email;
    private String phone;
    private String website;

    // Address fields
    private String addressLine1;
    private String addressLine2;
    private String city;
    private String postcode;
    private String country;

    @Column(columnDefinition = "TEXT")
    private String description;

    // Audit fields
    @CreatedDate
    private Instant createdDate;

    @LastModifiedDate
    private Instant lastModifiedDate;

    // getters and setters
}

LinkedOrg

Links a user to a secondary organization with specific access level.

@Entity
@Table(
    name = "linked_org",
    uniqueConstraints = @UniqueConstraint(
        columnNames = {"org_user_id", "organisation_id"}
    )
)
public class LinkedOrg {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "org_user_id")
    private OrgUser orgUser;

    @ManyToOne(optional = false)
    @JoinColumn(name = "organisation_id")
    private Organisation organisation;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private AccessLevel accessLevel;

    @Column(nullable = false)
    private boolean active = true;

    // Optional time-bound access
    private Instant validFrom;
    private Instant validTo;

    // Audit fields
    @CreatedDate
    private Instant createdDate;

    @CreatedBy
    private String createdBy;

    @LastModifiedDate
    private Instant lastModifiedDate;

    @LastModifiedBy
    private String lastModifiedBy;

    // Business logic
    public boolean isCurrentlyValid() {
        Instant now = Instant.now();

        if (validFrom != null && now.isBefore(validFrom)) {
            return false;
        }
        if (validTo != null && now.isAfter(validTo)) {
            return false;
        }
        return active;
    }

    // getters and setters
}

Features:

  • Unique constraint: One user-org pair per entry

  • Active flag for soft delete

  • Time-bound access with validFrom/validTo

  • Full audit trail

Person Dimension

Principal

The principal person entity representing the authenticated user’s identity.

@Entity
public class Principal {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String firstName;

    @Column(nullable = false, length = 100)
    private String lastName;

    @Column(length = 100)
    private String middleName;

    @Column(nullable = false)
    private LocalDate dateOfBirth;

    @Enumerated(EnumType.STRING)
    @Column(length = 20)
    private Gender gender;

    @Column(length = 100)
    private String nationality;

    // Contact
    @Column(length = 100)
    private String email;

    @Column(length = 20)
    private String phone;

    @OneToOne(mappedBy = "principalPerson")
    private OrgUser orgUser;

    // Audit fields
    @CreatedDate
    private Instant createdDate;

    @LastModifiedDate
    private Instant lastModifiedDate;

    // Computed fields
    public String getFullName() {
        return firstName + " " + lastName;
    }

    public Integer getAge() {
        return Period.between(dateOfBirth, LocalDate.now()).getYears();
    }

    // getters and setters
}

LinkedPerson

Links a principal to another person with delegated access.

@Entity
@Table(
    name = "linked_person",
    uniqueConstraints = @UniqueConstraint(
        columnNames = {"from_principal_id", "to_person_id"}
    )
)
public class LinkedPerson {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "from_principal_id")
    private Principal fromPrincipal;

    @Column(name = "to_person_id", nullable = false)
    private Long toPersonId;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private AccessLevel accessLevel;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 30)
    private LinkType linkType;

    @Column(nullable = false)
    private boolean active = true;

    // Optional time-bound access
    private Instant validFrom;
    private Instant validTo;

    // Optional notes about the relationship
    @Column(columnDefinition = "TEXT")
    private String notes;

    // Audit fields
    @CreatedDate
    private Instant createdDate;

    @CreatedBy
    private String createdBy;

    @LastModifiedDate
    private Instant lastModifiedDate;

    @LastModifiedBy
    private String lastModifiedBy;

    // Business logic
    public boolean isCurrentlyValid() {
        Instant now = Instant.now();

        if (validFrom != null && now.isBefore(validFrom)) {
            return false;
        }
        if (validTo != null && now.isAfter(validTo)) {
            return false;
        }
        return active;
    }

    // getters and setters
}

Features:

  • Links from one principal to another person (by ID)

  • Supports different relationship types (FAMILY, TEAM_MANAGER, etc.)

  • Access level per relationship

  • Time-bound and activatable

  • Full audit trail

Database Schema

ERD Overview

┌──────────────┐
│   OrgUser    │
├──────────────┤
│ id           │◄──┐
│ login        │   │
│ passwordHash │   │
└──────────────┘   │
       │           │
       │ 1         │
       │           │
       │ *         │
┌──────────────┐   │
│  LinkedOrg   │   │
├──────────────┤   │
│ id           │   │
│ org_user_id  │───┘
│ org_id       │───┐
│ accessLevel  │   │
│ active       │   │
└──────────────┘   │
                   │ *
                   │
                ┌──▼──────────┐
                │Organisation │
                ├─────────────┤
                │ id          │
                │ name        │
                │ active      │
                └─────────────┘

┌──────────────┐
│   OrgUser    │
├──────────────┤
│ principal_id │───┐
└──────────────┘   │
                   │ 1
                   │
                ┌──▼──────────┐
                │  Principal  │
                ├─────────────┤
                │ id          │◄──┐
                │ firstName   │   │
                │ lastName    │   │
                └─────────────┘   │
                       │          │
                       │ 1        │
                       │          │
                       │ *        │
                ┌──────▼──────┐   │
                │LinkedPerson │   │
                ├─────────────┤   │
                │ id          │   │
                │ from_pr_id  │───┘
                │ to_person_id│
                │ accessLevel │
                │ linkType    │
                │ active      │
                └─────────────┘

Key Tables

Table Purpose Key Fields

org_user

User accounts

login, primary_org_id, principal_person_id

linked_org

Secondary org access

org_user_id, organisation_id, access_level

organisation

Organizations

name, active

principal

Person identities

firstName, lastName, dateOfBirth

linked_person

Delegated person access

from_principal_id, to_person_id, access_level, link_type

Next Steps