Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/adr/0142-employee-profile-assignment-history-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# ADR 0142: Purpose-bound employee assignment-history read

- **Status:** Accepted on active PR #142; not protected-main truth until integrated.
- **Date:** 2026-08-28
- **Owners:** Orgmetra People API / HRIS core
- **Extends:** ADR 0003 (bitemporal HRIS data), ADR 0008 (purpose-bound PII authorization)

## Decision

The employee profile reads assignment history through a read-only People API boundary that authorizes the exact tenant, person, purpose, operation, and requested field set **before** calling the persistence adapter. The persistence adapter remains injected; this slice does not introduce direct SQL or a second Assignment source of truth.

Each returned row carries separate business-effective (`effective_from`, `effective_to`) and system-recorded (`recorded_from`, `recorded_to`) coordinates. `known_at` selects the half-open recorded interval `[recorded_from, recorded_to)`. Trust-bearing system instants require an exact built-in `datetime` paired with Python's built-in fixed-offset `timezone` at zero offset; caller-defined `tzinfo` providers are rejected before protected retrieval or row use so validation, comparison, and canonical rendering cannot depend on mutable user-supplied timezone behavior. Orgmetra then revalidates tenant/person scope, recorded-time visibility, row type, and assignment identity uniqueness because persistence output is untrusted at the service boundary.

Only fields granted by the purpose-bound authorization decision are emitted. Assignment identity is **not** an unconditional envelope field: a caller that is authorized only for `effective_from` receives only `effective_from`. This prevents a row identifier from becoming an accidental side channel around field minimization.

Results are deterministically ordered by business-effective start and assignment UUID. Allocation values use an exact four-decimal `Decimal` representation in `(0, 1.0000]`; the read path does not infer FTE semantics beyond the authoritative Assignment fact.

## Security and privacy consequences

This boundary follows resource-centric, per-request authorization. NIST SP 800-207 describes authorization before establishing access to an enterprise resource, while SP 800-207A extends identity-based granular policy enforcement to application and service boundaries. The implementation therefore delegates policy evaluation to Orgmetra's existing Keyverse adapter contract rather than embedding a second authorization engine.

The response intentionally excludes display name, contact data, compensation, ratings, assessments, candidate data, credentials, prompts, and model output. A caller may request only explicitly supported assignment-history fields, and schema drift fails closed.

## Data consequences

This ADR does not change the normalized Job / Position / Employment / Assignment model. It exposes historical Assignment versions for one authorized person while preserving the distinction between business time and system-recorded time required by ADR 0003. A future PostgreSQL adapter must remain tenant-scoped/RLS-governed and must not bypass the People service with cross-service application-table SQL.

## Verification

PR #142 must demonstrate:

1. denied fields cause zero persistence calls;
2. tenant/person or system-time mismatches fail closed;
3. caller-defined UTC-looking timezone providers fail closed before protected retrieval or persisted row use;
4. duplicate visible assignment identities fail closed;
5. only authorized fields are returned;
6. canonical allocation/time representations and deterministic ordering;
7. exact 100% owned People API statement and branch coverage on the current PR head;
8. applicable Foundation, SAST, Security, Recovery, and central required-workflow evidence before any integration decision.
17 changes: 17 additions & 0 deletions docs/doctoring/employee-profile-assignment-history-references.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Employee profile assignment-history read — primary references

## Current authoritative references

National Institute of Standards and Technology. (2020). *Zero trust architecture* (NIST Special Publication 800-207). U.S. Department of Commerce. https://doi.org/10.6028/NIST.SP.800-207

National Institute of Standards and Technology. (2023). *A zero trust architecture model for access control in cloud-native applications in multi-cloud environments* (NIST Special Publication 800-207A). U.S. Department of Commerce. https://doi.org/10.6028/NIST.SP.800-207A

## Why these sources matter to PR #142

NIST SP 800-207 treats authentication and authorization as resource-access decisions rather than implicit consequences of network location. SP 800-207A extends granular identity-based policy enforcement to application/service boundaries. PR #142 applies that principle narrowly: the People API authorizes the exact tenant, person, purpose, operation, and requested fields before the protected assignment-history port is called.

These references support the **authorization boundary**, not a claim of NIST certification or compliance. Orgmetra's bitemporal representation remains governed by its own accepted ADR 0003 and authoritative Assignment model. The references do not justify deriving employment decisions, ratings, compensation actions, or inferred worker characteristics from history records.

## Review date

Rechecked against official NIST publication pages on 2026-08-28. Re-review these references if NIST publishes a superseding final revision that materially changes application-level authorization guidance.
36 changes: 36 additions & 0 deletions docs/traceability/employee-profile-assignment-history-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Employee profile assignment-history read traceability

## Product requirement

Protected `docs/PRD.md` lists **Employee profile with bitemporal assignment history** as a P1 HRIS requirement. PR #142 owns only the backend governed-read slice needed to expose Assignment history to an employee-profile surface.

## Protected-main truth consumed

- `packages/hris-kernel/src/orgmetra_hris_kernel/facts.py` defines immutable `AssignmentFact` identity, Employment/Person/Position binding, exact allocation, business-effective interval, and system-recorded interval.
- `services/people-api/src/orgmetra_people_api/authorization.py` delegates field access to the existing purpose-bound Keyverse adapter contract.
- `services/people-api/src/orgmetra_people_api/people.py` establishes the People API pattern that authorizes before protected repository access and revalidates resolved target scope.
- `.github/workflows/people-api-quality.yml` requires exact 100% People API statement and branch coverage.

## PR #142 active implementation

| Requirement | Production boundary | Regression evidence |
| --- | --- | --- |
| Authorize before protected retrieval | `read_assignment_history()` calls `authorize_resource_fields()` before `AssignmentHistoryReadPort` | denied-field test asserts zero port calls |
| Preserve business and system time separately | `AssignmentHistoryRecord.effective_*` and `.recorded_*` | visible/history ordering and recorded-cutoff regressions |
| Deterministic UTC trust boundary | `known_at` and recorded instants require an exact `datetime` using Python's built-in fixed-offset `timezone` at zero offset | caller-defined UTC-looking `tzinfo` providers fail before protected retrieval and at persistence-row construction |
| Tenant/person isolation | service revalidates every returned row | other-tenant and other-person rows fail closed |
| Half-open system-time visibility | `[recorded_from, recorded_to)` at exact `known_at` | future-recorded and `recorded_to == known_at` rows fail closed |
| Field minimization | output is built only from `decision.authorized_fields` | effective-only policy does not leak assignment identity |
| No reflective schema expansion | explicit supported-field encoder requires an exact built-in `str` | unknown fields and string-subclass fields fail closed |
| Deterministic history | sort by effective start then assignment UUID | reversed persistence order produces deterministic business-time order |
| Exact allocation evidence | finite four-decimal `Decimal` in `(0, 1.0000]` | NaN, zero, >1, and noncanonical scale rejected |
| Trust-bearing identity integrity | operational identity requires exact built-in `UUID`, not subclasses or sentinel values | UUID subclasses plus Nil/Max sentinels are rejected |
| Persistence runtime integrity | exact tuple + exact row type + `AssignmentHistoryRecord.assert_runtime_integrity()` immediately after retrieval | mutable container, unsupported row type, and post-construction NaN reinjection fail closed |

## Scope exclusions

PR #142 does **not** create or change Assignment, Employment, Position, Person, candidate, compensation, performance, or decision records. It adds no UI geometry and does not write to Keyverse or another CWL repository. A PostgreSQL persistence adapter and employee-profile UI wiring remain separate follow-on work and must reuse this contract rather than bypass it.

## Merge evidence rule

Only evidence bound to the final unchanged PR #142 head is applicable. Pending, queued, skipped, cancelled, predecessor-head, status-only, or model-only evidence is non-passing. Reviews/checks from another PR do not transfer.
24 changes: 19 additions & 5 deletions services/people-api/src/orgmetra_people_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
"""Request-edge, governed read, confirmed-hire, and People mutation contracts."""

from orgmetra_people_api.assignment_history import (
AssignmentHistoryIntegrityError,
AssignmentHistoryReadPort,
AssignmentHistoryRecord,
AuthorizedAssignmentHistoryEntry,
AuthorizedAssignmentHistoryView,
read_assignment_history,
)
from orgmetra_people_api.auth import (
AuthenticatedPrincipal,
AuthenticationFailed,
Expand Down Expand Up @@ -45,13 +53,22 @@
from orgmetra_people_api.postgres_mutations import PostgresPeopleMutationPort

__all__ = [
"AssignmentHistoryIntegrityError",
"AssignmentHistoryReadPort",
"AssignmentHistoryRecord",
"AssignmentMutationCommand",
"AssignmentMutationResult",
"AuthenticatedPrincipal",
"AuthenticationFailed",
"AuthorizedAssignmentHistoryEntry",
"AuthorizedAssignmentHistoryView",
"AuthorizedWorkerPeopleView",
"EmploymentMutationCommand",
"EmploymentMutationResult",
"HireAcceptanceAsgiApp",
"HireAcceptanceCommand",
"HireAcceptancePort",
"HireAcceptanceResult",
"HireAcceptanceAsgiApp",
"HireDecisionIntegrityError",
"HireDecisionNotFound",
"PeopleAsgiApp",
Expand All @@ -67,10 +84,6 @@
"PostgresHireAcceptancePort",
"PostgresPeopleMutationPort",
"PostgresPeopleReadPort",
"AssignmentMutationCommand",
"AssignmentMutationResult",
"EmploymentMutationCommand",
"EmploymentMutationResult",
"TokenAuthenticator",
"WorkerPeopleRecord",
"accept_confirmed_hire",
Expand All @@ -79,5 +92,6 @@
"create_employment_record",
"create_position_record",
"extract_bearer_token",
"read_assignment_history",
"read_worker_people_record",
]
Loading
Loading