Orpheus Systems Architecture

5. Visibility by filtering in the web part rather than item-level permissions

5. Visibility by filtering in the web part rather than item-level permissions

Date: 2026-10-22

Status

Accepted


Problem

Contractors should see only their own contracts and managers only the ones they manage. SharePoint can enforce this with item-level permissions, but that requires breaking inheritance per item (event receiver or flow), makes provisioning heavier, and the service also needs to update OnSiteUsedDays / OffSiteUsedDays on Contracts on every entry write.


Decision

We will keep all three lists at site-default permissions this iteration and enforce visibility by filtering in the web part / data service: contractors get Contractor eq me, managers get ContractManager eq me. Role is derived at runtime in order: appears as Contractor on at least one contract → contractor; appears as ContractManager → manager; member of the site Owners group → admin; roles can combine. Contractors therefore hold Contribute on Contracts; the form customizer, not permissions, keeps them out of contract editing. This is agreed to be sufficient for now.

Recorded alternative (hardening option, not built): per-item permission breaking on Contracts and CalendarEntries via a provisioning-time event receiver or Power Automate, plus a read-restricted view for the lists. Tracked as risk R-SEC-1 and roadmap item RM-2.

Role-based filtering instead of item-level permissions

flowchart TB
  U["Signed-in user"] --> R{"Derive roles<br/>at runtime,<br/>roles combine"}
  R -->|"a read fails"| DENY["No role:<br/>access denied<br/>(fail closed)"]
  subgraph CHOSEN["Chosen: filtering"]
    C["Contractor<br/>filter:<br/>Contractor eq me"]
    M["Manager<br/>filter:<br/>ContractManager eq me<br/>may create contracts"]
    A["Admin<br/>no role filter"]
    HR["HR, agent only<br/>all contracts,<br/>read only"]
    FORM["Form customizer<br/>create: Admin or Manager<br/>edit: Admin or the<br/>contract's own manager"]
  end
  R -->|"Contractor<br/>on a contract"| C
  R -->|"ContractManager on a contract,<br/>or member of<br/>Orpheus Contract Managers"| M
  R -->|"IsSiteAdmin, or in the<br/>associated Owners group"| A
  R -->|"member of<br/>Orpheus HR"| HR
  C --> L
  M --> L
  A --> L
  HR --> L
  FORM --> L
  L[("Contracts, CalendarEntries,<br/>Holidays<br/>site-default<br/>permissions; contractors<br/>hold Contribute<br/>on Contracts")]
  subgraph ALT["Not built: R-SEC-1, RM-2"]
    IP["Per-item permissions on<br/>Contracts, CalendarEntries<br/>event receiver or<br/>Power Automate"]
    RV["Read-restricted<br/>views"]
  end
  IP -.->|"hardening option"| L
  RV -.-> L

Considered Options

Option Summary Pros Cons
✅ Filtering in the data service Site-default permissions; role-based OData filters Simple provisioning; service can maintain used-days columns; adequate for a demo/portfolio tenant Users with list UI access can read or edit others' rows; relies on the form customizer for contract editing
Item-level permissions (event receiver / Power Automate) Break inheritance per item on create Enforced by the platform Extra moving parts; used-days writes need a privileged writer; re-provisioning complexity
Separate lists per contractor Partition by user Platform-enforced Does not scale; web part list picker becomes per user
Read-restricted views only Hide rows in views Easy Not a security boundary

Consequences

Positive:

Negative / Trade-offs:

Risks: | Risk ID | Description | Impact (E/H/M/L) | Mitigation | |---------|-------------|------------------|------------| | R-SEC-1 | A contractor reads or edits another contractor's rows via the list UI | H | Hardening path above; pick up RM-2 before any multi-tenant or sensitive use |


Amendments

Amendment (2026-10-23) — The decision stands: visibility is still filtering on site-default permissions. The role rules it relies on changed in wave 2 and are now: