diff --git a/CHANGELOG.md b/CHANGELOG.md index 16454da3d..98d5b3e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ All notable changes to Orgmetra will be documented in this file. - `employment_record_version.employment_concurrency_code` constrained to `exclusive` or `concurrent`. - ADR 0005 for exclusive employment and staffable seats. - `orgmetra_hris_kernel` 0.3.0 with identity-scoped bitemporal resolution, assignment-employment coverage, allocation-portfolio checks, and a Memorial Hospital RN correction case at 100% statement and branch coverage. -- `employment_record_version` and `position_record_version` so employment and position identity stay stable across retroactive corrections. +- `employment_record_version` and `position_record_version` so corrections no longer mint a new employment or position identifier. - `assignment_record.employment_record_id` bound to the same person as the covering employment. - `orgmetra_keyverse_adapter` that binds an opaque Keyverse subject to a person and rejects passwords, passkeys, and tokens. - Design tokens for the repeating HR actions: approve, review, correct, request evidence, compare, export, and escalate. @@ -54,6 +54,7 @@ All notable changes to Orgmetra will be documented in this file. - Made assignment coverage status-aware: `active` and `leave` remain staffable while `terminated` and other non-eligible employment statuses fail closed. - Made organization hierarchy reconstruction fail closed on a cycle at the requested tenant, effective day, and knowledge cutoff while ignoring future-recorded and foreign-tenant facts. - Build the outbox due-work index concurrently during migration 0008, requiring that index step to run outside an explicit transaction block so established queues do not block writers while the index is built; pre-index hardening and post-index privileged role setup use separate explicit transactions. +- Active-PR People mutation commands now require exact built-in governance text and hire status values before digesting or persisting high-impact employment evidence. ### Security diff --git a/manifest.json b/manifest.json index f7b6cf55e..ac1648354 100644 --- a/manifest.json +++ b/manifest.json @@ -29,9 +29,9 @@ }, { "path": "CHANGELOG.md", - "sha256": "f2d2e0b488c0440533effa821808f2f17e37d92f8fb586174c2fdb594f760ca5", - "bytes": 17539, - "lines": 77 + "sha256": "9ad6dad273c94c30741522ca87205ff24eb92c53becc8b53739d93acb28126f9", + "bytes": 17697, + "lines": 78 }, { "path": "CLAUDE.md", @@ -353,8 +353,8 @@ }, { "path": "schemas/openapi.yaml", - "sha256": "09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f", - "bytes": 29503, + "sha256": "c37522504d1f6ac6410eaac833dddbf09aacc85572da38a1cf7539541833ea8e", + "bytes": 29511, "lines": 1020 }, { diff --git a/schemas/openapi.yaml b/schemas/openapi.yaml index 0fd397e92..c03ffab0b 100644 --- a/schemas/openapi.yaml +++ b/schemas/openapi.yaml @@ -641,7 +641,7 @@ components: format: uuid allocation_ratio: type: string - pattern: '^(0\.[0-9]{4}|1\.0000)$' + pattern: '^(0\.(?!0000)[0-9]{4}|1\.0000)$' effective_from: type: string format: date diff --git a/services/people-api/src/orgmetra_people_api/hire.py b/services/people-api/src/orgmetra_people_api/hire.py index 6823f4c59..86ff5a625 100644 --- a/services/people-api/src/orgmetra_people_api/hire.py +++ b/services/people-api/src/orgmetra_people_api/hire.py @@ -8,7 +8,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, replace from datetime import date import re from typing import Protocol, runtime_checkable @@ -35,8 +35,8 @@ class HireDecisionIntegrityError(RuntimeError): def _validate_operational_uuid(field_name: str, value: object) -> None: - """Require a real UUID outside Orgmetra's reserved protocol sentinels.""" - if not isinstance(value, UUID) or value.int in (0, _MAX_UUID_INT): + """Require an exact UUID outside Orgmetra's reserved protocol sentinels.""" + if type(value) is not UUID or value.int in (0, _MAX_UUID_INT): raise ValueError(f"{field_name} must be an operational UUID.") @@ -83,7 +83,7 @@ def __post_init__(self) -> None: _validate_operational_uuid(field_name, getattr(self, field_name)) if type(self.effective_from) is not date: raise ValueError("effective_from must be a business date.") - if not isinstance(self.display_name, str): + if type(self.display_name) is not str: raise ValueError("display_name must be a string.") try: self.display_name.encode("utf-8") @@ -95,7 +95,7 @@ def __post_init__(self) -> None: raise ValueError("display_name must not contain control characters.") validate_idempotency_key(self.idempotency_key) if ( - not isinstance(self.employment_status_code, str) + type(self.employment_status_code) is not str or _STATUS_CODE_PATTERN.fullmatch(self.employment_status_code) is None ): raise ValueError("employment_status_code must be a lower snake_case code.") @@ -147,8 +147,14 @@ def accept_confirmed_hire( ``materialize_worker`` operation and ``candidate_worker_conversion`` field; possession of an identity token or purpose string alone is insufficient. """ - if not isinstance(command, HireAcceptanceCommand): + if type(command) is not HireAcceptanceCommand: raise TypeError("command must be a HireAcceptanceCommand") + command = replace(command) + expected_person_record_id = UUID(int=command.person_record_id.int) + expected_employment_record_id = UUID(int=command.employment_record_id.int) + expected_conversion_record_id = UUID( + int=command.candidate_worker_conversion_record_id.int + ) if not isinstance(mutation_port, HireAcceptancePort): raise TypeError("mutation_port must implement HireAcceptancePort") @@ -164,6 +170,13 @@ def accept_confirmed_hire( policy=policy, ) result = mutation_port.accept_hire(command=command, authorization=authorization) - if not isinstance(result, HireAcceptanceResult): + if type(result) is not HireAcceptanceResult: raise TypeError("mutation_port must return HireAcceptanceResult") + HireAcceptanceResult.__post_init__(result) + if ( + result.person_record_id != expected_person_record_id + or result.employment_record_id != expected_employment_record_id + or result.candidate_worker_conversion_record_id != expected_conversion_record_id + ): + raise HireDecisionIntegrityError("hire result identity does not match command") return result diff --git a/services/people-api/src/orgmetra_people_api/mutations.py b/services/people-api/src/orgmetra_people_api/mutations.py index 6baeac684..776c80955 100644 --- a/services/people-api/src/orgmetra_people_api/mutations.py +++ b/services/people-api/src/orgmetra_people_api/mutations.py @@ -10,7 +10,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, replace from datetime import date from decimal import Decimal from hashlib import sha256 @@ -47,26 +47,26 @@ class PeopleMutationIntegrityError(RuntimeError): def _validate_operational_uuid(field_name: str, value: object) -> None: - """Require a real UUID outside Orgmetra's reserved protocol sentinels.""" - if not isinstance(value, UUID) or value.int in (0, _MAX_UUID_INT): + """Require an exact UUID outside Orgmetra's reserved protocol sentinels.""" + if type(value) is not UUID or value.int in (0, _MAX_UUID_INT): raise ValueError(f"{field_name} must be an operational UUID.") def _validate_confirmation(value: object) -> None: """Require one namespaced human-confirmation reference.""" - if not isinstance(value, str) or _REFERENCE_PATTERN.fullmatch(value) is None: + if type(value) is not str or _REFERENCE_PATTERN.fullmatch(value) is None: raise ValueError("confirmation_reference must be a namespaced opaque reference.") def _validate_evidence_version(value: object) -> None: """Require one whitespace-free evidence version token.""" - if not isinstance(value, str) or _VERSION_PATTERN.fullmatch(value) is None: + if type(value) is not str or _VERSION_PATTERN.fullmatch(value) is None: raise ValueError("evidence_version_code must be a whitespace-free version token.") def validate_idempotency_key(value: object) -> str: """Require the same visible-ASCII Idempotency-Key contract as the HTTP boundary.""" - if not isinstance(value, str) or not (_IDEMPOTENCY_MIN <= len(value) <= _IDEMPOTENCY_MAX): + if type(value) is not str or not (_IDEMPOTENCY_MIN <= len(value) <= _IDEMPOTENCY_MAX): raise ValueError("idempotency_key must be 16 to 200 visible ASCII characters.") if any(ord(character) < 0x21 or ord(character) > 0x7E for character in value): raise ValueError("idempotency_key must be 16 to 200 visible ASCII characters.") @@ -83,11 +83,11 @@ def command_route( command: EmploymentMutationCommand | PositionMutationCommand | AssignmentMutationCommand, ) -> str: """Return the durable route that scopes one People mutation idempotency key.""" - if isinstance(command, EmploymentMutationCommand): + if type(command) is EmploymentMutationCommand: return "employment-records" - if isinstance(command, PositionMutationCommand): + if type(command) is PositionMutationCommand: return "position-records" - if isinstance(command, AssignmentMutationCommand): + if type(command) is AssignmentMutationCommand: return "assignment-records" raise TypeError("command must be a governed People mutation command") @@ -99,6 +99,7 @@ def idempotency_record_id( idempotency_key: str, ) -> UUID: """Derive a stable operational identity for one tenant/route/key binding.""" + _validate_operational_uuid("tenant_record_id", tenant_record_id) return uuid5( _IDEMPOTENCY_NAMESPACE, f"{tenant_record_id}:{command_route_value}:{idempotency_key}", @@ -115,9 +116,10 @@ def mutation_command_digest( Generated record identifiers are excluded so a retry that allocates fresh UUIDs still matches the first committed command. """ - if not isinstance(authorization, AuthorizationDecision): + if type(authorization) is not AuthorizationDecision: raise TypeError("authorization must be an AuthorizationDecision") - if isinstance(command, EmploymentMutationCommand): + if type(command) is EmploymentMutationCommand: + EmploymentMutationCommand.__post_init__(command) route = "employment-records" semantic_command: dict[str, object] = { "confirmation_reference": command.confirmation_reference, @@ -127,7 +129,8 @@ def mutation_command_digest( "evidence_version_code": command.evidence_version_code, "person_record_id": str(command.person_record_id), } - elif isinstance(command, PositionMutationCommand): + elif type(command) is PositionMutationCommand: + PositionMutationCommand.__post_init__(command) route = "position-records" semantic_command = { "confirmation_reference": command.confirmation_reference, @@ -137,7 +140,8 @@ def mutation_command_digest( "organization_unit_id": str(command.organization_unit_id), "position_status_code": command.position_status_code, } - elif isinstance(command, AssignmentMutationCommand): + elif type(command) is AssignmentMutationCommand: + AssignmentMutationCommand.__post_init__(command) route = "assignment-records" semantic_command = { "allocation_ratio": _canonical_allocation_ratio(command.allocation_ratio), @@ -191,10 +195,10 @@ def __post_init__(self) -> None: _validate_operational_uuid(field_name, getattr(self, field_name)) if type(self.effective_from) is not date: raise ValueError("effective_from must be a business date.") - if not isinstance(self.employment_status_code, str) or self.employment_status_code not in _EMPLOYMENT_STATUSES: + if type(self.employment_status_code) is not str or self.employment_status_code not in _EMPLOYMENT_STATUSES: raise ValueError("employment_status_code must be active, leave, or terminated.") if ( - not isinstance(self.employment_concurrency_code, str) + type(self.employment_concurrency_code) is not str or self.employment_concurrency_code not in _CONCURRENCY_CODES ): raise ValueError("employment_concurrency_code must be exclusive or concurrent.") @@ -234,7 +238,7 @@ def __post_init__(self) -> None: _validate_operational_uuid(field_name, getattr(self, field_name)) if type(self.effective_from) is not date: raise ValueError("effective_from must be a business date.") - if not isinstance(self.position_status_code, str) or self.position_status_code not in _POSITION_STATUSES: + if type(self.position_status_code) is not str or self.position_status_code not in _POSITION_STATUSES: raise ValueError("position_status_code must be a staffable or closed seat status.") _validate_confirmation(self.confirmation_reference) _validate_evidence_version(self.evidence_version_code) @@ -272,7 +276,7 @@ def __post_init__(self) -> None: _validate_operational_uuid(field_name, getattr(self, field_name)) if type(self.effective_from) is not date: raise ValueError("effective_from must be a business date.") - if not isinstance(self.allocation_ratio, Decimal): + if type(self.allocation_ratio) is not Decimal: raise ValueError("allocation_ratio must be a Decimal.") if not self.allocation_ratio.is_finite(): raise ValueError("allocation_ratio must be finite.") @@ -285,37 +289,49 @@ def __post_init__(self) -> None: validate_idempotency_key(self.idempotency_key) +def _validate_replay_command_digest(value: object) -> None: + """Require exact inert replay evidence when a mutation result carries it.""" + if value is not None and type(value) is not str: + raise ValueError("replay_command_digest must be an exact string when present.") + + @dataclass(frozen=True, slots=True) class EmploymentMutationResult: - """Opaque identity returned after one committed employment mutation.""" + """Opaque identity and optional verified-replay evidence for one employment mutation.""" employment_record_id: UUID + replay_command_digest: str | None = None def __post_init__(self) -> None: """Prevent malformed persistence results from crossing the service boundary.""" _validate_operational_uuid("employment_record_id", self.employment_record_id) + _validate_replay_command_digest(self.replay_command_digest) @dataclass(frozen=True, slots=True) class PositionMutationResult: - """Opaque identity returned after one committed position mutation.""" + """Opaque identity and optional verified-replay evidence for one position mutation.""" position_record_id: UUID + replay_command_digest: str | None = None def __post_init__(self) -> None: """Prevent malformed persistence results from crossing the service boundary.""" _validate_operational_uuid("position_record_id", self.position_record_id) + _validate_replay_command_digest(self.replay_command_digest) @dataclass(frozen=True, slots=True) class AssignmentMutationResult: - """Opaque identity returned after one committed assignment mutation.""" + """Opaque identity and optional verified-replay evidence for one assignment mutation.""" assignment_record_id: UUID + replay_command_digest: str | None = None def __post_init__(self) -> None: """Prevent malformed persistence results from crossing the service boundary.""" _validate_operational_uuid("assignment_record_id", self.assignment_record_id) + _validate_replay_command_digest(self.replay_command_digest) @runtime_checkable @@ -354,6 +370,24 @@ def _require_port(mutation_port: object) -> PeopleMutationPort: return mutation_port +def _require_result_identity_or_replay( + *, + result_record_id: UUID, + expected_record_id: UUID, + replay_command_digest: str | None, + command: EmploymentMutationCommand | PositionMutationCommand | AssignmentMutationCommand, + authorization: AuthorizationDecision, + result_name: str, +) -> None: + """Accept a foreign identity only with replay evidence bound to this semantic command.""" + if replay_command_digest is not None: + if replay_command_digest != mutation_command_digest(command=command, authorization=authorization): + raise PeopleMutationIntegrityError(f"{result_name} replay evidence does not match command") + return + if result_record_id != expected_record_id: + raise PeopleMutationIntegrityError(f"{result_name} result identity does not match command") + + def create_employment_record( *, principal: AuthenticatedPrincipal, @@ -363,8 +397,10 @@ def create_employment_record( mutation_port: PeopleMutationPort, ) -> EmploymentMutationResult: """Authorize the exact employment target before persisting worker employment truth.""" - if not isinstance(command, EmploymentMutationCommand): + if type(command) is not EmploymentMutationCommand: raise TypeError("command must be an EmploymentMutationCommand") + command = replace(command) + expected_employment_record_id = UUID(int=command.employment_record_id.int) port = _require_port(mutation_port) authorization = authorize_resource_fields( principal=principal, @@ -378,8 +414,17 @@ def create_employment_record( policy=policy, ) result = port.create_employment(command=command, authorization=authorization) - if not isinstance(result, EmploymentMutationResult): + if type(result) is not EmploymentMutationResult: raise TypeError("mutation_port must return EmploymentMutationResult") + EmploymentMutationResult.__post_init__(result) + _require_result_identity_or_replay( + result_record_id=result.employment_record_id, + expected_record_id=expected_employment_record_id, + replay_command_digest=result.replay_command_digest, + command=command, + authorization=authorization, + result_name="employment", + ) return result @@ -392,8 +437,10 @@ def create_position_record( mutation_port: PeopleMutationPort, ) -> PositionMutationResult: """Authorize the exact position target before persisting a staffable seat.""" - if not isinstance(command, PositionMutationCommand): + if type(command) is not PositionMutationCommand: raise TypeError("command must be a PositionMutationCommand") + command = replace(command) + expected_position_record_id = UUID(int=command.position_record_id.int) port = _require_port(mutation_port) authorization = authorize_resource_fields( principal=principal, @@ -407,8 +454,17 @@ def create_position_record( policy=policy, ) result = port.create_position(command=command, authorization=authorization) - if not isinstance(result, PositionMutationResult): + if type(result) is not PositionMutationResult: raise TypeError("mutation_port must return PositionMutationResult") + PositionMutationResult.__post_init__(result) + _require_result_identity_or_replay( + result_record_id=result.position_record_id, + expected_record_id=expected_position_record_id, + replay_command_digest=result.replay_command_digest, + command=command, + authorization=authorization, + result_name="position", + ) return result @@ -421,8 +477,10 @@ def create_assignment_record( mutation_port: PeopleMutationPort, ) -> AssignmentMutationResult: """Authorize the exact assignment target before persisting seat allocation.""" - if not isinstance(command, AssignmentMutationCommand): + if type(command) is not AssignmentMutationCommand: raise TypeError("command must be an AssignmentMutationCommand") + command = replace(command) + expected_assignment_record_id = UUID(int=command.assignment_record_id.int) port = _require_port(mutation_port) authorization = authorize_resource_fields( principal=principal, @@ -436,13 +494,22 @@ def create_assignment_record( policy=policy, ) result = port.create_assignment(command=command, authorization=authorization) - if not isinstance(result, AssignmentMutationResult): + if type(result) is not AssignmentMutationResult: raise TypeError("mutation_port must return AssignmentMutationResult") + AssignmentMutationResult.__post_init__(result) + _require_result_identity_or_replay( + result_record_id=result.assignment_record_id, + expected_record_id=expected_assignment_record_id, + replay_command_digest=result.replay_command_digest, + command=command, + authorization=authorization, + result_name="assignment", + ) return result def parse_allocation_ratio(raw_value: object) -> Decimal: """Parse the OpenAPI allocation token into an exact four-decimal ratio.""" - if not isinstance(raw_value, str) or re.fullmatch(r"^(0\.[0-9]{4}|1\.0000)$", raw_value) is None: + if type(raw_value) is not str or re.fullmatch(r"^(0\.(?!0000)[0-9]{4}|1\.0000)$", raw_value) is None: raise ValueError("allocation_ratio must match 0.0001-1.0000 four-decimal form.") return Decimal(raw_value) diff --git a/services/people-api/src/orgmetra_people_api/postgres_hire.py b/services/people-api/src/orgmetra_people_api/postgres_hire.py index 4c328e02f..9ec7ff291 100644 --- a/services/people-api/src/orgmetra_people_api/postgres_hire.py +++ b/services/people-api/src/orgmetra_people_api/postgres_hire.py @@ -12,12 +12,13 @@ from contextlib import AbstractContextManager from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timezone from hashlib import sha256 import json import re from typing import Any, Callable from uuid import UUID +from zoneinfo import ZoneInfo from orgmetra_hris_kernel.audit import AuditOutboxEvent from orgmetra_keyverse_adapter import AuthorizationDecision @@ -165,18 +166,34 @@ def _is_operational_uuid(value: object) -> bool: """Return whether a value is an Orgmetra operational UUID.""" - return isinstance(value, UUID) and value.int not in (0, _MAX_UUID_INT) + return type(value) is UUID and value.int not in (0, _MAX_UUID_INT) def _is_aware_datetime(value: object) -> bool: - """Return whether a value is a timezone-aware datetime with a real offset.""" - return isinstance(value, datetime) and value.tzinfo is not None and value.utcoffset() is not None + """Return whether durable time is exact and backed by an inert standard provider.""" + if type(value) is not datetime or value.tzinfo is None: + return False + if type(value.tzinfo) not in (timezone, ZoneInfo): + return False + return value.utcoffset() is not None + + +def _unpack_fixed_rows(value: object, *, row_width: int, error_message: str) -> tuple[tuple[object, ...], ...]: + """Detach one bounded fixed projection only from inert built-in containers.""" + if type(value) not in (list, tuple): + raise HireDecisionIntegrityError(error_message) + rows: list[tuple[object, ...]] = [] + for row in value: + if type(row) not in (list, tuple) or len(row) != row_width: + raise HireDecisionIntegrityError(error_message) + rows.append(tuple(row)) + return tuple(rows) def _validate_authorization(command: HireAcceptanceCommand, authorization: object) -> AuthorizationDecision: """Require an exact allow decision for this immutable selection decision.""" expected_reference = f"selection_decision:{command.selection_decision_id.hex}" - if not isinstance(authorization, AuthorizationDecision): + if type(authorization) is not AuthorizationDecision: raise HireDecisionIntegrityError("hire mutation requires a typed authorization decision") if ( not authorization.allowed @@ -227,13 +244,17 @@ def _replayed_hire( key_parameters = (command.tenant_record_id, _HIRE_IDEMPOTENCY_ROUTE, command.idempotency_key) cursor.execute(_LOOKUP_HIRE_IDEMPOTENCY_SQL, key_parameters) cursor.execute(_READ_HIRE_IDEMPOTENCY_SQL, key_parameters) - rows = cursor.fetchmany(2) + rows = _unpack_fixed_rows( + cursor.fetchmany(2), + row_width=2, + error_message="hire idempotency row is invalid", + ) if not rows: return None - if len(rows) != 1 or len(rows[0]) != 2: + if len(rows) != 1: raise HireDecisionIntegrityError("hire idempotency row is invalid") created_record_id, stored_digest = rows[0] - if not _is_operational_uuid(created_record_id) or not isinstance(stored_digest, str): + if not _is_operational_uuid(created_record_id) or type(stored_digest) is not str: raise HireDecisionIntegrityError("hire idempotency row is invalid") if stored_digest != _hire_command_digest(command, authorization): raise HireDecisionIntegrityError("hire idempotency key is bound to a different command") @@ -277,8 +298,11 @@ class PostgresHireAcceptancePort: ``connection_factory`` must return a DB-API connection context manager whose successful exit commits and exceptional exit rolls back, as psycopg - connections do. Pooling, TLS, credentials, and database roles remain a - deployment concern outside this service package. + connections do. Fixed projections must cross this adapter boundary as exact + built-in list/tuple row collections containing exact built-in list/tuple + rows; custom row factories must normalize before durable evidence is read. + Pooling, TLS, credentials, and database roles remain a deployment concern + outside this service package. """ connection_factory: PostgresConnectionFactory @@ -304,8 +328,9 @@ def accept_hire( authority. Tenant/route/key advisory serialization prevents concurrent retries from racing the unique idempotency binding. """ - if not isinstance(command, HireAcceptanceCommand): + if type(command) is not HireAcceptanceCommand: raise TypeError("command must be a HireAcceptanceCommand") + HireAcceptanceCommand.__post_init__(command) decision = _validate_authorization(command, authorization) with self.connection_factory() as connection: @@ -323,14 +348,15 @@ def accept_hire( command.candidate_profile_id, ), ) - rows = cursor.fetchmany(2) + rows = _unpack_fixed_rows( + cursor.fetchmany(2), + row_width=7, + error_message="decision provenance row has an invalid shape", + ) if not rows: raise HireDecisionNotFound("confirmed hire decision with sealed evidence was not found") if len(rows) != 1: raise HireDecisionIntegrityError("multiple decision provenance rows matched the hire") - row = rows[0] - if len(row) != 7: - raise HireDecisionIntegrityError("decision provenance row has an invalid shape") ( decision_actor_reference, decision_purpose_code, @@ -339,18 +365,25 @@ def accept_hire( decided_at, evidence_set_id, transaction_recorded_at, - ) = row - + ) = rows[0] + + if any( + type(value) is not str + for value in ( + decision_actor_reference, + decision_purpose_code, + decision_code, + confirmation_reference, + ) + ): + raise HireDecisionIntegrityError("selection decision provenance text is invalid") if decision_code != "hire": raise HireDecisionIntegrityError("selection decision is not an explicit hire") if decision_actor_reference != decision.actor_reference: raise HireDecisionIntegrityError("selection decision actor does not match authorized actor") if decision_purpose_code != decision.purpose_code: raise HireDecisionIntegrityError("selection decision purpose does not match authorized purpose") - if ( - not isinstance(confirmation_reference, str) - or _REFERENCE_PATTERN.fullmatch(confirmation_reference) is None - ): + if _REFERENCE_PATTERN.fullmatch(confirmation_reference) is None: raise HireDecisionIntegrityError("selection decision lacks valid human confirmation") if not _is_operational_uuid(evidence_set_id): raise HireDecisionIntegrityError("selection decision evidence set identity is invalid") diff --git a/services/people-api/src/orgmetra_people_api/postgres_mutations.py b/services/people-api/src/orgmetra_people_api/postgres_mutations.py index d94832cf8..c2a5cef3f 100644 --- a/services/people-api/src/orgmetra_people_api/postgres_mutations.py +++ b/services/people-api/src/orgmetra_people_api/postgres_mutations.py @@ -9,11 +9,12 @@ from __future__ import annotations from contextlib import AbstractContextManager -from dataclasses import dataclass -from datetime import date, datetime +from dataclasses import dataclass, replace +from datetime import date, datetime, timezone from decimal import Decimal from typing import Any, Callable from uuid import UUID +from zoneinfo import ZoneInfo from orgmetra_hris_kernel import ( AssignmentFact, @@ -252,13 +253,34 @@ def _is_operational_uuid(value: object) -> bool: - """Return whether a value is an Orgmetra operational UUID.""" - return isinstance(value, UUID) and value.int not in (0, _MAX_UUID_INT) + """Return whether a value is an exact operational UUID.""" + return type(value) is UUID and value.int not in (0, _MAX_UUID_INT) def _is_aware_datetime(value: object) -> bool: - """Return whether a value is a timezone-aware datetime with a real offset.""" - return isinstance(value, datetime) and value.tzinfo is not None and value.utcoffset() is not None + """Return whether durable time is exact and backed by an inert standard provider.""" + if type(value) is not datetime or value.tzinfo is None: + return False + if type(value.tzinfo) not in (timezone, ZoneInfo): + return False + return value.utcoffset() is not None + + +def _unpack_fixed_rows( + value: object, + *, + row_width: int, + error_message: str, +) -> tuple[tuple[object, ...], ...]: + """Detach exact built-in DB row containers before projection values are inspected.""" + if type(value) not in (list, tuple): + raise PeopleMutationIntegrityError(error_message) + detached: list[tuple[object, ...]] = [] + for row in value: + if type(row) not in (list, tuple) or len(row) != row_width: + raise PeopleMutationIntegrityError(error_message) + detached.append(tuple(row)) + return tuple(detached) def _replayed_record_id( @@ -266,25 +288,29 @@ def _replayed_record_id( *, command: EmploymentMutationCommand | PositionMutationCommand | AssignmentMutationCommand, authorization: AuthorizationDecision, -) -> UUID | None: - """Serialize one key, then return its committed record identity when present.""" +) -> tuple[UUID, str] | None: + """Serialize one key and return its committed identity plus verified semantic digest.""" route = command_route(command) digest = mutation_command_digest(command=command, authorization=authorization) key_parameters = (command.tenant_record_id, route, command.idempotency_key) cursor.execute(_LOOKUP_IDEMPOTENCY_SQL, key_parameters) cursor.execute(_READ_IDEMPOTENCY_SQL, key_parameters) - rows = cursor.fetchmany(2) + rows = _unpack_fixed_rows( + cursor.fetchmany(2), + row_width=2, + error_message="idempotency row is invalid", + ) if not rows: return None - if len(rows) != 1 or len(rows[0]) != 2: + if len(rows) != 1: raise PeopleMutationIntegrityError("idempotency row is invalid") created_record_id, stored_digest = rows[0] - if not _is_operational_uuid(created_record_id) or not isinstance(stored_digest, str): + if not _is_operational_uuid(created_record_id) or type(stored_digest) is not str: raise PeopleMutationIntegrityError("idempotency row is invalid") if stored_digest != digest: raise PeopleMutationIntegrityError("idempotency key is bound to a different command") assert isinstance(created_record_id, UUID) - return created_record_id + return created_record_id, stored_digest def _record_idempotency( @@ -322,7 +348,7 @@ def _require_authorization( requested_fields: frozenset[str], ) -> AuthorizationDecision: """Require an exact allow decision for the intended mutation target.""" - if not isinstance(authorization, AuthorizationDecision): + if type(authorization) is not AuthorizationDecision: raise PeopleMutationIntegrityError("people mutation requires a typed authorization decision") if ( not authorization.allowed @@ -378,8 +404,8 @@ def _employment_version_from_row(tenant_record_id: UUID, row: tuple[object, ...] not _is_operational_uuid(employment_record_id) or not _is_operational_uuid(employment_record_version_id) or not _is_operational_uuid(person_record_id) - or not isinstance(status_code, str) - or not isinstance(concurrency_code, str) + or type(status_code) is not str + or type(concurrency_code) is not str or type(effective_from) is not date or (effective_to is not None and type(effective_to) is not date) or not _is_aware_datetime(recorded_from) @@ -419,7 +445,7 @@ def _position_version_from_row(tenant_record_id: UUID, row: tuple[object, ...]) if ( not _is_operational_uuid(position_record_id) or not _is_operational_uuid(position_record_version_id) - or not isinstance(status_code, str) + or type(status_code) is not str or type(effective_from) is not date or (effective_to is not None and type(effective_to) is not date) or not _is_aware_datetime(recorded_from) @@ -460,7 +486,7 @@ def _assignment_from_row(tenant_record_id: UUID, row: tuple[object, ...]) -> Ass or not _is_operational_uuid(employment_record_id) or not _is_operational_uuid(person_record_id) or not _is_operational_uuid(position_record_id) - or not isinstance(allocation_ratio, Decimal) + or type(allocation_ratio) is not Decimal or type(effective_from) is not date or (effective_to is not None and type(effective_to) is not date) or not _is_aware_datetime(recorded_from) @@ -485,15 +511,18 @@ def _assignment_from_row(tenant_record_id: UUID, row: tuple[object, ...]) -> Ass ) -def _require_one_conversion(rows: list[tuple[object, ...]]) -> tuple[UUID, datetime]: +def _require_one_conversion(rows: object) -> tuple[UUID, datetime]: """Require exactly one current conversion row and a usable transaction timestamp.""" - if not rows: + detached = _unpack_fixed_rows( + rows, + row_width=2, + error_message="conversion row has an invalid shape", + ) + if not detached: raise PeopleMutationIntegrityError("person has no governed candidate-worker conversion") - if len(rows) != 1: + if len(detached) != 1: raise PeopleMutationIntegrityError("multiple candidate-worker conversions matched the person") - if len(rows[0]) != 2: - raise PeopleMutationIntegrityError("conversion row has an invalid shape") - conversion_id, recorded_at = rows[0] + conversion_id, recorded_at = detached[0] if not _is_operational_uuid(conversion_id) or not _is_aware_datetime(recorded_at): raise PeopleMutationIntegrityError("conversion identity or transaction time is invalid") assert isinstance(conversion_id, UUID) @@ -504,8 +533,12 @@ def _require_one_conversion(rows: list[tuple[object, ...]]) -> tuple[UUID, datet def _post_lock_recorded_at(cursor: Any) -> datetime: """Read one database clock instant only after the relevant conflict lock is held.""" cursor.execute(_POST_LOCK_RECORDED_AT_SQL) - rows = cursor.fetchmany(2) - if len(rows) != 1 or len(rows[0]) != 1 or not _is_aware_datetime(rows[0][0]): + rows = _unpack_fixed_rows( + cursor.fetchmany(2), + row_width=1, + error_message="post-lock database clock row is invalid", + ) + if len(rows) != 1 or not _is_aware_datetime(rows[0][0]): raise PeopleMutationIntegrityError("post-lock database clock row is invalid") recorded_at = rows[0][0] assert isinstance(recorded_at, datetime) @@ -517,7 +550,9 @@ class PostgresPeopleMutationPort: """Persist People mutations and governance evidence in one DB transaction. ``connection_factory`` must return a DB-API connection context manager whose - successful exit commits and exceptional exit rolls back. + successful exit commits and exceptional exit rolls back. Fixed query + projections must arrive as exact built-in list/tuple batches and rows; + custom row factories must normalize before this trust boundary. """ connection_factory: PostgresConnectionFactory @@ -534,8 +569,9 @@ def create_employment( authorization: AuthorizationDecision, ) -> EmploymentMutationResult: """Persist one employment after conversion and exclusivity checks.""" - if not isinstance(command, EmploymentMutationCommand): + if type(command) is not EmploymentMutationCommand: raise TypeError("command must be an EmploymentMutationCommand") + command = replace(command) decision = _require_authorization( authorization=authorization, tenant_record_id=command.tenant_record_id, @@ -549,7 +585,11 @@ def create_employment( cursor.execute(_TENANT_CONTEXT_SQL, (str(command.tenant_record_id),)) replayed = _replayed_record_id(cursor, command=command, authorization=decision) if replayed is not None: - return EmploymentMutationResult(employment_record_id=replayed) + replayed_record_id, replay_digest = replayed + return EmploymentMutationResult( + employment_record_id=replayed_record_id, + replay_command_digest=replay_digest, + ) cursor.execute(_CONVERSION_SQL, (command.tenant_record_id, command.person_record_id)) _require_one_conversion(cursor.fetchmany(2)) recorded_at = _post_lock_recorded_at(cursor) @@ -557,9 +597,14 @@ def create_employment( _EMPLOYMENT_VERSIONS_SQL, (command.tenant_record_id, command.person_record_id), ) + existing_rows = _unpack_fixed_rows( + cursor.fetchall(), + row_width=9, + error_message="employment version row has an invalid shape", + ) existing = [ _employment_version_from_row(command.tenant_record_id, row) - for row in cursor.fetchall() + for row in existing_rows ] proposed = EmploymentVersion( tenant_record_id=command.tenant_record_id, @@ -637,8 +682,9 @@ def create_position( authorization: AuthorizationDecision, ) -> PositionMutationResult: """Persist one position after organization and job parent checks.""" - if not isinstance(command, PositionMutationCommand): + if type(command) is not PositionMutationCommand: raise TypeError("command must be a PositionMutationCommand") + command = replace(command) decision = _require_authorization( authorization=authorization, tenant_record_id=command.tenant_record_id, @@ -652,19 +698,29 @@ def create_position( cursor.execute(_TENANT_CONTEXT_SQL, (str(command.tenant_record_id),)) replayed = _replayed_record_id(cursor, command=command, authorization=decision) if replayed is not None: - return PositionMutationResult(position_record_id=replayed) + replayed_record_id, replay_digest = replayed + return PositionMutationResult( + position_record_id=replayed_record_id, + replay_command_digest=replay_digest, + ) cursor.execute( _POSITION_PARENTS_SQL, (command.job_profile_id, command.tenant_record_id, command.organization_unit_id), ) - rows = cursor.fetchmany(2) + rows = _unpack_fixed_rows( + cursor.fetchmany(2), + row_width=3, + error_message="position parent row is invalid", + ) if not rows: raise PeopleMutationNotFound("organization unit or job profile was not found") - if len(rows) != 1 or len(rows[0]) != 3: + if len(rows) != 1: raise PeopleMutationIntegrityError("position parent row is invalid") organization_unit_id, job_profile_id, recorded_at = rows[0] if ( - organization_unit_id != command.organization_unit_id + not _is_operational_uuid(organization_unit_id) + or not _is_operational_uuid(job_profile_id) + or organization_unit_id != command.organization_unit_id or job_profile_id != command.job_profile_id or not _is_aware_datetime(recorded_at) ): @@ -727,8 +783,9 @@ def create_assignment( authorization: AuthorizationDecision, ) -> AssignmentMutationResult: """Persist one assignment after conversion and kernel coverage checks.""" - if not isinstance(command, AssignmentMutationCommand): + if type(command) is not AssignmentMutationCommand: raise TypeError("command must be an AssignmentMutationCommand") + command = replace(command) decision = _require_authorization( authorization=authorization, tenant_record_id=command.tenant_record_id, @@ -742,24 +799,38 @@ def create_assignment( cursor.execute(_TENANT_CONTEXT_SQL, (str(command.tenant_record_id),)) replayed = _replayed_record_id(cursor, command=command, authorization=decision) if replayed is not None: - return AssignmentMutationResult(assignment_record_id=replayed) + replayed_record_id, replay_digest = replayed + return AssignmentMutationResult( + assignment_record_id=replayed_record_id, + replay_command_digest=replay_digest, + ) cursor.execute(_CONVERSION_SQL, (command.tenant_record_id, command.person_record_id)) _require_one_conversion(cursor.fetchmany(2)) cursor.execute( _NAMED_EMPLOYMENT_VERSIONS_SQL, (command.tenant_record_id, command.employment_record_id), ) + employment_rows = _unpack_fixed_rows( + cursor.fetchall(), + row_width=9, + error_message="employment version row has an invalid shape", + ) employment_versions = [ _employment_version_from_row(command.tenant_record_id, row) - for row in cursor.fetchall() + for row in employment_rows ] cursor.execute( _NAMED_POSITION_VERSIONS_SQL, (command.tenant_record_id, command.position_record_id), ) + position_rows = _unpack_fixed_rows( + cursor.fetchall(), + row_width=7, + error_message="position version row has an invalid shape", + ) position_versions = [ _position_version_from_row(command.tenant_record_id, row) - for row in cursor.fetchall() + for row in position_rows ] recorded_at = _post_lock_recorded_at(cursor) cursor.execute( @@ -770,9 +841,14 @@ def create_assignment( command.position_record_id, ), ) + assignment_rows = _unpack_fixed_rows( + cursor.fetchall(), + row_width=9, + error_message="assignment row has an invalid shape", + ) existing_assignments = [ _assignment_from_row(command.tenant_record_id, row) - for row in cursor.fetchall() + for row in assignment_rows ] proposed = AssignmentFact( tenant_record_id=command.tenant_record_id, diff --git a/services/people-api/tests/test_hire_authorization_command_snapshot.py b/services/people-api/tests/test_hire_authorization_command_snapshot.py new file mode 100644 index 000000000..30804e05f --- /dev/null +++ b/services/people-api/tests/test_hire_authorization_command_snapshot.py @@ -0,0 +1,147 @@ +"""Application command-snapshot regression for confirmed-hire authorization.""" + +from __future__ import annotations + +from datetime import date +import unittest +from uuid import UUID + +from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.hire import ( + HireAcceptanceCommand, + HireAcceptanceResult, + accept_confirmed_hire, +) + +TENANT = UUID("0198a412-c200-7000-8000-000000000001") +CANDIDATE = UUID("0198a412-c200-7000-8000-000000000010") +SELECTION_DECISION = UUID("0198a412-c200-7000-8000-000000000011") +PERSON = UUID("0198a412-c200-7000-8000-000000000020") +PERSON_NAME = UUID("0198a412-c200-7000-8000-000000000021") +EMPLOYMENT = UUID("0198a412-c200-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-c200-7000-8000-000000000031") +CONVERSION = UUID("0198a412-c200-7000-8000-000000000040") +AUDIT_EVENT = UUID("0198a412-c200-7000-8000-000000000050") +OUTBOX = UUID("0198a412-c200-7000-8000-000000000051") +EFFECTIVE_FROM = date(2026, 9, 5) +ORIGINAL_DISPLAY_NAME = "Authorization Snapshot Worker" +MUTATED_DISPLAY_NAME = "Authorization Callback Rewrite" + +PRINCIPAL = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:hire-authorization-snapshot-operator", + granted_scope_codes=frozenset({"orgmetra.people.materialize_worker"}), +) + + +class _MutatingResourceKind(str): + """Rewrite retained caller hire data when policy comparison executes.""" + + def __new__( + cls, + value: str, + *, + command: HireAcceptanceCommand, + ) -> _MutatingResourceKind: + """Retain the caller command solely for the adversarial comparison callback.""" + instance = super().__new__(cls, value) + instance.command = command + return instance + + def _mutate_command(self) -> None: + """Rewrite valid PII after application validation but during authorization.""" + object.__setattr__(self.command, "display_name", MUTATED_DISPLAY_NAME) + + def __eq__(self, other: object) -> bool: + """Mutate before preserving ordinary string equality semantics.""" + self._mutate_command() + return str.__eq__(self, other) + + def __ne__(self, other: object) -> bool: + """Mutate before preserving ordinary string inequality semantics.""" + self._mutate_command() + return str.__ne__(self, other) + + +class _CapturingHirePort: + """Capture the hire command that crosses the application port boundary.""" + + command: HireAcceptanceCommand | None = None + + def accept_hire( + self, + *, + command: HireAcceptanceCommand, + authorization: object, + ) -> HireAcceptanceResult: + """Capture hire semantics and return the commanded authoritative identities.""" + del authorization + self.command = command + return HireAcceptanceResult( + person_record_id=command.person_record_id, + employment_record_id=command.employment_record_id, + candidate_worker_conversion_record_id=command.candidate_worker_conversion_record_id, + ) + + +def _command() -> HireAcceptanceCommand: + """Build one valid confirmed-hire command for authorization interleaving.""" + return HireAcceptanceCommand( + tenant_record_id=TENANT, + candidate_profile_id=CANDIDATE, + selection_decision_id=SELECTION_DECISION, + person_record_id=PERSON, + person_name_record_id=PERSON_NAME, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + candidate_worker_conversion_record_id=CONVERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + effective_from=EFFECTIVE_FROM, + display_name=ORIGINAL_DISPLAY_NAME, + idempotency_key="hire-authorization-snapshot-1", + ) + + +def _policy(command: HireAcceptanceCommand) -> PurposeBoundAccessPolicy: + """Build a valid policy whose resource-kind comparison mutates caller state.""" + return PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="hire-authorization-snapshot-v1", + resource_kind=_MutatingResourceKind("selection_decision", command=command), + purpose_code="candidate_hire", + operation_code="materialize_worker", + required_scope_code="orgmetra.people.materialize_worker", + permitted_fields=frozenset({"candidate_worker_conversion"}), + ) + + +class HireAuthorizationCommandSnapshotTests(unittest.TestCase): + """Require authorization callbacks to have no authority over the port hire command.""" + + def test_hire_port_receives_pre_authorization_semantics(self) -> None: + """Policy execution may mutate caller PII but not the detached port command.""" + command = _command() + port = _CapturingHirePort() + + result = accept_confirmed_hire( + principal=PRINCIPAL, + command=command, + purpose_code="candidate_hire", + policy=_policy(command), + mutation_port=port, + ) + + self.assertEqual(command.display_name, MUTATED_DISPLAY_NAME) + self.assertIsNotNone(port.command) + assert port.command is not None + self.assertEqual(port.command.display_name, ORIGINAL_DISPLAY_NAME) + self.assertIsNot(port.command, command) + self.assertEqual(result.person_record_id, PERSON) + self.assertEqual(result.employment_record_id, EMPLOYMENT) + self.assertEqual(result.candidate_worker_conversion_record_id, CONVERSION) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/people-api/tests/test_hire_identity_runtime_integrity.py b/services/people-api/tests/test_hire_identity_runtime_integrity.py new file mode 100644 index 000000000..1b3a44c23 --- /dev/null +++ b/services/people-api/tests/test_hire_identity_runtime_integrity.py @@ -0,0 +1,185 @@ +"""Runtime identity-integrity regressions for confirmed-hire contracts.""" + +from __future__ import annotations + +from datetime import date +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.hire import ( + HireAcceptanceCommand, + HireAcceptanceResult, + accept_confirmed_hire, +) + +TENANT = UUID("0198a412-7000-7000-8000-000000000001") + + +class _ForgedUUID(UUID): + """Attempt to make immutable hire evidence render a different identity.""" + + def __str__(self) -> str: + """Render a caller-chosen UUID instead of the underlying value.""" + return "0198a412-7000-7000-8000-ffffffffffff" + + +class _UnvalidatedHireCommand(HireAcceptanceCommand): + """Attempt to bypass base dataclass validation through dynamic post-init dispatch.""" + + def __post_init__(self) -> None: + """Intentionally skip the governed base validation.""" + + +class _UnvalidatedHireResult(HireAcceptanceResult): + """Attempt to return malformed persistence evidence through a result subclass.""" + + def __post_init__(self) -> None: + """Intentionally skip the governed base validation.""" + + +def _command_values(**overrides: object) -> dict[str, object]: + """Return one otherwise-valid confirmed-hire command mapping.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "candidate_profile_id": UUID("0198a412-7000-7000-8000-000000000010"), + "selection_decision_id": UUID("0198a412-7000-7000-8000-000000000011"), + "person_record_id": UUID("0198a412-7000-7000-8000-000000000020"), + "person_name_record_id": UUID("0198a412-7000-7000-8000-000000000021"), + "employment_record_id": UUID("0198a412-7000-7000-8000-000000000030"), + "employment_record_version_id": UUID("0198a412-7000-7000-8000-000000000031"), + "candidate_worker_conversion_record_id": UUID("0198a412-7000-7000-8000-000000000040"), + "audit_event_record_id": UUID("0198a412-7000-7000-8000-000000000050"), + "outbox_delivery_record_id": UUID("0198a412-7000-7000-8000-000000000051"), + "effective_from": date(2026, 8, 21), + "display_name": "Ada Lovelace", + "idempotency_key": "hire-runtime-integrity-21", + "employment_status_code": "active", + } + values.update(overrides) + return values + + +def _command(**overrides: object) -> HireAcceptanceCommand: + """Build one otherwise-valid confirmed-hire command.""" + return HireAcceptanceCommand(**_command_values(**overrides)) # type: ignore[arg-type] + + +def _principal() -> AuthenticatedPrincipal: + """Return a principal authorized for the focused application-boundary tests.""" + return AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-21", + granted_scope_codes=frozenset({"orgmetra.people.materialize_worker"}), + ) + + +def _policy() -> PurposeBoundAccessPolicy: + """Return the exact purpose-bound policy for confirmed-hire materialization.""" + return PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="people-hire-v1", + resource_kind="selection_decision", + purpose_code="candidate_hire", + operation_code="materialize_worker", + required_scope_code="orgmetra.people.materialize_worker", + permitted_fields=frozenset({"candidate_worker_conversion"}), + ) + + +class _RecordingPort: + """Capture whether malformed commands cross the governed application boundary.""" + + def __init__(self) -> None: + self.called = False + + def accept_hire(self, *, command: HireAcceptanceCommand, authorization: object) -> HireAcceptanceResult: + """Return a valid opaque result while recording the call.""" + del authorization + self.called = True + return HireAcceptanceResult( + person_record_id=command.person_record_id, + employment_record_id=command.employment_record_id, + candidate_worker_conversion_record_id=command.candidate_worker_conversion_record_id, + ) + + +class _MalformedResultPort: + """Return an invalid subclass that skipped the result contract's post-init checks.""" + + def accept_hire(self, *, command: HireAcceptanceCommand, authorization: object) -> HireAcceptanceResult: + """Produce malformed result evidence after a valid authorization call.""" + del command, authorization + return _UnvalidatedHireResult( + person_record_id="not-a-uuid", # type: ignore[arg-type] + employment_record_id=UUID("0198a412-7000-7000-8000-000000000030"), + candidate_worker_conversion_record_id=UUID("0198a412-7000-7000-8000-000000000040"), + ) + + +@pytest.mark.parametrize( + "field_name", + [ + "tenant_record_id", + "candidate_profile_id", + "selection_decision_id", + "person_record_id", + "person_name_record_id", + "employment_record_id", + "employment_record_version_id", + "candidate_worker_conversion_record_id", + "audit_event_record_id", + "outbox_delivery_record_id", + ], +) +def test_hire_command_rejects_uuid_subclasses_before_idempotency_or_persistence( + field_name: str, +) -> None: + """Caller-controlled UUID rendering cannot rewrite confirmed-hire semantics.""" + forged = _ForgedUUID("0198a412-7000-7000-8000-000000000123") + with pytest.raises(ValueError, match=f"{field_name} must be an operational UUID"): + _command(**{field_name: forged}) + + +def test_hire_result_rejects_uuid_subclasses_before_crossing_service_boundary() -> None: + """A persistence adapter cannot return identity objects with forged rendering.""" + forged = _ForgedUUID("0198a412-7000-7000-8000-000000000123") + with pytest.raises(ValueError, match="person_record_id must be an operational UUID"): + HireAcceptanceResult( + person_record_id=forged, + employment_record_id=UUID("0198a412-7000-7000-8000-000000000030"), + candidate_worker_conversion_record_id=UUID("0198a412-7000-7000-8000-000000000040"), + ) + + +def test_confirmed_hire_rejects_command_subclass_that_bypassed_post_init() -> None: + """Only an exact validated command may cross into authoritative persistence.""" + forged = _UnvalidatedHireCommand( + **_command_values(effective_from="not-a-business-date") # type: ignore[arg-type] + ) + port = _RecordingPort() + + with pytest.raises(TypeError, match="command must be a HireAcceptanceCommand"): + accept_confirmed_hire( + principal=_principal(), + command=forged, + purpose_code="candidate_hire", + policy=_policy(), + mutation_port=port, + ) + + assert port.called is False + + +def test_confirmed_hire_rejects_result_subclass_that_bypassed_post_init() -> None: + """Only an exact validated result may leave the authoritative mutation boundary.""" + with pytest.raises(TypeError, match="mutation_port must return HireAcceptanceResult"): + accept_confirmed_hire( + principal=_principal(), + command=_command(), + purpose_code="candidate_hire", + policy=_policy(), + mutation_port=_MalformedResultPort(), + ) diff --git a/services/people-api/tests/test_hire_post_construction_integrity.py b/services/people-api/tests/test_hire_post_construction_integrity.py new file mode 100644 index 000000000..3ba432939 --- /dev/null +++ b/services/people-api/tests/test_hire_post_construction_integrity.py @@ -0,0 +1,162 @@ +"""Reject post-construction rewrites at confirmed-hire consumer boundaries.""" + +from __future__ import annotations + +from datetime import date +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.hire import ( + HireAcceptanceCommand, + HireAcceptanceResult, + accept_confirmed_hire, +) +from orgmetra_people_api.postgres_hire import PostgresHireAcceptancePort + +TENANT = UUID("0198a412-7800-7000-8000-000000000001") +SELECTION_DECISION = UUID("0198a412-7800-7000-8000-000000000002") +PERSON = UUID("0198a412-7800-7000-8000-000000000003") +EMPLOYMENT = UUID("0198a412-7800-7000-8000-000000000004") +CONVERSION = UUID("0198a412-7800-7000-8000-000000000005") + + +class _ExecutableUUID(UUID): + """Expose UUID rendering attempted before an exact runtime-type gate.""" + + def __getattribute__(self, name: str) -> object: + """Fail if a rewritten UUID is rendered before command revalidation.""" + if name == "hex": + raise AttributeError("UUID subtype behavior executed before command revalidation") + return super().__getattribute__(name) + + +class _RecordingPort: + """Record whether a rewritten command crosses the application boundary.""" + + def __init__(self) -> None: + """Start with no durable-port invocation.""" + self.called = False + + def accept_hire(self, *, command: HireAcceptanceCommand, authorization: object) -> HireAcceptanceResult: + """Return one valid result if the application incorrectly calls the port.""" + del authorization + self.called = True + return HireAcceptanceResult( + person_record_id=command.person_record_id, + employment_record_id=command.employment_record_id, + candidate_worker_conversion_record_id=command.candidate_worker_conversion_record_id, + ) + + +class _RewrittenResultPort: + """Return an exact result whose identity was rewritten after construction.""" + + def accept_hire(self, *, command: HireAcceptanceCommand, authorization: object) -> HireAcceptanceResult: + """Rewrite one exact result after its constructor invariant has already run.""" + del command, authorization + result = HireAcceptanceResult( + person_record_id=PERSON, + employment_record_id=EMPLOYMENT, + candidate_worker_conversion_record_id=CONVERSION, + ) + object.__setattr__(result, "person_record_id", "not-a-uuid") + return result + + +def _command() -> HireAcceptanceCommand: + """Build one valid confirmed-hire command before deliberate low-level rewrite.""" + return HireAcceptanceCommand( + tenant_record_id=TENANT, + candidate_profile_id=UUID("0198a412-7800-7000-8000-000000000010"), + selection_decision_id=SELECTION_DECISION, + person_record_id=PERSON, + person_name_record_id=UUID("0198a412-7800-7000-8000-000000000011"), + employment_record_id=EMPLOYMENT, + employment_record_version_id=UUID("0198a412-7800-7000-8000-000000000012"), + candidate_worker_conversion_record_id=CONVERSION, + audit_event_record_id=UUID("0198a412-7800-7000-8000-000000000013"), + outbox_delivery_record_id=UUID("0198a412-7800-7000-8000-000000000014"), + effective_from=date(2026, 9, 5), + display_name="Ada Lovelace", + idempotency_key="hire-post-construction-228", + employment_status_code="active", + ) + + +def _principal() -> AuthenticatedPrincipal: + """Return the authenticated principal for the application-boundary regression.""" + return AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-228", + granted_scope_codes=frozenset({"orgmetra.people.materialize_worker"}), + ) + + +def _policy() -> PurposeBoundAccessPolicy: + """Return the purpose-bound policy for confirmed-hire materialization.""" + return PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="people-hire-v1", + resource_kind="selection_decision", + purpose_code="candidate_hire", + operation_code="materialize_worker", + required_scope_code="orgmetra.people.materialize_worker", + permitted_fields=frozenset({"candidate_worker_conversion"}), + ) + + +def _rewrite_selection_decision(command: HireAcceptanceCommand) -> None: + """Replace one validated UUID with executable subtype evidence after construction.""" + object.__setattr__( + command, + "selection_decision_id", + _ExecutableUUID("0198a412-7800-7000-8000-0000000000ff"), + ) + + +def _forbidden_connection_factory() -> object: + """Fail if a rewritten command reaches database acquisition.""" + raise AssertionError("database acquisition occurred before command revalidation") + + +def test_application_revalidates_rewritten_hire_command_before_authorization_rendering() -> None: + """A rewritten command must fail before UUID rendering or the mutation port.""" + command = _command() + _rewrite_selection_decision(command) + port = _RecordingPort() + + with pytest.raises(ValueError, match="selection_decision_id must be an operational UUID"): + accept_confirmed_hire( + principal=_principal(), + command=command, + purpose_code="candidate_hire", + policy=_policy(), + mutation_port=port, + ) + + assert port.called is False + + +def test_application_revalidates_rewritten_exact_hire_result_before_return() -> None: + """An exact result rewritten after construction must not leave the service boundary.""" + with pytest.raises(ValueError, match="person_record_id must be an operational UUID"): + accept_confirmed_hire( + principal=_principal(), + command=_command(), + purpose_code="candidate_hire", + policy=_policy(), + mutation_port=_RewrittenResultPort(), + ) + + +def test_postgres_port_revalidates_rewritten_hire_command_before_authorization_or_db() -> None: + """Direct durable-port entry must reject rewritten evidence before callbacks or DB work.""" + command = _command() + _rewrite_selection_decision(command) + port = PostgresHireAcceptancePort(connection_factory=_forbidden_connection_factory) + + with pytest.raises(ValueError, match="selection_decision_id must be an operational UUID"): + port.accept_hire(command=command, authorization=object()) # type: ignore[arg-type] diff --git a/services/people-api/tests/test_people_mutation_allocation_text_runtime_integrity.py b/services/people-api/tests/test_people_mutation_allocation_text_runtime_integrity.py new file mode 100644 index 000000000..f2f238356 --- /dev/null +++ b/services/people-api/tests/test_people_mutation_allocation_text_runtime_integrity.py @@ -0,0 +1,47 @@ +"""Reject non-canonical allocation-ratio text before Decimal parsing.""" + +from __future__ import annotations + +from pathlib import Path +import re + +import pytest + +from orgmetra_people_api.mutations import parse_allocation_ratio + +_OPENAPI_PATH = Path(__file__).resolve().parents[3] / "schemas" / "openapi.yaml" + + +class _AllocationRatioText(str): + """Represent a valid-looking allocation token with caller-defined runtime identity.""" + + +def _published_allocation_pattern() -> re.Pattern[str]: + """Read the Assignment allocation token pattern from the published OpenAPI contract.""" + schema = _OPENAPI_PATH.read_text(encoding="utf-8") + match = re.search( + r"allocation_ratio:\n\s+type: string\n\s+pattern: '([^']+)'", + schema, + ) + assert match is not None, "CreateAssignmentRecordCommand allocation pattern is missing" + return re.compile(match.group(1)) + + +def test_parse_allocation_ratio_rejects_string_subclasses() -> None: + """Assignment allocation text must be the exact built-in value that was parsed.""" + with pytest.raises(ValueError, match="allocation_ratio"): + parse_allocation_ratio(_AllocationRatioText("0.2500")) + + +def test_parse_allocation_ratio_rejects_zero_before_domain_construction() -> None: + """The HTTP scalar parser must enforce the same strictly-positive Assignment invariant.""" + with pytest.raises(ValueError, match="allocation_ratio"): + parse_allocation_ratio("0.0000") + + +def test_openapi_allocation_pattern_matches_the_strictly_positive_domain_range() -> None: + """Generated clients and handlers must not advertise zero as a valid Assignment ratio.""" + pattern = _published_allocation_pattern() + assert pattern.fullmatch("0.0000") is None + for token in ("0.0001", "0.2500", "0.9999", "1.0000"): + assert pattern.fullmatch(token) is not None diff --git a/services/people-api/tests/test_people_mutation_authorization_command_snapshot.py b/services/people-api/tests/test_people_mutation_authorization_command_snapshot.py new file mode 100644 index 000000000..d67c91b18 --- /dev/null +++ b/services/people-api/tests/test_people_mutation_authorization_command_snapshot.py @@ -0,0 +1,274 @@ +"""Application command-snapshot regressions across purpose-bound authorization.""" + +from __future__ import annotations + +from datetime import date +from decimal import Decimal +import unittest +from uuid import UUID + +from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.mutations import ( + AssignmentMutationCommand, + AssignmentMutationResult, + EmploymentMutationCommand, + EmploymentMutationResult, + PositionMutationCommand, + PositionMutationResult, + create_assignment_record, + create_employment_record, + create_position_record, +) + +TENANT = UUID("0198a412-c100-7000-8000-000000000001") +PERSON = UUID("0198a412-c100-7000-8000-000000000010") +OTHER_PERSON = UUID("0198a412-c100-7000-8000-000000000011") +EMPLOYMENT = UUID("0198a412-c100-7000-8000-000000000020") +EMPLOYMENT_VERSION = UUID("0198a412-c100-7000-8000-000000000021") +ORGANIZATION = UUID("0198a412-c100-7000-8000-000000000030") +JOB = UUID("0198a412-c100-7000-8000-000000000040") +OTHER_JOB = UUID("0198a412-c100-7000-8000-000000000041") +POSITION = UUID("0198a412-c100-7000-8000-000000000050") +POSITION_VERSION = UUID("0198a412-c100-7000-8000-000000000051") +OTHER_POSITION = UUID("0198a412-c100-7000-8000-000000000052") +ASSIGNMENT = UUID("0198a412-c100-7000-8000-000000000060") +AUDIT_EVENT = UUID("0198a412-c100-7000-8000-000000000070") +OUTBOX = UUID("0198a412-c100-7000-8000-000000000071") +EFFECTIVE_FROM = date(2026, 9, 5) + +PRINCIPAL = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:authorization-snapshot-operator", + granted_scope_codes=frozenset({"orgmetra.people.write"}), +) + + +class _MutatingResourceKind(str): + """Rewrite a retained caller command when policy comparison executes.""" + + def __new__( + cls, + value: str, + *, + command: object, + field_name: str, + replacement: object, + ) -> _MutatingResourceKind: + """Retain the caller command solely for the adversarial comparison callback.""" + instance = super().__new__(cls, value) + instance.command = command + instance.field_name = field_name + instance.replacement = replacement + return instance + + def _mutate_command(self) -> None: + """Simulate caller-owned executable policy behavior during authorization.""" + object.__setattr__(self.command, self.field_name, self.replacement) + + def __eq__(self, other: object) -> bool: + """Mutate before preserving ordinary string equality semantics.""" + self._mutate_command() + return str.__eq__(self, other) + + def __ne__(self, other: object) -> bool: + """Mutate before preserving ordinary string inequality semantics.""" + self._mutate_command() + return str.__ne__(self, other) + + +class _CapturingMutationPort: + """Capture the semantic command that crosses the application port boundary.""" + + employment_command: EmploymentMutationCommand | None = None + position_command: PositionMutationCommand | None = None + assignment_command: AssignmentMutationCommand | None = None + + def create_employment( + self, + *, + command: EmploymentMutationCommand, + authorization: object, + ) -> EmploymentMutationResult: + """Capture Employment semantics and return the commanded target identity.""" + del authorization + self.employment_command = command + return EmploymentMutationResult(employment_record_id=command.employment_record_id) + + def create_position( + self, + *, + command: PositionMutationCommand, + authorization: object, + ) -> PositionMutationResult: + """Capture Position semantics and return the commanded target identity.""" + del authorization + self.position_command = command + return PositionMutationResult(position_record_id=command.position_record_id) + + def create_assignment( + self, + *, + command: AssignmentMutationCommand, + authorization: object, + ) -> AssignmentMutationResult: + """Capture Assignment semantics and return the commanded target identity.""" + del authorization + self.assignment_command = command + return AssignmentMutationResult(assignment_record_id=command.assignment_record_id) + + +def _policy( + *, + resource_kind: str, + field_name: str, + command: object, + command_field_name: str, + replacement: object, +) -> PurposeBoundAccessPolicy: + """Build a valid policy whose resource-kind comparison mutates caller state.""" + return PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="authorization-snapshot-v1", + resource_kind=_MutatingResourceKind( + resource_kind, + command=command, + field_name=command_field_name, + replacement=replacement, + ), + purpose_code="workforce_admin", + operation_code="create_record", + required_scope_code="orgmetra.people.write", + permitted_fields=frozenset({field_name}), + ) + + +def _employment_command() -> EmploymentMutationCommand: + """Build one valid Employment command for authorization interleaving.""" + return EmploymentMutationCommand( + tenant_record_id=TENANT, + person_record_id=PERSON, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + employment_status_code="active", + employment_concurrency_code="exclusive", + effective_from=EFFECTIVE_FROM, + confirmation_reference="human_confirmation:authorization-snapshot", + evidence_version_code="authorization-snapshot-v1", + idempotency_key="authorization-snapshot-employment-1", + ) + + +def _position_command() -> PositionMutationCommand: + """Build one valid Position command for authorization interleaving.""" + return PositionMutationCommand( + tenant_record_id=TENANT, + organization_unit_id=ORGANIZATION, + job_profile_id=JOB, + position_record_id=POSITION, + position_record_version_id=POSITION_VERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + position_status_code="open", + effective_from=EFFECTIVE_FROM, + confirmation_reference="human_confirmation:authorization-snapshot", + evidence_version_code="authorization-snapshot-v1", + idempotency_key="authorization-snapshot-position-1", + ) + + +def _assignment_command() -> AssignmentMutationCommand: + """Build one valid Assignment command for authorization interleaving.""" + return AssignmentMutationCommand( + tenant_record_id=TENANT, + employment_record_id=EMPLOYMENT, + person_record_id=PERSON, + position_record_id=POSITION, + assignment_record_id=ASSIGNMENT, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + allocation_ratio=Decimal("0.5000"), + effective_from=EFFECTIVE_FROM, + confirmation_reference="human_confirmation:authorization-snapshot", + evidence_version_code="authorization-snapshot-v1", + idempotency_key="authorization-snapshot-assignment-1", + ) + + +class PeopleMutationAuthorizationCommandSnapshotTests(unittest.TestCase): + """Require authorization callbacks to see no caller-owned command authority.""" + + def test_employment_port_receives_pre_authorization_semantics(self) -> None: + """Policy execution may mutate caller state but not the Employment port command.""" + command = _employment_command() + port = _CapturingMutationPort() + create_employment_record( + principal=PRINCIPAL, + command=command, + purpose_code="workforce_admin", + policy=_policy( + resource_kind="employment_record", + field_name="employment_record", + command=command, + command_field_name="person_record_id", + replacement=OTHER_PERSON, + ), + mutation_port=port, + ) + self.assertEqual(command.person_record_id, OTHER_PERSON) + self.assertIsNotNone(port.employment_command) + assert port.employment_command is not None + self.assertEqual(port.employment_command.person_record_id, PERSON) + self.assertIsNot(port.employment_command, command) + + def test_position_port_receives_pre_authorization_semantics(self) -> None: + """Policy execution may mutate caller state but not the Position port command.""" + command = _position_command() + port = _CapturingMutationPort() + create_position_record( + principal=PRINCIPAL, + command=command, + purpose_code="workforce_admin", + policy=_policy( + resource_kind="position_record", + field_name="position_record", + command=command, + command_field_name="job_profile_id", + replacement=OTHER_JOB, + ), + mutation_port=port, + ) + self.assertEqual(command.job_profile_id, OTHER_JOB) + self.assertIsNotNone(port.position_command) + assert port.position_command is not None + self.assertEqual(port.position_command.job_profile_id, JOB) + self.assertIsNot(port.position_command, command) + + def test_assignment_port_receives_pre_authorization_semantics(self) -> None: + """Policy execution may mutate caller state but not the Assignment port command.""" + command = _assignment_command() + port = _CapturingMutationPort() + create_assignment_record( + principal=PRINCIPAL, + command=command, + purpose_code="workforce_admin", + policy=_policy( + resource_kind="assignment_record", + field_name="assignment_record", + command=command, + command_field_name="position_record_id", + replacement=OTHER_POSITION, + ), + mutation_port=port, + ) + self.assertEqual(command.position_record_id, OTHER_POSITION) + self.assertIsNotNone(port.assignment_command) + assert port.assignment_command is not None + self.assertEqual(port.assignment_command.position_record_id, POSITION) + self.assertIsNot(port.assignment_command, command) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/people-api/tests/test_people_mutation_digest_runtime_integrity.py b/services/people-api/tests/test_people_mutation_digest_runtime_integrity.py new file mode 100644 index 000000000..efb67db23 --- /dev/null +++ b/services/people-api/tests/test_people_mutation_digest_runtime_integrity.py @@ -0,0 +1,104 @@ +"""Runtime-integrity regressions for People mutation idempotency evidence.""" + +from __future__ import annotations + +from datetime import date +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import AuthorizationDecision +from orgmetra_people_api.mutations import ( + EmploymentMutationCommand, + idempotency_record_id, + mutation_command_digest, +) + +TENANT = UUID("0198a412-8000-7000-8000-000000000001") + + +class _ForgedUUID(UUID): + """Attempt to select idempotency identity with caller-controlled string rendering.""" + + def __str__(self) -> str: + """Render another tenant identifier while retaining the original UUID value.""" + return "0198a412-8000-7000-8000-ffffffffffff" + + +class _ForgedDecision(AuthorizationDecision): + """Attempt to rewrite immutable authorization evidence during digest construction.""" + + def __getattribute__(self, name: str) -> object: + """Forge only the actor value observed by digest construction.""" + if name == "actor_reference": + return "keyverse_subject:forged-actor" + return super().__getattribute__(name) + + +def _command() -> EmploymentMutationCommand: + """Build one exact employment mutation command.""" + return EmploymentMutationCommand( + tenant_record_id=TENANT, + person_record_id=UUID("0198a412-8000-7000-8000-000000000020"), + employment_record_id=UUID("0198a412-8000-7000-8000-000000000030"), + employment_record_version_id=UUID("0198a412-8000-7000-8000-000000000031"), + audit_event_record_id=UUID("0198a412-8000-7000-8000-000000000080"), + outbox_delivery_record_id=UUID("0198a412-8000-7000-8000-000000000081"), + employment_status_code="active", + employment_concurrency_code="exclusive", + effective_from=date(2026, 8, 21), + confirmation_reference="human_confirmation:runtime-21", + evidence_version_code="decision_evidence_set:v1", + idempotency_key="mutation-runtime-key-21", + ) + + +def _decision() -> AuthorizationDecision: + """Build one exact authorized mutation decision.""" + employment_id = _command().employment_record_id + return AuthorizationDecision( + allowed=True, + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-21", + resource_reference=f"employment_record:{employment_id.hex}", + policy_version_code="people-mutation-v1", + purpose_code="workforce_admin", + operation_code="create_record", + resource_kind="employment_record", + requested_fields=frozenset({"employment_record"}), + authorized_fields=frozenset({"employment_record"}), + reason_code="access_permitted", + next_action="Continue with only the authorized fields.", + ) + + +def test_idempotency_record_id_rejects_uuid_subclass_before_tenant_key_derivation() -> None: + """Idempotency identity cannot be derived from caller-controlled tenant rendering.""" + forged = _ForgedUUID("0198a412-8000-7000-8000-000000000001") + with pytest.raises(ValueError, match="tenant_record_id must be an operational UUID"): + idempotency_record_id( + tenant_record_id=forged, + command_route_value="employment-records", + idempotency_key="mutation-runtime-key-21", + ) + + +def test_mutation_digest_rejects_authorization_decision_subclasses() -> None: + """Digest evidence must use the exact decision produced by the authorization adapter.""" + base = _decision() + forged = _ForgedDecision( + allowed=base.allowed, + tenant_record_id=base.tenant_record_id, + actor_reference=base.actor_reference, + resource_reference=base.resource_reference, + policy_version_code=base.policy_version_code, + purpose_code=base.purpose_code, + operation_code=base.operation_code, + resource_kind=base.resource_kind, + requested_fields=base.requested_fields, + authorized_fields=base.authorized_fields, + reason_code=base.reason_code, + next_action=base.next_action, + ) + with pytest.raises(TypeError, match="authorization must be an AuthorizationDecision"): + mutation_command_digest(command=_command(), authorization=forged) diff --git a/services/people-api/tests/test_people_mutation_idempotent_replay_result.py b/services/people-api/tests/test_people_mutation_idempotent_replay_result.py new file mode 100644 index 000000000..093c61960 --- /dev/null +++ b/services/people-api/tests/test_people_mutation_idempotent_replay_result.py @@ -0,0 +1,119 @@ +"""Result-receipt regressions for idempotent generic People mutation replay.""" + +from __future__ import annotations + +import unittest +from uuid import UUID + +from orgmetra_people_api.mutations import ( + AssignmentMutationCommand, + AssignmentMutationResult, + EmploymentMutationCommand, + EmploymentMutationResult, + PeopleMutationIntegrityError, + PositionMutationCommand, + PositionMutationResult, + create_assignment_record, + create_employment_record, + create_position_record, + mutation_command_digest, +) +from test_people_mutations import ( + ASSIGNMENT, + EMPLOYMENT, + POSITION, + PRINCIPAL, + assignment_command, + assignment_policy, + employment_command, + employment_policy, + position_command, + position_policy, +) + +NEW_EMPLOYMENT = UUID("0198a412-8200-7000-8000-000000000033") +NEW_POSITION = UUID("0198a412-8200-7000-8000-000000000044") +NEW_ASSIGNMENT = UUID("0198a412-8200-7000-8000-000000000077") + + +class ReplayReceiptPort: + """Return first-committed identities with independently checkable replay evidence.""" + + def __init__(self, *, digest_override: str | None = None) -> None: + self.digest_override = digest_override + + def _digest(self, *, command: object, authorization: object) -> str: + digest = mutation_command_digest(command=command, authorization=authorization) # type: ignore[arg-type] + return self.digest_override if self.digest_override is not None else digest + + def create_employment(self, *, command: EmploymentMutationCommand, authorization: object) -> EmploymentMutationResult: + return EmploymentMutationResult( + employment_record_id=EMPLOYMENT, + replay_command_digest=self._digest(command=command, authorization=authorization), + ) + + def create_position(self, *, command: PositionMutationCommand, authorization: object) -> PositionMutationResult: + return PositionMutationResult( + position_record_id=POSITION, + replay_command_digest=self._digest(command=command, authorization=authorization), + ) + + def create_assignment(self, *, command: AssignmentMutationCommand, authorization: object) -> AssignmentMutationResult: + return AssignmentMutationResult( + assignment_record_id=ASSIGNMENT, + replay_command_digest=self._digest(command=command, authorization=authorization), + ) + + +class PeopleMutationIdempotentReplayResultTests(unittest.TestCase): + """Reconcile first-committed replay identity with result-integrity hardening.""" + + def test_matching_replay_receipt_may_return_first_committed_identity(self) -> None: + port = ReplayReceiptPort() + + employment = create_employment_record( + principal=PRINCIPAL, + command=employment_command(employment_record_id=NEW_EMPLOYMENT), + purpose_code="workforce_admin", + policy=employment_policy(), + mutation_port=port, + ) + position = create_position_record( + principal=PRINCIPAL, + command=position_command(position_record_id=NEW_POSITION), + purpose_code="job_architecture_admin", + policy=position_policy(), + mutation_port=port, + ) + assignment = create_assignment_record( + principal=PRINCIPAL, + command=assignment_command(assignment_record_id=NEW_ASSIGNMENT), + purpose_code="workforce_admin", + policy=assignment_policy(), + mutation_port=port, + ) + + self.assertEqual(employment.employment_record_id, EMPLOYMENT) + self.assertEqual(position.position_record_id, POSITION) + self.assertEqual(assignment.assignment_record_id, ASSIGNMENT) + + def test_foreign_identity_with_mismatched_replay_digest_fails_closed(self) -> None: + with self.assertRaisesRegex(PeopleMutationIntegrityError, "replay evidence"): + create_employment_record( + principal=PRINCIPAL, + command=employment_command(employment_record_id=NEW_EMPLOYMENT), + purpose_code="workforce_admin", + policy=employment_policy(), + mutation_port=ReplayReceiptPort(digest_override="0" * 64), + ) + + def test_replay_digest_must_be_an_exact_string(self) -> None: + with self.assertRaisesRegex(ValueError, "replay_command_digest"): + EmploymentMutationResult( + employment_record_id=EMPLOYMENT, + replay_command_digest=object(), # type: ignore[arg-type] + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/people-api/tests/test_people_mutation_post_construction_integrity.py b/services/people-api/tests/test_people_mutation_post_construction_integrity.py new file mode 100644 index 000000000..0475d877a --- /dev/null +++ b/services/people-api/tests/test_people_mutation_post_construction_integrity.py @@ -0,0 +1,190 @@ +"""Post-construction runtime-integrity regressions for governed People mutations.""" + +from __future__ import annotations + +from datetime import date +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import AuthorizationDecision, PurposeBoundAccessPolicy +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.mutations import ( + AssignmentMutationCommand, + AssignmentMutationResult, + EmploymentMutationCommand, + EmploymentMutationResult, + PositionMutationCommand, + PositionMutationResult, + create_employment_record, + mutation_command_digest, +) + +TENANT = UUID("0198a412-a600-7000-8000-000000000001") +PERSON = UUID("0198a412-a600-7000-8000-000000000020") +EMPLOYMENT = UUID("0198a412-a600-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-a600-7000-8000-000000000031") +AUDIT_EVENT = UUID("0198a412-a600-7000-8000-000000000080") +OUTBOX = UUID("0198a412-a600-7000-8000-000000000081") + + +class _ExecutableUUID(UUID): + """Trip if a rewritten UUID is observed before exact runtime revalidation.""" + + @property + def hex(self) -> str: + """Fail if resource-reference rendering executes before validation.""" + raise AssertionError("rewritten UUID hex behavior must not execute") + + def __str__(self) -> str: + """Fail if canonical rendering executes before validation.""" + raise AssertionError("rewritten UUID string behavior must not execute") + + +def _command() -> EmploymentMutationCommand: + """Build one initially valid exact employment mutation command.""" + return EmploymentMutationCommand( + tenant_record_id=TENANT, + person_record_id=PERSON, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + employment_status_code="active", + employment_concurrency_code="exclusive", + effective_from=date(2026, 9, 5), + confirmation_reference="human_confirmation:post-construction-1", + evidence_version_code="employment-evidence-v1", + idempotency_key="post-construction-runtime-1", + ) + + +def _decision() -> AuthorizationDecision: + """Build exact authorization evidence for the employment command.""" + fields = frozenset({"employment_record"}) + return AuthorizationDecision( + allowed=True, + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-226", + resource_reference=f"employment_record:{EMPLOYMENT.hex}", + policy_version_code="people-mutation-v1", + purpose_code="workforce_admin", + operation_code="create_record", + resource_kind="employment_record", + requested_fields=fields, + authorized_fields=fields, + reason_code="access_permitted", + next_action="continue", + ) + + +def _principal() -> AuthenticatedPrincipal: + """Build one exact authenticated principal for service-boundary testing.""" + return AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-226", + granted_scope_codes=frozenset({"orgmetra.people.write"}), + ) + + +def _policy() -> PurposeBoundAccessPolicy: + """Build one exact purpose-bound policy for employment creation.""" + return PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="people-mutation-v1", + resource_kind="employment_record", + purpose_code="workforce_admin", + operation_code="create_record", + required_scope_code="orgmetra.people.write", + permitted_fields=frozenset({"employment_record"}), + ) + + +class _ResultPort: + """Return one supplied employment result while satisfying the mutation protocol.""" + + def __init__(self, result: EmploymentMutationResult) -> None: + """Retain the exact result supplied by the regression.""" + self.result = result + + def create_employment( + self, + *, + command: EmploymentMutationCommand, + authorization: AuthorizationDecision, + ) -> EmploymentMutationResult: + """Return the supplied employment result without changing it.""" + del command, authorization + return self.result + + def create_position( + self, + *, + command: PositionMutationCommand, + authorization: AuthorizationDecision, + ) -> PositionMutationResult: + """Reject unrelated position work in this focused regression port.""" + del command, authorization + raise AssertionError("position mutation is outside this regression") + + def create_assignment( + self, + *, + command: AssignmentMutationCommand, + authorization: AuthorizationDecision, + ) -> AssignmentMutationResult: + """Reject unrelated assignment work in this focused regression port.""" + del command, authorization + raise AssertionError("assignment mutation is outside this regression") + + +def test_digest_revalidates_exact_command_after_object_setattr_rewrite() -> None: + """Canonical digesting must reject rewritten command evidence before callbacks.""" + command = _command() + object.__setattr__( + command, + "person_record_id", + _ExecutableUUID("0198a412-a600-7000-8000-000000000099"), + ) + + with pytest.raises(ValueError, match="person_record_id must be an operational UUID"): + mutation_command_digest(command=command, authorization=_decision()) + + +def test_service_revalidates_exact_command_before_authorization_or_port_work() -> None: + """An exact command rewritten after construction must fail before field rendering.""" + command = _command() + object.__setattr__( + command, + "employment_record_id", + _ExecutableUUID("0198a412-a600-7000-8000-000000000098"), + ) + result = EmploymentMutationResult(employment_record_id=EMPLOYMENT) + + with pytest.raises(ValueError, match="employment_record_id must be an operational UUID"): + create_employment_record( + principal=_principal(), + command=command, + purpose_code="workforce_admin", + policy=_policy(), + mutation_port=_ResultPort(result), + ) + + +def test_service_revalidates_exact_result_after_port_rewrite() -> None: + """An exact result rewritten by a port must not cross the People service boundary.""" + result = EmploymentMutationResult(employment_record_id=EMPLOYMENT) + object.__setattr__( + result, + "employment_record_id", + _ExecutableUUID("0198a412-a600-7000-8000-000000000097"), + ) + + with pytest.raises(ValueError, match="employment_record_id must be an operational UUID"): + create_employment_record( + principal=_principal(), + command=_command(), + purpose_code="workforce_admin", + policy=_policy(), + mutation_port=_ResultPort(result), + ) diff --git a/services/people-api/tests/test_people_mutation_result_identity_integrity.py b/services/people-api/tests/test_people_mutation_result_identity_integrity.py new file mode 100644 index 000000000..0871c8dc5 --- /dev/null +++ b/services/people-api/tests/test_people_mutation_result_identity_integrity.py @@ -0,0 +1,301 @@ +"""Result-to-command identity regressions for governed People mutations.""" + +from __future__ import annotations + +from datetime import date +from decimal import Decimal +import unittest +from uuid import UUID + +from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.hire import ( + HireAcceptanceCommand, + HireAcceptanceResult, + HireDecisionIntegrityError, + accept_confirmed_hire, +) +from orgmetra_people_api.mutations import ( + AssignmentMutationCommand, + AssignmentMutationResult, + EmploymentMutationCommand, + EmploymentMutationResult, + PeopleMutationIntegrityError, + PositionMutationCommand, + PositionMutationResult, + create_assignment_record, + create_employment_record, + create_position_record, +) + +TENANT = UUID("0198a412-b100-7000-8000-000000000001") +PERSON = UUID("0198a412-b100-7000-8000-000000000020") +PERSON_NAME = UUID("0198a412-b100-7000-8000-000000000021") +CANDIDATE = UUID("0198a412-b100-7000-8000-000000000022") +SELECTION_DECISION = UUID("0198a412-b100-7000-8000-000000000023") +EMPLOYMENT = UUID("0198a412-b100-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-b100-7000-8000-000000000031") +POSITION = UUID("0198a412-b100-7000-8000-000000000040") +POSITION_VERSION = UUID("0198a412-b100-7000-8000-000000000041") +ORGANIZATION = UUID("0198a412-b100-7000-8000-000000000050") +JOB = UUID("0198a412-b100-7000-8000-000000000060") +ASSIGNMENT = UUID("0198a412-b100-7000-8000-000000000070") +CONVERSION = UUID("0198a412-b100-7000-8000-000000000071") +AUDIT_EVENT = UUID("0198a412-b100-7000-8000-000000000080") +OUTBOX = UUID("0198a412-b100-7000-8000-000000000081") +OTHER = UUID("0198a412-b100-7000-8000-000000000099") +EFFECTIVE_FROM = date(2026, 9, 5) + +PRINCIPAL = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:result-integrity-operator", + granted_scope_codes=frozenset( + { + "orgmetra.people.write", + "orgmetra.job_architecture.write", + "orgmetra.people.materialize_worker", + } + ), +) + + +def _employment_command() -> EmploymentMutationCommand: + """Build one governed Employment create command.""" + return EmploymentMutationCommand( + tenant_record_id=TENANT, + person_record_id=PERSON, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + employment_status_code="active", + employment_concurrency_code="exclusive", + effective_from=EFFECTIVE_FROM, + confirmation_reference="human_confirmation:result-integrity", + evidence_version_code="result-integrity-v1", + idempotency_key="result-integrity-employment-1", + ) + + +def _position_command() -> PositionMutationCommand: + """Build one governed Position create command.""" + return PositionMutationCommand( + tenant_record_id=TENANT, + organization_unit_id=ORGANIZATION, + job_profile_id=JOB, + position_record_id=POSITION, + position_record_version_id=POSITION_VERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + position_status_code="open", + effective_from=EFFECTIVE_FROM, + confirmation_reference="human_confirmation:result-integrity", + evidence_version_code="result-integrity-v1", + idempotency_key="result-integrity-position-1", + ) + + +def _assignment_command() -> AssignmentMutationCommand: + """Build one governed Assignment create command.""" + return AssignmentMutationCommand( + tenant_record_id=TENANT, + employment_record_id=EMPLOYMENT, + person_record_id=PERSON, + position_record_id=POSITION, + assignment_record_id=ASSIGNMENT, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + allocation_ratio=Decimal("1.0000"), + effective_from=EFFECTIVE_FROM, + confirmation_reference="human_confirmation:result-integrity", + evidence_version_code="result-integrity-v1", + idempotency_key="result-integrity-assignment-1", + ) + + +def _hire_command() -> HireAcceptanceCommand: + """Build one governed confirmed-hire command.""" + return HireAcceptanceCommand( + tenant_record_id=TENANT, + candidate_profile_id=CANDIDATE, + selection_decision_id=SELECTION_DECISION, + person_record_id=PERSON, + person_name_record_id=PERSON_NAME, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + candidate_worker_conversion_record_id=CONVERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + effective_from=EFFECTIVE_FROM, + display_name="Result Integrity Worker", + idempotency_key="result-integrity-hire-1", + ) + + +def _policy( + *, + resource_kind: str, + purpose_code: str, + operation_code: str, + scope_code: str, + field_name: str, +) -> PurposeBoundAccessPolicy: + """Build one exact purpose-bound policy for a mutation target.""" + return PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="result-integrity-v1", + resource_kind=resource_kind, + purpose_code=purpose_code, + operation_code=operation_code, + required_scope_code=scope_code, + permitted_fields=frozenset({field_name}), + ) + + +class _PeopleResultPort: + """Return supplied structurally valid results without honoring command identity.""" + + def __init__( + self, + *, + employment_result: EmploymentMutationResult | None = None, + position_result: PositionMutationResult | None = None, + assignment_result: AssignmentMutationResult | None = None, + ) -> None: + """Retain the result selected by each focused regression.""" + self.employment_result = employment_result + self.position_result = position_result + self.assignment_result = assignment_result + + def create_employment(self, *, command: EmploymentMutationCommand, authorization: object) -> EmploymentMutationResult: + """Return the configured Employment result.""" + del command, authorization + assert self.employment_result is not None + return self.employment_result + + def create_position(self, *, command: PositionMutationCommand, authorization: object) -> PositionMutationResult: + """Return the configured Position result.""" + del command, authorization + assert self.position_result is not None + return self.position_result + + def create_assignment(self, *, command: AssignmentMutationCommand, authorization: object) -> AssignmentMutationResult: + """Return the configured Assignment result.""" + del command, authorization + assert self.assignment_result is not None + return self.assignment_result + + +class _HireResultPort: + """Return one structurally valid confirmed-hire result supplied by the regression.""" + + def __init__(self, result: HireAcceptanceResult) -> None: + """Retain the result without deriving it from the command.""" + self.result = result + + def accept_hire(self, *, command: HireAcceptanceCommand, authorization: object) -> HireAcceptanceResult: + """Return the configured hire result.""" + del command, authorization + return self.result + + +class PeopleMutationResultIdentityTests(unittest.TestCase): + """Require generic People port results to name exactly the commanded records.""" + + def test_employment_result_must_match_command_identity(self) -> None: + """A valid but different Employment identity must fail closed.""" + with self.assertRaisesRegex(PeopleMutationIntegrityError, "employment result identity"): + create_employment_record( + principal=PRINCIPAL, + command=_employment_command(), + purpose_code="workforce_admin", + policy=_policy( + resource_kind="employment_record", + purpose_code="workforce_admin", + operation_code="create_record", + scope_code="orgmetra.people.write", + field_name="employment_record", + ), + mutation_port=_PeopleResultPort( + employment_result=EmploymentMutationResult(employment_record_id=OTHER) + ), + ) + + def test_position_result_must_match_command_identity(self) -> None: + """A valid but different Position identity must fail closed.""" + with self.assertRaisesRegex(PeopleMutationIntegrityError, "position result identity"): + create_position_record( + principal=PRINCIPAL, + command=_position_command(), + purpose_code="job_architecture_admin", + policy=_policy( + resource_kind="position_record", + purpose_code="job_architecture_admin", + operation_code="create_record", + scope_code="orgmetra.job_architecture.write", + field_name="position_record", + ), + mutation_port=_PeopleResultPort(position_result=PositionMutationResult(position_record_id=OTHER)), + ) + + def test_assignment_result_must_match_command_identity(self) -> None: + """A valid but different Assignment identity must fail closed.""" + with self.assertRaisesRegex(PeopleMutationIntegrityError, "assignment result identity"): + create_assignment_record( + principal=PRINCIPAL, + command=_assignment_command(), + purpose_code="workforce_admin", + policy=_policy( + resource_kind="assignment_record", + purpose_code="workforce_admin", + operation_code="create_record", + scope_code="orgmetra.people.write", + field_name="assignment_record", + ), + mutation_port=_PeopleResultPort( + assignment_result=AssignmentMutationResult(assignment_record_id=OTHER) + ), + ) + + +class HireResultIdentityTests(unittest.TestCase): + """Require confirmed-hire results to preserve every commanded authoritative identity.""" + + def test_hire_result_must_match_person_employment_and_conversion_identities(self) -> None: + """Any structurally valid but foreign hire identity must fail closed.""" + mismatched_results = ( + HireAcceptanceResult( + person_record_id=OTHER, + employment_record_id=EMPLOYMENT, + candidate_worker_conversion_record_id=CONVERSION, + ), + HireAcceptanceResult( + person_record_id=PERSON, + employment_record_id=OTHER, + candidate_worker_conversion_record_id=CONVERSION, + ), + HireAcceptanceResult( + person_record_id=PERSON, + employment_record_id=EMPLOYMENT, + candidate_worker_conversion_record_id=OTHER, + ), + ) + for result in mismatched_results: + with self.subTest(result=result), self.assertRaisesRegex(HireDecisionIntegrityError, "hire result identity"): + accept_confirmed_hire( + principal=PRINCIPAL, + command=_hire_command(), + purpose_code="candidate_hire", + policy=_policy( + resource_kind="selection_decision", + purpose_code="candidate_hire", + operation_code="materialize_worker", + scope_code="orgmetra.people.materialize_worker", + field_name="candidate_worker_conversion", + ), + mutation_port=_HireResultPort(result), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/people-api/tests/test_people_mutation_result_target_snapshot.py b/services/people-api/tests/test_people_mutation_result_target_snapshot.py new file mode 100644 index 000000000..17d56c9c6 --- /dev/null +++ b/services/people-api/tests/test_people_mutation_result_target_snapshot.py @@ -0,0 +1,202 @@ +"""Pre-port target binding regressions for People mutation results.""" + +from __future__ import annotations + +from datetime import date +import unittest +from uuid import UUID + +from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.hire import ( + HireAcceptanceCommand, + HireAcceptanceResult, + HireDecisionIntegrityError, + accept_confirmed_hire, +) +from orgmetra_people_api.mutations import ( + AssignmentMutationCommand, + AssignmentMutationResult, + EmploymentMutationCommand, + EmploymentMutationResult, + PeopleMutationIntegrityError, + PositionMutationCommand, + PositionMutationResult, + create_employment_record, +) + +TENANT = UUID("0198a412-b200-7000-8000-000000000001") +PERSON = UUID("0198a412-b200-7000-8000-000000000020") +PERSON_NAME = UUID("0198a412-b200-7000-8000-000000000021") +CANDIDATE = UUID("0198a412-b200-7000-8000-000000000022") +SELECTION_DECISION = UUID("0198a412-b200-7000-8000-000000000023") +EMPLOYMENT = UUID("0198a412-b200-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-b200-7000-8000-000000000031") +CONVERSION = UUID("0198a412-b200-7000-8000-000000000040") +AUDIT_EVENT = UUID("0198a412-b200-7000-8000-000000000050") +OUTBOX = UUID("0198a412-b200-7000-8000-000000000051") +OTHER = UUID("0198a412-b200-7000-8000-000000000099") +EFFECTIVE_FROM = date(2026, 9, 5) + +PRINCIPAL = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:target-snapshot-operator", + granted_scope_codes=frozenset( + {"orgmetra.people.write", "orgmetra.people.materialize_worker"} + ), +) + + +def _employment_command() -> EmploymentMutationCommand: + """Build one valid Employment command whose target can be rewritten by a port.""" + return EmploymentMutationCommand( + tenant_record_id=TENANT, + person_record_id=PERSON, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + employment_status_code="active", + employment_concurrency_code="exclusive", + effective_from=EFFECTIVE_FROM, + confirmation_reference="human_confirmation:target-snapshot", + evidence_version_code="target-snapshot-v1", + idempotency_key="target-snapshot-employment-1", + ) + + +def _hire_command() -> HireAcceptanceCommand: + """Build one valid hire command whose target can be rewritten by a port.""" + return HireAcceptanceCommand( + tenant_record_id=TENANT, + candidate_profile_id=CANDIDATE, + selection_decision_id=SELECTION_DECISION, + person_record_id=PERSON, + person_name_record_id=PERSON_NAME, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + candidate_worker_conversion_record_id=CONVERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + effective_from=EFFECTIVE_FROM, + display_name="Target Snapshot Worker", + idempotency_key="target-snapshot-hire-1", + ) + + +def _policy( + *, + resource_kind: str, + purpose_code: str, + operation_code: str, + scope_code: str, + field_name: str, +) -> PurposeBoundAccessPolicy: + """Build one exact policy for the focused mutation boundary.""" + return PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="target-snapshot-v1", + resource_kind=resource_kind, + purpose_code=purpose_code, + operation_code=operation_code, + required_scope_code=scope_code, + permitted_fields=frozenset({field_name}), + ) + + +class _MutatingPeoplePort: + """Rewrite the caller command during the port call and report the rewritten identity.""" + + def create_employment( + self, + *, + command: EmploymentMutationCommand, + authorization: object, + ) -> EmploymentMutationResult: + """Replace the commanded Employment target before returning a valid result.""" + del authorization + object.__setattr__(command, "employment_record_id", OTHER) + return EmploymentMutationResult(employment_record_id=OTHER) + + def create_position( + self, + *, + command: PositionMutationCommand, + authorization: object, + ) -> PositionMutationResult: + """Reject unrelated Position work while satisfying the runtime protocol.""" + del command, authorization + raise AssertionError("position mutation is outside this regression") + + def create_assignment( + self, + *, + command: AssignmentMutationCommand, + authorization: object, + ) -> AssignmentMutationResult: + """Reject unrelated Assignment work while satisfying the runtime protocol.""" + del command, authorization + raise AssertionError("assignment mutation is outside this regression") + + +class _MutatingHirePort: + """Rewrite all hire targets during the port call and report those rewritten identities.""" + + def accept_hire( + self, + *, + command: HireAcceptanceCommand, + authorization: object, + ) -> HireAcceptanceResult: + """Replace authoritative targets after authorization but before service return.""" + del authorization + object.__setattr__(command, "person_record_id", OTHER) + object.__setattr__(command, "employment_record_id", OTHER) + object.__setattr__(command, "candidate_worker_conversion_record_id", OTHER) + return HireAcceptanceResult( + person_record_id=OTHER, + employment_record_id=OTHER, + candidate_worker_conversion_record_id=OTHER, + ) + + +class PeopleMutationResultTargetSnapshotTests(unittest.TestCase): + """Require result coherence against targets captured before executable port work.""" + + def test_employment_result_check_uses_pre_port_target(self) -> None: + """A port must not redefine the expected Employment identity by mutating the command.""" + with self.assertRaisesRegex(PeopleMutationIntegrityError, "employment result identity"): + create_employment_record( + principal=PRINCIPAL, + command=_employment_command(), + purpose_code="workforce_admin", + policy=_policy( + resource_kind="employment_record", + purpose_code="workforce_admin", + operation_code="create_record", + scope_code="orgmetra.people.write", + field_name="employment_record", + ), + mutation_port=_MutatingPeoplePort(), + ) + + def test_hire_result_check_uses_pre_port_targets(self) -> None: + """A port must not redefine Person/Employment/conversion result authority.""" + with self.assertRaisesRegex(HireDecisionIntegrityError, "hire result identity"): + accept_confirmed_hire( + principal=PRINCIPAL, + command=_hire_command(), + purpose_code="candidate_hire", + policy=_policy( + resource_kind="selection_decision", + purpose_code="candidate_hire", + operation_code="materialize_worker", + scope_code="orgmetra.people.materialize_worker", + field_name="candidate_worker_conversion", + ), + mutation_port=_MutatingHirePort(), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/people-api/tests/test_people_mutation_runtime_integrity.py b/services/people-api/tests/test_people_mutation_runtime_integrity.py new file mode 100644 index 000000000..2b40dc67f --- /dev/null +++ b/services/people-api/tests/test_people_mutation_runtime_integrity.py @@ -0,0 +1,236 @@ +"""Runtime-integrity regressions for authoritative People mutations.""" + +from __future__ import annotations + +from datetime import date +from decimal import Decimal +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import AuthorizationDecision, PurposeBoundAccessPolicy +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.mutations import ( + AssignmentMutationCommand, + EmploymentMutationCommand, + EmploymentMutationResult, + command_route, + create_employment_record, + mutation_command_digest, +) + +TENANT = UUID("0198a412-8000-7000-8000-000000000001") +PERSON = UUID("0198a412-8000-7000-8000-000000000020") +EMPLOYMENT = UUID("0198a412-8000-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-8000-7000-8000-000000000031") +AUDIT_EVENT = UUID("0198a412-8000-7000-8000-000000000080") +OUTBOX = UUID("0198a412-8000-7000-8000-000000000081") + + +class _ForgedUUID(UUID): + """Attempt to rewrite mutation identity text during canonical digesting.""" + + def __str__(self) -> str: + """Render an identity different from the underlying UUID.""" + return "0198a412-8000-7000-8000-ffffffffffff" + + +class _ForgedDecimal(Decimal): + """Attempt to rewrite an assignment ratio during canonical digesting.""" + + def __format__(self, spec: str) -> str: + """Render a ratio different from the underlying Decimal value.""" + del spec + return "0.9999" + + +class _UnvalidatedEmploymentCommand(EmploymentMutationCommand): + """Attempt to bypass base command validation through post-init dispatch.""" + + def __post_init__(self) -> None: + """Intentionally skip the governed base validation.""" + + +class _UnvalidatedEmploymentResult(EmploymentMutationResult): + """Attempt to bypass persistence-result validation.""" + + def __post_init__(self) -> None: + """Intentionally skip the governed base validation.""" + + +def _employment_values(**overrides: object) -> dict[str, object]: + """Return one otherwise-valid employment mutation command mapping.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "person_record_id": PERSON, + "employment_record_id": EMPLOYMENT, + "employment_record_version_id": EMPLOYMENT_VERSION, + "audit_event_record_id": AUDIT_EVENT, + "outbox_delivery_record_id": OUTBOX, + "employment_status_code": "active", + "employment_concurrency_code": "exclusive", + "effective_from": date(2026, 8, 21), + "confirmation_reference": "human_confirmation:runtime-21", + "evidence_version_code": "decision_evidence_set:v1", + "idempotency_key": "mutation-runtime-key-21", + } + values.update(overrides) + return values + + +def _employment(**overrides: object) -> EmploymentMutationCommand: + """Build one exact employment mutation command.""" + return EmploymentMutationCommand(**_employment_values(**overrides)) # type: ignore[arg-type] + + +def _assignment(**overrides: object) -> AssignmentMutationCommand: + """Build one exact assignment mutation command.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "employment_record_id": EMPLOYMENT, + "person_record_id": PERSON, + "position_record_id": UUID("0198a412-8000-7000-8000-000000000040"), + "assignment_record_id": UUID("0198a412-8000-7000-8000-000000000070"), + "audit_event_record_id": AUDIT_EVENT, + "outbox_delivery_record_id": OUTBOX, + "allocation_ratio": Decimal("1.0000"), + "effective_from": date(2026, 8, 21), + "confirmation_reference": "human_confirmation:runtime-21", + "evidence_version_code": "decision_evidence_set:v1", + "idempotency_key": "mutation-runtime-key-21", + } + values.update(overrides) + return AssignmentMutationCommand(**values) # type: ignore[arg-type] + + +def _decision() -> AuthorizationDecision: + """Build one minimal exact authorization decision for digest testing.""" + return AuthorizationDecision( + allowed=True, + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-21", + resource_reference=f"employment_record:{EMPLOYMENT.hex}", + policy_version_code="people-mutation-v1", + purpose_code="workforce_admin", + operation_code="create_record", + resource_kind="employment_record", + requested_fields=frozenset({"employment_record"}), + authorized_fields=frozenset({"employment_record"}), + reason_code="access_permitted", + next_action="Continue with only the authorized fields.", + ) + + +def test_mutation_command_rejects_uuid_subclass_before_digest_or_persistence() -> None: + """Caller-controlled UUID rendering cannot rewrite People mutation identity.""" + forged = _ForgedUUID("0198a412-8000-7000-8000-000000000123") + with pytest.raises(ValueError, match="person_record_id must be an operational UUID"): + _employment(person_record_id=forged) + + +def test_mutation_result_rejects_uuid_subclass_before_service_return() -> None: + """Persistence cannot return an identity object with forged rendering.""" + forged = _ForgedUUID("0198a412-8000-7000-8000-000000000123") + with pytest.raises(ValueError, match="employment_record_id must be an operational UUID"): + EmploymentMutationResult(employment_record_id=forged) + + +def test_assignment_rejects_decimal_subclass_before_canonical_ratio_digest() -> None: + """Allocation evidence cannot invoke caller-controlled Decimal formatting.""" + forged = _ForgedDecimal("0.5000") + with pytest.raises(ValueError, match="allocation_ratio must be a Decimal"): + _assignment(allocation_ratio=forged) + + +def test_command_helpers_reject_validation_bypassing_subclasses() -> None: + """Routing and digest helpers require exact validated mutation commands.""" + forged = _UnvalidatedEmploymentCommand( + **_employment_values(person_record_id="not-a-uuid") # type: ignore[arg-type] + ) + with pytest.raises(TypeError, match="governed People mutation command"): + command_route(forged) + with pytest.raises(TypeError, match="governed People mutation command"): + mutation_command_digest(command=forged, authorization=_decision()) + + +def test_create_employment_rejects_command_subclass_before_authorization_or_port() -> None: + """A command that skipped post-init validation cannot reach the mutation port.""" + forged = _UnvalidatedEmploymentCommand( + **_employment_values(person_record_id="not-a-uuid") # type: ignore[arg-type] + ) + principal = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-21", + granted_scope_codes=frozenset({"orgmetra.people.write"}), + ) + policy = PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="people-mutation-v1", + resource_kind="employment_record", + purpose_code="workforce_admin", + operation_code="create_record", + required_scope_code="orgmetra.people.write", + permitted_fields=frozenset({"employment_record"}), + ) + + class _Port: + def create_employment(self, *, command: EmploymentMutationCommand, authorization: object) -> EmploymentMutationResult: + del command, authorization + pytest.fail("validation-bypassing command reached persistence") + + def create_position(self, *, command: object, authorization: object) -> object: + del command, authorization + raise AssertionError + + def create_assignment(self, *, command: object, authorization: object) -> object: + del command, authorization + raise AssertionError + + with pytest.raises(TypeError, match="command must be an EmploymentMutationCommand"): + create_employment_record( + principal=principal, + command=forged, + purpose_code="workforce_admin", + policy=policy, + mutation_port=_Port(), # type: ignore[arg-type] + ) + + +def test_create_employment_rejects_result_subclass_that_skipped_validation() -> None: + """Malformed result subclasses cannot cross the authoritative mutation boundary.""" + principal = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-21", + granted_scope_codes=frozenset({"orgmetra.people.write"}), + ) + policy = PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="people-mutation-v1", + resource_kind="employment_record", + purpose_code="workforce_admin", + operation_code="create_record", + required_scope_code="orgmetra.people.write", + permitted_fields=frozenset({"employment_record"}), + ) + + class _Port: + def create_employment(self, *, command: EmploymentMutationCommand, authorization: object) -> EmploymentMutationResult: + del command, authorization + return _UnvalidatedEmploymentResult(employment_record_id="not-a-uuid") # type: ignore[arg-type] + + def create_position(self, *, command: object, authorization: object) -> object: + del command, authorization + raise AssertionError + + def create_assignment(self, *, command: object, authorization: object) -> object: + del command, authorization + raise AssertionError + + with pytest.raises(TypeError, match="mutation_port must return EmploymentMutationResult"): + create_employment_record( + principal=principal, + command=_employment(), + purpose_code="workforce_admin", + policy=policy, + mutation_port=_Port(), # type: ignore[arg-type] + ) diff --git a/services/people-api/tests/test_people_mutation_text_runtime_integrity.py b/services/people-api/tests/test_people_mutation_text_runtime_integrity.py new file mode 100644 index 000000000..2b0bad992 --- /dev/null +++ b/services/people-api/tests/test_people_mutation_text_runtime_integrity.py @@ -0,0 +1,200 @@ +"""Reject caller-controlled text subclasses at authoritative People write boundaries.""" + +from __future__ import annotations + +from datetime import date +from uuid import UUID + +import pytest + +from orgmetra_people_api.hire import HireAcceptanceCommand +from orgmetra_people_api.mutations import EmploymentMutationCommand, PositionMutationCommand + +TENANT = UUID("0198a412-8000-7000-8000-000000000101") +PERSON = UUID("0198a412-8000-7000-8000-000000000102") +CANDIDATE = UUID("0198a412-8000-7000-8000-000000000103") +SELECTION_DECISION = UUID("0198a412-8000-7000-8000-000000000104") +EMPLOYMENT = UUID("0198a412-8000-7000-8000-000000000105") +EMPLOYMENT_VERSION = UUID("0198a412-8000-7000-8000-000000000106") +ORGANIZATION = UUID("0198a412-8000-7000-8000-000000000107") +JOB = UUID("0198a412-8000-7000-8000-000000000108") +POSITION = UUID("0198a412-8000-7000-8000-000000000109") +POSITION_VERSION = UUID("0198a412-8000-7000-8000-00000000010a") +PERSON_NAME = UUID("0198a412-8000-7000-8000-00000000010b") +CONVERSION = UUID("0198a412-8000-7000-8000-00000000010c") +AUDIT = UUID("0198a412-8000-7000-8000-00000000010d") +OUTBOX = UUID("0198a412-8000-7000-8000-00000000010e") + + +class _ForgedClosedCode(str): + """Present unsafe underlying text as the reviewed ``active`` status.""" + + def __hash__(self) -> int: + """Collide with the reviewed status during set lookup.""" + return hash("active") + + def __eq__(self, other: object) -> bool: + """Claim equality with the reviewed status while retaining unsafe text.""" + return other == "active" + + def __ne__(self, other: object) -> bool: + """Keep inequality consistent with the forged equality result.""" + return not self.__eq__(other) + + +class _ForgedConcurrencyCode(str): + """Present unsafe underlying text as the reviewed ``exclusive`` code.""" + + def __hash__(self) -> int: + """Collide with the reviewed concurrency code during set lookup.""" + return hash("exclusive") + + def __eq__(self, other: object) -> bool: + """Claim equality with the reviewed concurrency code.""" + return other == "exclusive" + + def __ne__(self, other: object) -> bool: + """Keep inequality consistent with the forged equality result.""" + return not self.__eq__(other) + + +class _ForgedIdempotencyKey(str): + """Hide an unsafe underlying key from length and character validation.""" + + def __len__(self) -> int: + """Pretend the key satisfies the governed length contract.""" + return 20 + + def __iter__(self): + """Yield only visible ASCII while retaining unsafe underlying text.""" + return iter("A" * 20) + + +class _ForgedDisplayName(str): + """Hide control-character PII from the mutable Person-name validation path.""" + + def encode(self, *args: object, **kwargs: object) -> bytes: + """Pretend the underlying text encodes as a harmless display name.""" + del args, kwargs + return b"Alice" + + def strip(self, *args: object, **kwargs: object) -> str: + """Pretend the underlying text contains usable non-whitespace content.""" + del args, kwargs + return "Alice" + + def __len__(self) -> int: + """Pretend the underlying text satisfies the bounded PII length.""" + return 5 + + def __iter__(self): + """Hide the underlying control character from character validation.""" + return iter("Alice") + + +class _ForgedGovernanceText(str): + """Present reviewed governance text with caller-defined rendering semantics.""" + + def __str__(self) -> str: + """Render a different value if canonical evidence later formats the field.""" + return "caller_defined_governance_text" + + +def _employment(**overrides: object) -> EmploymentMutationCommand: + """Build one otherwise-valid high-impact employment mutation command.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "person_record_id": PERSON, + "employment_record_id": EMPLOYMENT, + "employment_record_version_id": EMPLOYMENT_VERSION, + "audit_event_record_id": AUDIT, + "outbox_delivery_record_id": OUTBOX, + "employment_status_code": "active", + "employment_concurrency_code": "exclusive", + "effective_from": date(2026, 8, 22), + "confirmation_reference": "human_confirmation:text-runtime-22", + "evidence_version_code": "decision_evidence_set:v1", + "idempotency_key": "people-text-runtime-key-22", + } + values.update(overrides) + return EmploymentMutationCommand(**values) # type: ignore[arg-type] + + +def _position(**overrides: object) -> PositionMutationCommand: + """Build one otherwise-valid position mutation command.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "organization_unit_id": ORGANIZATION, + "job_profile_id": JOB, + "position_record_id": POSITION, + "position_record_version_id": POSITION_VERSION, + "audit_event_record_id": AUDIT, + "outbox_delivery_record_id": OUTBOX, + "position_status_code": "active", + "effective_from": date(2026, 8, 22), + "confirmation_reference": "human_confirmation:text-runtime-22", + "evidence_version_code": "position_evidence:v1", + "idempotency_key": "position-text-runtime-key-22", + } + values.update(overrides) + return PositionMutationCommand(**values) # type: ignore[arg-type] + + +def _hire(**overrides: object) -> HireAcceptanceCommand: + """Build one otherwise-valid confirmed-hire command.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "candidate_profile_id": CANDIDATE, + "selection_decision_id": SELECTION_DECISION, + "person_record_id": PERSON, + "person_name_record_id": PERSON_NAME, + "employment_record_id": EMPLOYMENT, + "employment_record_version_id": EMPLOYMENT_VERSION, + "candidate_worker_conversion_record_id": CONVERSION, + "audit_event_record_id": AUDIT, + "outbox_delivery_record_id": OUTBOX, + "effective_from": date(2026, 8, 22), + "display_name": "Alice Example", + "idempotency_key": "hire-text-runtime-key-22", + "employment_status_code": "active", + } + values.update(overrides) + return HireAcceptanceCommand(**values) # type: ignore[arg-type] + + +def test_rejects_status_string_subclass_that_forges_allow_list_membership() -> None: + """Canonical employment status text must be the exact value that was reviewed.""" + with pytest.raises(ValueError, match="employment_status_code"): + _employment(employment_status_code=_ForgedClosedCode("model_decided")) + with pytest.raises(ValueError, match="position_status_code"): + _position(position_status_code=_ForgedClosedCode("model_decided")) + with pytest.raises(ValueError, match="employment_status_code"): + _hire(employment_status_code=_ForgedClosedCode("model_decided")) + + +def test_rejects_concurrency_string_subclass_that_forges_allow_list_membership() -> None: + """Concurrency evidence cannot substitute caller-defined equality semantics.""" + with pytest.raises(ValueError, match="employment_concurrency_code"): + _employment(employment_concurrency_code=_ForgedConcurrencyCode("shadow_parallel")) + + +def test_rejects_idempotency_string_subclass_that_forges_scalar_validation() -> None: + """Idempotency identity must bind the exact validated visible-ASCII text.""" + with pytest.raises(ValueError, match="idempotency_key"): + _employment(idempotency_key=_ForgedIdempotencyKey("\n")) + + +def test_rejects_display_name_string_subclass_before_person_pii_persistence() -> None: + """Necessary Person-name PII cannot hide control text behind overridden methods.""" + with pytest.raises(ValueError, match="display_name"): + _hire(display_name=_ForgedDisplayName("\n")) + + +def test_rejects_governance_text_subclasses_before_digest_or_persistence() -> None: + """Confirmation and evidence text must retain the exact reviewed runtime value.""" + with pytest.raises(ValueError, match="confirmation_reference"): + _employment( + confirmation_reference=_ForgedGovernanceText("human_confirmation:text-runtime-22") + ) + with pytest.raises(ValueError, match="evidence_version_code"): + _position(evidence_version_code=_ForgedGovernanceText("position_evidence:v1")) diff --git a/services/people-api/tests/test_postgres_hire_authority_runtime_integrity.py b/services/people-api/tests/test_postgres_hire_authority_runtime_integrity.py new file mode 100644 index 000000000..ff45c5232 --- /dev/null +++ b/services/people-api/tests/test_postgres_hire_authority_runtime_integrity.py @@ -0,0 +1,99 @@ +"""Adversarial runtime-integrity contracts for the PostgreSQL hire authority boundary.""" + +from __future__ import annotations + +from datetime import date +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import AuthorizationDecision +from orgmetra_people_api.hire import HireAcceptanceCommand, HireDecisionIntegrityError +from orgmetra_people_api.postgres_hire import PostgresHireAcceptancePort + +TENANT = UUID("0198a412-7100-7000-8000-000000000001") +CANDIDATE = UUID("0198a412-7100-7000-8000-000000000010") +DECISION = UUID("0198a412-7100-7000-8000-000000000011") +PERSON = UUID("0198a412-7100-7000-8000-000000000020") +PERSON_NAME = UUID("0198a412-7100-7000-8000-000000000021") +EMPLOYMENT = UUID("0198a412-7100-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-7100-7000-8000-000000000031") +CONVERSION = UUID("0198a412-7100-7000-8000-000000000040") +AUDIT_EVENT = UUID("0198a412-7100-7000-8000-000000000050") +OUTBOX_DELIVERY = UUID("0198a412-7100-7000-8000-000000000051") +ACTOR = "keyverse_subject:operator-17" +PURPOSE = "candidate_hire" + + +class ForgedHireAcceptanceCommand(HireAcceptanceCommand): + """Represent a validation-bypassing caller-defined hire command subtype.""" + + +class ForgedAuthorizationDecision(AuthorizationDecision): + """Represent a caller-defined authorization subtype at a trust boundary.""" + + +def _command(command_type: type[HireAcceptanceCommand] = HireAcceptanceCommand) -> HireAcceptanceCommand: + """Build one deterministic valid hire command using the requested runtime type.""" + return command_type( + tenant_record_id=TENANT, + candidate_profile_id=CANDIDATE, + selection_decision_id=DECISION, + person_record_id=PERSON, + person_name_record_id=PERSON_NAME, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + candidate_worker_conversion_record_id=CONVERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX_DELIVERY, + effective_from=date(2026, 8, 18), + display_name="Ada Lovelace", + idempotency_key="hire-authority-runtime-integrity", + ) + + +def _authorization( + authorization_type: type[AuthorizationDecision] = AuthorizationDecision, +) -> AuthorizationDecision: + """Build one deterministic exact-scope allow decision using the requested runtime type.""" + return authorization_type( + allowed=True, + tenant_record_id=TENANT, + actor_reference=ACTOR, + resource_reference=f"selection_decision:{DECISION.hex}", + policy_version_code="people-hire-v1", + purpose_code=PURPOSE, + operation_code="materialize_worker", + resource_kind="selection_decision", + requested_fields=frozenset({"candidate_worker_conversion"}), + authorized_fields=frozenset({"candidate_worker_conversion"}), + reason_code="access_permitted", + next_action="continue", + ) + + +def _forbidden_connection_factory() -> object: + """Fail the regression if untrusted runtime input reaches database work.""" + raise AssertionError("database work must not begin for forged runtime authority objects") + + +def test_postgres_hire_port_rejects_command_subclass_before_database_work() -> None: + """Require the persistence authority to accept only the exact governed command type.""" + port = PostgresHireAcceptancePort(_forbidden_connection_factory) + + with pytest.raises(TypeError, match="command must be a HireAcceptanceCommand"): + port.accept_hire( + command=_command(ForgedHireAcceptanceCommand), + authorization=_authorization(), + ) + + +def test_postgres_hire_port_rejects_authorization_subclass_before_database_work() -> None: + """Require the persistence authority to accept only the exact governed authorization type.""" + port = PostgresHireAcceptancePort(_forbidden_connection_factory) + + with pytest.raises(HireDecisionIntegrityError, match="typed authorization decision"): + port.accept_hire( + command=_command(), + authorization=_authorization(ForgedAuthorizationDecision), + ) diff --git a/services/people-api/tests/test_postgres_hire_idempotency_text_runtime_integrity.py b/services/people-api/tests/test_postgres_hire_idempotency_text_runtime_integrity.py new file mode 100644 index 000000000..8b3d73cbf --- /dev/null +++ b/services/people-api/tests/test_postgres_hire_idempotency_text_runtime_integrity.py @@ -0,0 +1,119 @@ +"""Runtime-integrity contracts for durable hire idempotency digest text.""" + +from __future__ import annotations + +from datetime import date +from typing import Any +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import AuthorizationDecision +from orgmetra_people_api.hire import HireAcceptanceCommand, HireDecisionIntegrityError +from orgmetra_people_api.postgres_hire import _hire_command_digest, _replayed_hire + +TENANT = UUID("0198a412-7100-7000-8000-000000000001") +CANDIDATE = UUID("0198a412-7100-7000-8000-000000000010") +DECISION = UUID("0198a412-7100-7000-8000-000000000011") +PERSON = UUID("0198a412-7100-7000-8000-000000000020") +PERSON_NAME = UUID("0198a412-7100-7000-8000-000000000021") +EMPLOYMENT = UUID("0198a412-7100-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-7100-7000-8000-000000000031") +CONVERSION = UUID("0198a412-7100-7000-8000-000000000040") +AUDIT_EVENT = UUID("0198a412-7100-7000-8000-000000000050") +OUTBOX_DELIVERY = UUID("0198a412-7100-7000-8000-000000000051") +ACTOR = "keyverse_subject:operator-17" +PURPOSE = "candidate_hire" + + +class _ExecutableText(str): + """Expose comparison performed before exact durable-text validation.""" + + def __eq__(self, other: object) -> bool: + """Fail if untrusted text participates in trusted equality.""" + del other + raise AssertionError("text subtype equality executed before exact-type validation") + + def __ne__(self, other: object) -> bool: + """Fail if untrusted text participates in trusted inequality.""" + del other + raise AssertionError("text subtype inequality executed before exact-type validation") + + +class _ReplayCursor: + """Return one durable idempotency row without touching a real database.""" + + def __init__(self, row: tuple[object, object]) -> None: + """Store the single replay row returned after advisory serialization.""" + self._row = row + + def execute(self, statement: str, parameters: tuple[object, ...]) -> None: + """Accept the two read-side SQL calls used by replay resolution.""" + assert statement + assert parameters + + def fetchmany(self, size: int) -> list[tuple[object, object]]: + """Return the configured row using the adapter's bounded read size.""" + assert size == 2 + return [self._row] + + +def _command() -> HireAcceptanceCommand: + """Build one deterministic valid confirmed-hire command.""" + return HireAcceptanceCommand( + tenant_record_id=TENANT, + candidate_profile_id=CANDIDATE, + selection_decision_id=DECISION, + person_record_id=PERSON, + person_name_record_id=PERSON_NAME, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + candidate_worker_conversion_record_id=CONVERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX_DELIVERY, + effective_from=date(2026, 8, 18), + display_name="Ada Lovelace", + idempotency_key="hire-idempotency-text-runtime-integrity", + ) + + +def _authorization() -> AuthorizationDecision: + """Build the exact allow decision required for the confirmed-hire command.""" + return AuthorizationDecision( + allowed=True, + tenant_record_id=TENANT, + actor_reference=ACTOR, + resource_reference=f"selection_decision:{DECISION.hex}", + policy_version_code="people-hire-v1", + purpose_code=PURPOSE, + operation_code="materialize_worker", + resource_kind="selection_decision", + requested_fields=frozenset({"candidate_worker_conversion"}), + authorized_fields=frozenset({"candidate_worker_conversion"}), + reason_code="access_permitted", + next_action="continue", + ) + + +def test_hire_replay_rejects_digest_subtype_before_comparison() -> None: + """Database-returned digest subtypes must fail without executing comparison hooks.""" + command = _command() + authorization = _authorization() + digest = _ExecutableText(_hire_command_digest(command, authorization)) + cursor: Any = _ReplayCursor((CONVERSION, digest)) + + with pytest.raises(HireDecisionIntegrityError, match="hire idempotency row is invalid"): + _replayed_hire(cursor, command=command, authorization=authorization) + + +def test_hire_replay_accepts_exact_builtin_digest_text() -> None: + """An exact persisted digest still replays the exact committed conversion.""" + command = _command() + authorization = _authorization() + digest = _hire_command_digest(command, authorization) + cursor: Any = _ReplayCursor((CONVERSION, digest)) + + result = _replayed_hire(cursor, command=command, authorization=authorization) + + assert result is not None + assert result.candidate_worker_conversion_record_id == CONVERSION diff --git a/services/people-api/tests/test_postgres_hire_provenance_text_runtime_integrity.py b/services/people-api/tests/test_postgres_hire_provenance_text_runtime_integrity.py new file mode 100644 index 000000000..ae9e57718 --- /dev/null +++ b/services/people-api/tests/test_postgres_hire_provenance_text_runtime_integrity.py @@ -0,0 +1,181 @@ +"""Runtime-integrity contracts for durable hire decision-provenance text.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import AuthorizationDecision +from orgmetra_people_api.hire import HireAcceptanceCommand, HireDecisionIntegrityError +from orgmetra_people_api.postgres_hire import PostgresHireAcceptancePort + +TENANT = UUID("0198a412-7100-7000-8000-000000000001") +CANDIDATE = UUID("0198a412-7100-7000-8000-000000000010") +DECISION = UUID("0198a412-7100-7000-8000-000000000011") +PERSON = UUID("0198a412-7100-7000-8000-000000000020") +PERSON_NAME = UUID("0198a412-7100-7000-8000-000000000021") +EMPLOYMENT = UUID("0198a412-7100-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-7100-7000-8000-000000000031") +CONVERSION = UUID("0198a412-7100-7000-8000-000000000040") +AUDIT_EVENT = UUID("0198a412-7100-7000-8000-000000000050") +OUTBOX_DELIVERY = UUID("0198a412-7100-7000-8000-000000000051") +EVIDENCE_SET = UUID("0198a412-7100-7000-8000-000000000060") +DECIDED_AT = datetime(2026, 8, 18, 0, 0, tzinfo=timezone.utc) +TRANSACTION_AT = datetime(2026, 8, 18, 0, 1, tzinfo=timezone.utc) +ACTOR = "keyverse_subject:operator-17" +PURPOSE = "candidate_hire" +CONFIRMATION = "human_confirmation:review-88" + + +class _ExecutableText(str): + """Expose comparison performed before exact durable-text validation.""" + + def __eq__(self, other: object) -> bool: + """Fail if durable-row validation executes subtype equality.""" + del other + raise AssertionError("provenance text equality executed before exact-type validation") + + def __ne__(self, other: object) -> bool: + """Fail if durable-row validation executes subtype inequality.""" + del other + raise AssertionError("provenance text inequality executed before exact-type validation") + + +class _Cursor: + """Serve one idempotency miss followed by one decision-provenance row.""" + + def __init__(self, decision_row: tuple[object, ...]) -> None: + """Store the row and initialize transaction-observation state.""" + self._batches = [[], [decision_row]] + self.executions: list[str] = [] + + def __enter__(self) -> _Cursor: + """Return the same cursor for the transaction context.""" + return self + + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: + """Leave exception handling to the connection context.""" + del exc_type, exc_value, traceback + + def execute(self, statement: str, parameters: tuple[object, ...] | None = None) -> None: + """Record SQL without evaluating trust-bearing row values.""" + del parameters + self.executions.append(statement) + + def fetchmany(self, size: int) -> list[tuple[object, ...]]: + """Return each bounded batch in adapter execution order.""" + assert size == 2 + return self._batches.pop(0)[:size] + + +class _Connection: + """Provide the focused cursor through a DB-API-style context boundary.""" + + def __init__(self, cursor: _Cursor) -> None: + """Retain the one cursor used by the focused durable-row test.""" + self._cursor = cursor + + def __enter__(self) -> _Connection: + """Return the same transaction connection.""" + return self + + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: + """Leave exception propagation unchanged.""" + del exc_type, exc_value, traceback + + def cursor(self) -> _Cursor: + """Return the configured focused cursor.""" + return self._cursor + + +def _command() -> HireAcceptanceCommand: + """Build one deterministic valid confirmed-hire command.""" + return HireAcceptanceCommand( + tenant_record_id=TENANT, + candidate_profile_id=CANDIDATE, + selection_decision_id=DECISION, + person_record_id=PERSON, + person_name_record_id=PERSON_NAME, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + candidate_worker_conversion_record_id=CONVERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX_DELIVERY, + effective_from=date(2026, 8, 18), + display_name="Ada Lovelace", + idempotency_key="hire-provenance-text-runtime-integrity", + ) + + +def _authorization() -> AuthorizationDecision: + """Build the exact allow decision required for the confirmed-hire command.""" + return AuthorizationDecision( + allowed=True, + tenant_record_id=TENANT, + actor_reference=ACTOR, + resource_reference=f"selection_decision:{DECISION.hex}", + policy_version_code="people-hire-v1", + purpose_code=PURPOSE, + operation_code="materialize_worker", + resource_kind="selection_decision", + requested_fields=frozenset({"candidate_worker_conversion"}), + authorized_fields=frozenset({"candidate_worker_conversion"}), + reason_code="access_permitted", + next_action="continue", + ) + + +def _decision_row(**overrides: object) -> tuple[object, ...]: + """Build one durable confirmed-hire provenance row in SQL column order.""" + values: dict[str, object] = { + "actor_reference": ACTOR, + "purpose_code": PURPOSE, + "decision_code": "hire", + "confirmation_reference": CONFIRMATION, + "decided_at": DECIDED_AT, + "decision_evidence_set_id": EVIDENCE_SET, + "transaction_recorded_at": TRANSACTION_AT, + } + values.update(overrides) + return ( + values["actor_reference"], + values["purpose_code"], + values["decision_code"], + values["confirmation_reference"], + values["decided_at"], + values["decision_evidence_set_id"], + values["transaction_recorded_at"], + ) + + +@pytest.mark.parametrize( + "field,value", + ( + ("actor_reference", _ExecutableText(ACTOR)), + ("purpose_code", _ExecutableText(PURPOSE)), + ("decision_code", _ExecutableText("hire")), + ("confirmation_reference", _ExecutableText(CONFIRMATION)), + ), +) +def test_hire_rejects_provenance_text_subtype_before_business_write(field: str, value: object) -> None: + """Database provenance text must be exact built-in text before semantic use.""" + cursor = _Cursor(_decision_row(**{field: value})) + port = PostgresHireAcceptancePort(lambda: _Connection(cursor)) + + with pytest.raises(HireDecisionIntegrityError, match="selection decision provenance text is invalid"): + port.accept_hire(command=_command(), authorization=_authorization()) + + assert not any("INSERT INTO public.person_record" in statement for statement in cursor.executions) + + +def test_hire_accepts_exact_builtin_provenance_text() -> None: + """Exact Psycopg-compatible text remains valid durable decision provenance.""" + cursor = _Cursor(_decision_row()) + port = PostgresHireAcceptancePort(lambda: _Connection(cursor)) + + result = port.accept_hire(command=_command(), authorization=_authorization()) + + assert result.candidate_worker_conversion_record_id == CONVERSION + assert any("INSERT INTO public.person_record" in statement for statement in cursor.executions) diff --git a/services/people-api/tests/test_postgres_hire_row_container_runtime_integrity.py b/services/people-api/tests/test_postgres_hire_row_container_runtime_integrity.py new file mode 100644 index 000000000..a58ebfca0 --- /dev/null +++ b/services/people-api/tests/test_postgres_hire_row_container_runtime_integrity.py @@ -0,0 +1,231 @@ +"""Runtime-integrity contracts for durable hire row containers.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import AuthorizationDecision +from orgmetra_people_api.hire import HireAcceptanceCommand, HireDecisionIntegrityError +from orgmetra_people_api.postgres_hire import PostgresHireAcceptancePort + +TENANT = UUID("0198a412-7100-7000-8000-000000000001") +CANDIDATE = UUID("0198a412-7100-7000-8000-000000000010") +DECISION = UUID("0198a412-7100-7000-8000-000000000011") +PERSON = UUID("0198a412-7100-7000-8000-000000000020") +PERSON_NAME = UUID("0198a412-7100-7000-8000-000000000021") +EMPLOYMENT = UUID("0198a412-7100-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-7100-7000-8000-000000000031") +CONVERSION = UUID("0198a412-7100-7000-8000-000000000040") +AUDIT_EVENT = UUID("0198a412-7100-7000-8000-000000000050") +OUTBOX_DELIVERY = UUID("0198a412-7100-7000-8000-000000000051") +EVIDENCE_SET = UUID("0198a412-7100-7000-8000-000000000060") +DECIDED_AT = datetime(2026, 8, 18, 0, 0, tzinfo=timezone.utc) +TRANSACTION_AT = datetime(2026, 8, 18, 0, 1, tzinfo=timezone.utc) +ACTOR = "keyverse_subject:operator-17" +PURPOSE = "candidate_hire" +CONFIRMATION = "human_confirmation:review-88" + + +class _ExecutableBatch(list[object]): + """Fail if a fetched row collection is consumed before exact-type validation.""" + + def __bool__(self) -> bool: + """Reject pre-gate truthiness.""" + raise TypeError("row collection truthiness executed before exact-type validation") + + def __len__(self) -> int: + """Reject pre-gate length inspection.""" + raise AssertionError("row collection length executed before exact-type validation") + + def __getitem__(self, key: object) -> object: + """Reject pre-gate indexed access.""" + del key + raise IndexError("row collection indexing executed before exact-type validation") + + def __iter__(self): + """Reject pre-gate row iteration.""" + raise AssertionError("row collection iteration executed before exact-type validation") + + +class _ExecutableRow(tuple): + """Fail if a fetched fixed row is consumed before exact-type validation.""" + + def __len__(self) -> int: + """Reject pre-gate row length inspection.""" + raise AssertionError("row length executed before exact-type validation") + + def __getitem__(self, key: object) -> object: + """Reject pre-gate row indexing.""" + del key + raise IndexError("row indexing executed before exact-type validation") + + def __iter__(self): + """Reject pre-gate row iteration.""" + raise AssertionError("row iteration executed before exact-type validation") + + +class _Cursor: + """Serve configured bounded fetch batches and record executed SQL.""" + + def __init__(self, batches: list[object]) -> None: + """Store the exact fetch results in adapter execution order.""" + self._batches = list(batches) + self.executions: list[str] = [] + + def __enter__(self) -> _Cursor: + """Return the same cursor for the transaction context.""" + return self + + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: + """Leave exception handling to the connection context.""" + del exc_type, exc_value, traceback + + def execute(self, statement: str, parameters: tuple[object, ...] | None = None) -> None: + """Record SQL without evaluating durable-row contents.""" + del parameters + self.executions.append(statement) + + def fetchmany(self, size: int) -> object: + """Return the next configured batch without touching its runtime hooks.""" + assert size == 2 + return self._batches.pop(0) + + +class _Connection: + """Provide the focused cursor through a DB-API-style context boundary.""" + + def __init__(self, cursor: _Cursor) -> None: + """Retain the one cursor used by the focused durable-row tests.""" + self._cursor = cursor + + def __enter__(self) -> _Connection: + """Return the same transaction connection.""" + return self + + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: + """Leave exception propagation unchanged.""" + del exc_type, exc_value, traceback + + def cursor(self) -> _Cursor: + """Return the configured focused cursor.""" + return self._cursor + + +def _command() -> HireAcceptanceCommand: + """Build one deterministic valid confirmed-hire command.""" + return HireAcceptanceCommand( + tenant_record_id=TENANT, + candidate_profile_id=CANDIDATE, + selection_decision_id=DECISION, + person_record_id=PERSON, + person_name_record_id=PERSON_NAME, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + candidate_worker_conversion_record_id=CONVERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX_DELIVERY, + effective_from=date(2026, 8, 18), + display_name="Ada Lovelace", + idempotency_key="hire-row-container-runtime-integrity", + ) + + +def _authorization() -> AuthorizationDecision: + """Build the exact allow decision required for the confirmed-hire command.""" + return AuthorizationDecision( + allowed=True, + tenant_record_id=TENANT, + actor_reference=ACTOR, + resource_reference=f"selection_decision:{DECISION.hex}", + policy_version_code="people-hire-v1", + purpose_code=PURPOSE, + operation_code="materialize_worker", + resource_kind="selection_decision", + requested_fields=frozenset({"candidate_worker_conversion"}), + authorized_fields=frozenset({"candidate_worker_conversion"}), + reason_code="access_permitted", + next_action="continue", + ) + + +def _decision_row() -> tuple[object, ...]: + """Build one valid durable confirmed-hire provenance row.""" + return ( + ACTOR, + PURPOSE, + "hire", + CONFIRMATION, + DECIDED_AT, + EVIDENCE_SET, + TRANSACTION_AT, + ) + + +def _port(*batches: object) -> tuple[PostgresHireAcceptancePort, _Cursor]: + """Build a port whose cursor returns the supplied bounded batches.""" + cursor = _Cursor(list(batches)) + return PostgresHireAcceptancePort(lambda: _Connection(cursor)), cursor + + +def test_hire_rejects_executable_idempotency_batch_before_collection_hooks() -> None: + """Replay lookup must reject a batch subtype before truthiness or iteration.""" + port, cursor = _port(_ExecutableBatch()) + + with pytest.raises(HireDecisionIntegrityError, match="hire idempotency row is invalid"): + port.accept_hire(command=_command(), authorization=_authorization()) + + assert not any("selection_decision AS decision" in statement for statement in cursor.executions) + + +def test_hire_rejects_executable_idempotency_row_before_row_hooks() -> None: + """Replay lookup must reject a row subtype before length or unpacking.""" + port, cursor = _port([_ExecutableRow((CONVERSION, "digest"))]) + + with pytest.raises(HireDecisionIntegrityError, match="hire idempotency row is invalid"): + port.accept_hire(command=_command(), authorization=_authorization()) + + assert not any("selection_decision AS decision" in statement for statement in cursor.executions) + + +def test_hire_rejects_executable_provenance_batch_before_collection_hooks() -> None: + """Decision lookup must reject a batch subtype before truthiness or iteration.""" + port, cursor = _port([], _ExecutableBatch([_decision_row()])) + + with pytest.raises(HireDecisionIntegrityError, match="decision provenance row has an invalid shape"): + port.accept_hire(command=_command(), authorization=_authorization()) + + assert not any("INSERT INTO public.person_record" in statement for statement in cursor.executions) + + +def test_hire_rejects_executable_provenance_row_before_row_hooks() -> None: + """Decision lookup must reject a row subtype before length or unpacking.""" + port, cursor = _port([], [_ExecutableRow(_decision_row())]) + + with pytest.raises(HireDecisionIntegrityError, match="decision provenance row has an invalid shape"): + port.accept_hire(command=_command(), authorization=_authorization()) + + assert not any("INSERT INTO public.person_record" in statement for statement in cursor.executions) + + +def test_hire_rejects_wrong_width_exact_rows_at_the_container_boundary() -> None: + """Exact built-in rows still require their fixed SQL projection width.""" + replay_port, _ = _port([(CONVERSION,)]) + with pytest.raises(HireDecisionIntegrityError, match="hire idempotency row is invalid"): + replay_port.accept_hire(command=_command(), authorization=_authorization()) + + provenance_port, _ = _port([], [(_decision_row()[0],)]) + with pytest.raises(HireDecisionIntegrityError, match="decision provenance row has an invalid shape"): + provenance_port.accept_hire(command=_command(), authorization=_authorization()) + + +def test_hire_accepts_exact_builtin_batches_and_rows() -> None: + """Default Psycopg-compatible list batches and tuple rows remain accepted.""" + port, cursor = _port([], [_decision_row()]) + + result = port.accept_hire(command=_command(), authorization=_authorization()) + + assert result.candidate_worker_conversion_record_id == CONVERSION + assert any("INSERT INTO public.person_record" in statement for statement in cursor.executions) diff --git a/services/people-api/tests/test_postgres_hire_timestamp_runtime_integrity.py b/services/people-api/tests/test_postgres_hire_timestamp_runtime_integrity.py new file mode 100644 index 000000000..15f62ff86 --- /dev/null +++ b/services/people-api/tests/test_postgres_hire_timestamp_runtime_integrity.py @@ -0,0 +1,53 @@ +"""Runtime-integrity contracts for durable hire-decision timestamps.""" + +from datetime import datetime, timedelta, timezone, tzinfo +from zoneinfo import ZoneInfo + +from orgmetra_people_api.postgres_hire import _is_aware_datetime + + +class _ExecutableTimezone(tzinfo): + """Record forbidden offset resolution at the People durable boundary.""" + + def __init__(self) -> None: + """Initialize the callback counter without resolving an offset.""" + self.calls = 0 + + def utcoffset(self, dt: datetime | None) -> timedelta: + """Fail if validation executes caller-defined timezone behavior.""" + del dt + self.calls += 1 + raise AssertionError("caller-defined timezone callback executed") + + +class _ExecutableDatetime(datetime): + """Fail if validation executes behavior from a datetime subtype.""" + + def utcoffset(self) -> timedelta: + """Expose subtype execution if the exact-type gate is missing.""" + raise AssertionError("datetime subtype callback executed") + + +def test_hire_timestamp_rejects_custom_timezone_before_callback() -> None: + """Exact datetime values cannot delegate offset validation to caller code.""" + provider = _ExecutableTimezone() + value = datetime(2026, 8, 18, 0, 0, tzinfo=provider) + + assert _is_aware_datetime(value) is False + assert provider.calls == 0 + + +def test_hire_timestamp_rejects_datetime_subtype_before_callback() -> None: + """Executable datetime subtypes are not durable selection-decision evidence.""" + value = _ExecutableDatetime(2026, 8, 18, 0, 0, tzinfo=timezone.utc) + + assert _is_aware_datetime(value) is False + + +def test_hire_timestamp_accepts_exact_standard_library_timezones() -> None: + """Psycopg-compatible standard-library timezone materialization stays valid.""" + utc_value = datetime(2026, 8, 18, 0, 0, tzinfo=timezone.utc) + seoul_value = datetime(2026, 8, 18, 9, 0, tzinfo=ZoneInfo("Asia/Seoul")) + + assert _is_aware_datetime(utc_value) is True + assert _is_aware_datetime(seoul_value) is True diff --git a/services/people-api/tests/test_postgres_hire_uuid_runtime_integrity.py b/services/people-api/tests/test_postgres_hire_uuid_runtime_integrity.py new file mode 100644 index 000000000..be18c536c --- /dev/null +++ b/services/people-api/tests/test_postgres_hire_uuid_runtime_integrity.py @@ -0,0 +1,32 @@ +"""Runtime-integrity contracts for durable hire UUID evidence.""" + +from uuid import UUID + +from orgmetra_people_api.postgres_hire import _is_operational_uuid + + +_MAX_UUID_INT = (1 << 128) - 1 + + +class _ExecutableUUID(UUID): + """Expose any UUID attribute inspection performed before an exact-type gate.""" + + def __getattribute__(self, name: str) -> object: + """Fail when untrusted UUID evidence is inspected as if it were inert.""" + if name == "int": + raise AttributeError("UUID subtype behavior executed before exact-type validation") + return super().__getattribute__(name) + + +def test_hire_durable_uuid_rejects_subtype_before_identity_inspection() -> None: + """Database-returned UUID subtypes must fail without executing subtype behavior.""" + value = _ExecutableUUID("0198a412-7100-7000-8000-000000000060") + + assert _is_operational_uuid(value) is False + + +def test_hire_durable_uuid_accepts_only_operational_exact_uuid_values() -> None: + """Exact Psycopg-compatible UUID values remain valid except reserved sentinels.""" + assert _is_operational_uuid(UUID("0198a412-7100-7000-8000-000000000060")) is True + assert _is_operational_uuid(UUID(int=0)) is False + assert _is_operational_uuid(UUID(int=_MAX_UUID_INT)) is False diff --git a/services/people-api/tests/test_postgres_mutation_authorization_runtime_integrity.py b/services/people-api/tests/test_postgres_mutation_authorization_runtime_integrity.py new file mode 100644 index 000000000..0273614cb --- /dev/null +++ b/services/people-api/tests/test_postgres_mutation_authorization_runtime_integrity.py @@ -0,0 +1,48 @@ +"""Adversarial runtime-integrity contract for PostgreSQL People mutation authorization.""" + +from __future__ import annotations + +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import AuthorizationDecision +from orgmetra_people_api.postgres_mutations import ( + PeopleMutationIntegrityError, + _require_authorization, +) + +TENANT = UUID("0198a412-7100-7000-8000-000000000001") +RESOURCE = "employment_record:0198a412710070008000000000000030" +FIELDS = frozenset({"employment_record"}) + + +class ForgedAuthorizationDecision(AuthorizationDecision): + """Represent a validation-bypassing caller-defined authorization subtype.""" + + +def test_postgres_people_mutation_rejects_authorization_subclass() -> None: + """Require persistence authorization to use the exact governed decision runtime type.""" + forged = ForgedAuthorizationDecision( + allowed=True, + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-17", + resource_reference=RESOURCE, + policy_version_code="people-employment-v1", + purpose_code="workforce_admin", + operation_code="create_record", + resource_kind="employment_record", + requested_fields=FIELDS, + authorized_fields=FIELDS, + reason_code="access_permitted", + next_action="continue", + ) + + with pytest.raises(PeopleMutationIntegrityError, match="typed authorization decision"): + _require_authorization( + authorization=forged, + tenant_record_id=TENANT, + resource_reference=RESOURCE, + resource_kind="employment_record", + requested_fields=FIELDS, + ) diff --git a/services/people-api/tests/test_postgres_mutation_command_runtime_integrity.py b/services/people-api/tests/test_postgres_mutation_command_runtime_integrity.py new file mode 100644 index 000000000..9ba409249 --- /dev/null +++ b/services/people-api/tests/test_postgres_mutation_command_runtime_integrity.py @@ -0,0 +1,140 @@ +"""Adversarial runtime-integrity contracts for PostgreSQL People mutation commands.""" + +from __future__ import annotations + +from datetime import date +from decimal import Decimal +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import AuthorizationDecision +from orgmetra_people_api.mutations import ( + AssignmentMutationCommand, + EmploymentMutationCommand, + PositionMutationCommand, +) +from orgmetra_people_api.postgres_mutations import PostgresPeopleMutationPort + +TENANT = UUID("0198a412-7100-7000-8000-000000000001") +PERSON = UUID("0198a412-7100-7000-8000-000000000020") +EMPLOYMENT = UUID("0198a412-7100-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-7100-7000-8000-000000000031") +ORGANIZATION = UUID("0198a412-7100-7000-8000-000000000040") +JOB = UUID("0198a412-7100-7000-8000-000000000041") +POSITION = UUID("0198a412-7100-7000-8000-000000000050") +POSITION_VERSION = UUID("0198a412-7100-7000-8000-000000000051") +ASSIGNMENT = UUID("0198a412-7100-7000-8000-000000000060") +AUDIT = UUID("0198a412-7100-7000-8000-000000000070") +OUTBOX = UUID("0198a412-7100-7000-8000-000000000071") + + +class ForgedEmploymentMutationCommand(EmploymentMutationCommand): + """Represent a validation-bypassing caller-defined employment command subtype.""" + + +class ForgedPositionMutationCommand(PositionMutationCommand): + """Represent a validation-bypassing caller-defined position command subtype.""" + + +class ForgedAssignmentMutationCommand(AssignmentMutationCommand): + """Represent a validation-bypassing caller-defined assignment command subtype.""" + + +def _authorization(resource_kind: str, record_id: UUID) -> AuthorizationDecision: + """Build one exact-scope allow decision for a People mutation target.""" + fields = frozenset({resource_kind}) + return AuthorizationDecision( + allowed=True, + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-17", + resource_reference=f"{resource_kind}:{record_id.hex}", + policy_version_code="people-mutation-v1", + purpose_code="workforce_admin", + operation_code="create_record", + resource_kind=resource_kind, + requested_fields=fields, + authorized_fields=fields, + reason_code="access_permitted", + next_action="continue", + ) + + +def _forbidden_connection_factory() -> object: + """Fail if a forged command crosses the persistence authority into database work.""" + raise AssertionError("database work must not begin for a forged People mutation command") + + +def test_postgres_employment_port_rejects_command_subclass_before_database_work() -> None: + """Require the employment persistence authority to accept only its exact command type.""" + command = ForgedEmploymentMutationCommand( + tenant_record_id=TENANT, + person_record_id=PERSON, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + audit_event_record_id=AUDIT, + outbox_delivery_record_id=OUTBOX, + employment_status_code="active", + employment_concurrency_code="exclusive", + effective_from=date(2026, 8, 22), + confirmation_reference="human_confirmation:employment-1", + evidence_version_code="employment-evidence-v1", + idempotency_key="employment-runtime-guard", + ) + port = PostgresPeopleMutationPort(_forbidden_connection_factory) + + with pytest.raises(TypeError, match="command must be an EmploymentMutationCommand"): + port.create_employment( + command=command, + authorization=_authorization("employment_record", EMPLOYMENT), + ) + + +def test_postgres_position_port_rejects_command_subclass_before_database_work() -> None: + """Require the position persistence authority to accept only its exact command type.""" + command = ForgedPositionMutationCommand( + tenant_record_id=TENANT, + organization_unit_id=ORGANIZATION, + job_profile_id=JOB, + position_record_id=POSITION, + position_record_version_id=POSITION_VERSION, + audit_event_record_id=AUDIT, + outbox_delivery_record_id=OUTBOX, + position_status_code="open", + effective_from=date(2026, 8, 22), + confirmation_reference="human_confirmation:position-1", + evidence_version_code="position-evidence-v1", + idempotency_key="position-runtime-guard", + ) + port = PostgresPeopleMutationPort(_forbidden_connection_factory) + + with pytest.raises(TypeError, match="command must be a PositionMutationCommand"): + port.create_position( + command=command, + authorization=_authorization("position_record", POSITION), + ) + + +def test_postgres_assignment_port_rejects_command_subclass_before_database_work() -> None: + """Require the assignment persistence authority to accept only its exact command type.""" + command = ForgedAssignmentMutationCommand( + tenant_record_id=TENANT, + employment_record_id=EMPLOYMENT, + person_record_id=PERSON, + position_record_id=POSITION, + assignment_record_id=ASSIGNMENT, + audit_event_record_id=AUDIT, + outbox_delivery_record_id=OUTBOX, + allocation_ratio=Decimal("1.0000"), + effective_from=date(2026, 8, 22), + confirmation_reference="human_confirmation:assignment-1", + evidence_version_code="assignment-evidence-v1", + idempotency_key="assignment-runtime-guard", + ) + port = PostgresPeopleMutationPort(_forbidden_connection_factory) + + with pytest.raises(TypeError, match="command must be an AssignmentMutationCommand"): + port.create_assignment( + command=command, + authorization=_authorization("assignment_record", ASSIGNMENT), + ) diff --git a/services/people-api/tests/test_postgres_mutation_post_construction_integrity.py b/services/people-api/tests/test_postgres_mutation_post_construction_integrity.py new file mode 100644 index 000000000..4e6858494 --- /dev/null +++ b/services/people-api/tests/test_postgres_mutation_post_construction_integrity.py @@ -0,0 +1,199 @@ +"""Post-construction command-integrity regressions for the PostgreSQL People port.""" + +from __future__ import annotations + +from datetime import date +from decimal import Decimal +from uuid import UUID + +import pytest + +from orgmetra_keyverse_adapter import AuthorizationDecision +from orgmetra_people_api.mutations import ( + AssignmentMutationCommand, + EmploymentMutationCommand, + PositionMutationCommand, +) +from orgmetra_people_api.postgres_mutations import PostgresPeopleMutationPort +from test_postgres_people_mutations import FakeConnection, RECORDED_AT, ScriptedCursor + +TENANT = UUID("0198a412-a700-7000-8000-000000000001") +PERSON = UUID("0198a412-a700-7000-8000-000000000020") +EMPLOYMENT = UUID("0198a412-a700-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-a700-7000-8000-000000000031") +ORGANIZATION = UUID("0198a412-a700-7000-8000-000000000040") +MUTATED_ORGANIZATION = UUID("0198a412-a700-7000-8000-000000000041") +JOB_PROFILE = UUID("0198a412-a700-7000-8000-000000000050") +POSITION = UUID("0198a412-a700-7000-8000-000000000060") +POSITION_VERSION = UUID("0198a412-a700-7000-8000-000000000061") +ASSIGNMENT = UUID("0198a412-a700-7000-8000-000000000070") +AUDIT_EVENT = UUID("0198a412-a700-7000-8000-000000000080") +OUTBOX = UUID("0198a412-a700-7000-8000-000000000081") + + +class _ExecutableUUID(UUID): + """Trip if the PostgreSQL authority renders rewritten identity before validation.""" + + @property + def hex(self) -> str: + """Fail if authorization-reference rendering runs before command validation.""" + raise AssertionError("rewritten UUID hex behavior must not execute") + + +def _authorization(*, resource_kind: str, record_id: UUID) -> AuthorizationDecision: + """Build one exact authorization decision for an original mutation identity.""" + fields = frozenset({resource_kind}) + return AuthorizationDecision( + allowed=True, + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-227", + resource_reference=f"{resource_kind}:{record_id.hex}", + policy_version_code="people-mutation-v1", + purpose_code="workforce_admin", + operation_code="create_record", + resource_kind=resource_kind, + requested_fields=fields, + authorized_fields=fields, + reason_code="access_permitted", + next_action="continue", + ) + + +def _forbidden_connection_factory() -> object: + """Fail if rewritten command evidence reaches PostgreSQL transaction work.""" + raise AssertionError("database work must not begin for a rewritten mutation command") + + +def test_postgres_employment_revalidates_exact_command_after_object_setattr_rewrite() -> None: + """Reject rewritten Employment identity before callback or database work.""" + command = EmploymentMutationCommand( + tenant_record_id=TENANT, + person_record_id=PERSON, + employment_record_id=EMPLOYMENT, + employment_record_version_id=EMPLOYMENT_VERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + employment_status_code="active", + employment_concurrency_code="exclusive", + effective_from=date(2026, 9, 5), + confirmation_reference="human_confirmation:post-construction-227-employment", + evidence_version_code="employment-evidence-v1", + idempotency_key="post-construction-runtime-227-employment", + ) + object.__setattr__( + command, + "employment_record_id", + _ExecutableUUID("0198a412-a700-7000-8000-000000000099"), + ) + port = PostgresPeopleMutationPort(_forbidden_connection_factory) + + with pytest.raises(ValueError, match="employment_record_id must be an operational UUID"): + port.create_employment( + command=command, + authorization=_authorization(resource_kind="employment_record", record_id=EMPLOYMENT), + ) + + +def test_postgres_position_revalidates_exact_command_after_object_setattr_rewrite() -> None: + """Reject rewritten Position identity before callback or database work.""" + command = PositionMutationCommand( + tenant_record_id=TENANT, + organization_unit_id=ORGANIZATION, + job_profile_id=JOB_PROFILE, + position_record_id=POSITION, + position_record_version_id=POSITION_VERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + position_status_code="open", + effective_from=date(2026, 9, 5), + confirmation_reference="human_confirmation:post-construction-227-position", + evidence_version_code="position-evidence-v1", + idempotency_key="post-construction-runtime-227-position", + ) + object.__setattr__( + command, + "position_record_id", + _ExecutableUUID("0198a412-a700-7000-8000-000000000098"), + ) + port = PostgresPeopleMutationPort(_forbidden_connection_factory) + + with pytest.raises(ValueError, match="position_record_id must be an operational UUID"): + port.create_position( + command=command, + authorization=_authorization(resource_kind="position_record", record_id=POSITION), + ) + + +def test_postgres_assignment_revalidates_exact_command_after_object_setattr_rewrite() -> None: + """Reject rewritten Assignment identity before callback or database work.""" + command = AssignmentMutationCommand( + tenant_record_id=TENANT, + employment_record_id=EMPLOYMENT, + person_record_id=PERSON, + position_record_id=POSITION, + assignment_record_id=ASSIGNMENT, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + allocation_ratio=Decimal("1.0000"), + effective_from=date(2026, 9, 5), + confirmation_reference="human_confirmation:post-construction-227-assignment", + evidence_version_code="assignment-evidence-v1", + idempotency_key="post-construction-runtime-227-assignment", + ) + object.__setattr__( + command, + "assignment_record_id", + _ExecutableUUID("0198a412-a700-7000-8000-000000000097"), + ) + port = PostgresPeopleMutationPort(_forbidden_connection_factory) + + with pytest.raises(ValueError, match="assignment_record_id must be an operational UUID"): + port.create_assignment( + command=command, + authorization=_authorization(resource_kind="assignment_record", record_id=ASSIGNMENT), + ) + + +def test_postgres_position_detaches_validated_command_before_connection_factory_callback() -> None: + """Keep one validated Position snapshot across the caller-owned connection callback.""" + command = PositionMutationCommand( + tenant_record_id=TENANT, + organization_unit_id=ORGANIZATION, + job_profile_id=JOB_PROFILE, + position_record_id=POSITION, + position_record_version_id=POSITION_VERSION, + audit_event_record_id=AUDIT_EVENT, + outbox_delivery_record_id=OUTBOX, + position_status_code="open", + effective_from=date(2026, 9, 5), + confirmation_reference="human_confirmation:post-construction-229-position", + evidence_version_code="position-evidence-v1", + idempotency_key="post-construction-runtime-229-position", + ) + cursor = ScriptedCursor( + [[], [(ORGANIZATION, JOB_PROFILE, RECORDED_AT)]], + [], + ) + connection = FakeConnection(cursor) + + def mutating_connection_factory() -> FakeConnection: + """Rewrite the caller's still-valid command only after authorization has completed.""" + object.__setattr__(command, "organization_unit_id", MUTATED_ORGANIZATION) + return connection + + port = PostgresPeopleMutationPort(mutating_connection_factory) + result = port.create_position( + command=command, + authorization=_authorization(resource_kind="position_record", record_id=POSITION), + ) + + assert result.position_record_id == POSITION + parent_query = next( + execution for execution in cursor.executions if "FROM public.organization_unit AS organization" in execution[0] + ) + assert parent_query[1] == (JOB_PROFILE, TENANT, ORGANIZATION) + insert_position = next( + execution for execution in cursor.executions if execution[0].startswith("INSERT INTO public.position_record (") + ) + assert insert_position[1] is not None + assert insert_position[1][2] == ORGANIZATION diff --git a/services/people-api/tests/test_postgres_mutation_projection_shape_coverage.py b/services/people-api/tests/test_postgres_mutation_projection_shape_coverage.py new file mode 100644 index 000000000..c8e9e49a2 --- /dev/null +++ b/services/people-api/tests/test_postgres_mutation_projection_shape_coverage.py @@ -0,0 +1,27 @@ +"""Hosted-coverage regressions for fixed People PostgreSQL projection widths.""" + +from __future__ import annotations + +import pytest + +import orgmetra_people_api.postgres_mutations as postgres_mutations +from orgmetra_people_api.mutations import PeopleMutationIntegrityError + + +@pytest.mark.parametrize( + ("helper_name", "error_message"), + [ + ("_employment_version_from_row", "employment version row has an invalid shape"), + ("_position_version_from_row", "position version row has an invalid shape"), + ("_assignment_from_row", "assignment row has an invalid shape"), + ], +) +def test_fixed_projection_helpers_reject_wrong_width( + helper_name: str, + error_message: str, +) -> None: + """Cover each fail-closed width guard reported missing by exact-head Foundation CI.""" + helper = getattr(postgres_mutations, helper_name) + + with pytest.raises(PeopleMutationIntegrityError, match=error_message): + helper(postgres_mutations.UUID("10000000-0000-7000-8000-000000000001"), ()) diff --git a/services/people-api/tests/test_postgres_mutation_row_container_integrity.py b/services/people-api/tests/test_postgres_mutation_row_container_integrity.py new file mode 100644 index 000000000..11545ca7c --- /dev/null +++ b/services/people-api/tests/test_postgres_mutation_row_container_integrity.py @@ -0,0 +1,104 @@ +"""Executable-container regressions for generic People PostgreSQL projections.""" + +from __future__ import annotations + +import pytest + +import orgmetra_people_api.postgres_mutations as postgres_mutations +from orgmetra_people_api.mutations import PeopleMutationIntegrityError + + +class _ExecutableRows(list[object]): + """Tripwire outer row collection that must be rejected before container hooks.""" + + calls = 0 + + def __bool__(self) -> bool: + """Fail if durable validation asks this untrusted collection for truthiness.""" + type(self).calls += 1 + raise TypeError("outer durable row collection executed __bool__") + + def __len__(self) -> int: + """Fail if durable validation asks this untrusted collection for cardinality.""" + type(self).calls += 1 + raise AssertionError("outer durable row collection executed __len__") + + def __getitem__(self, index: object) -> object: + """Fail if durable validation indexes this untrusted collection.""" + type(self).calls += 1 + raise IndexError("outer durable row collection executed __getitem__") + + def __iter__(self): + """Fail if durable validation iterates this untrusted collection.""" + type(self).calls += 1 + raise AssertionError("outer durable row collection executed __iter__") + + +class _ExecutableRow(tuple[object, ...]): + """Tripwire fixed row that must be rejected before row hooks.""" + + calls = 0 + + def __len__(self) -> int: + """Fail if durable validation asks this untrusted row for width.""" + type(self).calls += 1 + raise AssertionError("durable row executed __len__") + + def __iter__(self): + """Fail if durable validation iterates this untrusted row.""" + type(self).calls += 1 + raise AssertionError("durable row executed __iter__") + + +@pytest.fixture(autouse=True) +def _reset_tripwires() -> None: + """Reset shared counters so each rejection proves zero callback execution.""" + _ExecutableRows.calls = 0 + _ExecutableRow.calls = 0 + + +def _unpack(value: object, *, row_width: int) -> tuple[tuple[object, ...], ...]: + """Resolve the production boundary explicitly so predecessor absence is RED.""" + unpack = getattr(postgres_mutations, "_unpack_fixed_rows", None) + assert unpack is not None, "generic People PostgreSQL adapter lacks a fixed-row trust boundary" + return unpack(value, row_width=row_width, error_message="durable projection is invalid") + + +def test_fixed_rows_reject_executable_outer_collection_before_hooks() -> None: + """Reject a list subtype before truthiness, length, indexing, or iteration executes.""" + rows = _ExecutableRows([(UUID_SENTINEL, "digest")]) + + with pytest.raises(PeopleMutationIntegrityError, match="durable projection is invalid"): + _unpack(rows, row_width=2) + + assert _ExecutableRows.calls == 0 + + +def test_fixed_rows_reject_executable_row_before_hooks() -> None: + """Reject a tuple subtype before width or iteration executes.""" + row = _ExecutableRow((UUID_SENTINEL, "digest")) + + with pytest.raises(PeopleMutationIntegrityError, match="durable projection is invalid"): + _unpack([row], row_width=2) + + assert _ExecutableRow.calls == 0 + + +def test_fixed_rows_reject_wrong_width_exact_row() -> None: + """Reject an inert built-in row whose SQL projection width is impossible.""" + with pytest.raises(PeopleMutationIntegrityError, match="durable projection is invalid"): + _unpack([(1, 2, 3)], row_width=2) + + +def test_fixed_rows_detach_exact_builtin_batches_and_rows() -> None: + """Accept exact built-in containers and return one inert tuple-of-tuples copy.""" + source = [[1, "a"], (2, "b")] + + detached = _unpack(source, row_width=2) + + assert detached == ((1, "a"), (2, "b")) + assert type(detached) is tuple + assert all(type(row) is tuple for row in detached) + + +UUID_SENTINEL = object() diff --git a/services/people-api/tests/test_postgres_mutation_scalar_runtime_integrity.py b/services/people-api/tests/test_postgres_mutation_scalar_runtime_integrity.py new file mode 100644 index 000000000..5de9cbb1d --- /dev/null +++ b/services/people-api/tests/test_postgres_mutation_scalar_runtime_integrity.py @@ -0,0 +1,307 @@ +"""Runtime-integrity contracts for generic People durable scalar evidence.""" + +from datetime import date, datetime, timedelta, timezone, tzinfo +from decimal import Decimal +from typing import Any +from uuid import UUID +from zoneinfo import ZoneInfo + +from orgmetra_people_api.mutations import PeopleMutationIntegrityError +from orgmetra_people_api.postgres_mutations import ( + _assignment_from_row, + _employment_version_from_row, + _is_aware_datetime, + _is_operational_uuid, + _position_version_from_row, + _replayed_record_id, +) +from test_people_mutations import employment_command +from test_postgres_people_mutations import employment_authorization + + +_MAX_UUID_INT = (1 << 128) - 1 + + +class _ExecutableUUID(UUID): + """Expose UUID attribute inspection performed before an exact-type gate.""" + + def __getattribute__(self, name: str) -> object: + """Fail when untrusted UUID evidence is inspected as if it were inert.""" + if name == "int": + raise AttributeError("UUID subtype behavior executed before exact-type validation") + return super().__getattribute__(name) + + +class _ExecutableTimezone(tzinfo): + """Record forbidden offset resolution at the generic People durable boundary.""" + + def __init__(self) -> None: + """Initialize the callback counter without resolving an offset.""" + self.calls = 0 + + def utcoffset(self, dt: datetime | None) -> timedelta: + """Fail if validation executes caller-defined timezone behavior.""" + del dt + self.calls += 1 + raise AssertionError("caller-defined timezone callback executed") + + +class _ExecutableDatetime(datetime): + """Fail if validation executes behavior from a datetime subtype.""" + + def utcoffset(self) -> timedelta: + """Expose subtype execution if the exact-type gate is missing.""" + raise AssertionError("datetime subtype callback executed") + + +class _ExecutableDigest(str): + """Expose digest comparison performed before an exact-type gate.""" + + def __new__(cls, value: str) -> _ExecutableDigest: + """Create one tripwire text value without invoking comparison behavior.""" + instance = super().__new__(cls, value) + instance.calls = 0 + return instance + + def __eq__(self, other: object) -> bool: + """Fail if durable replay validation executes subtype equality.""" + del other + self.calls += 1 + raise AssertionError("digest subtype equality executed before exact-type validation") + + def __ne__(self, other: object) -> bool: + """Fail if durable replay validation executes subtype inequality.""" + del other + self.calls += 1 + raise AssertionError("digest subtype inequality executed before exact-type validation") + + +class _ExecutableStatusText(str): + """Expose persisted status-code behavior before an exact durable type gate.""" + + def __new__(cls, value: str) -> _ExecutableStatusText: + """Create one status-code tripwire without invoking comparison behavior.""" + instance = super().__new__(cls, value) + instance.calls = 0 + return instance + + __hash__ = None + + def __eq__(self, other: object) -> bool: + """Fail if HRIS validation compares persisted subtype text.""" + del other + self.calls += 1 + raise AssertionError("status subtype equality executed before exact-type validation") + + def __ne__(self, other: object) -> bool: + """Fail if HRIS validation compares persisted subtype text for inequality.""" + del other + self.calls += 1 + raise AssertionError("status subtype inequality executed before exact-type validation") + + +class _ExecutableDecimal(Decimal): + """Expose persisted allocation-ratio behavior before an exact durable type gate.""" + + def __new__(cls, value: str) -> _ExecutableDecimal: + """Create one Decimal tripwire without performing portfolio arithmetic.""" + instance = super().__new__(cls, value) + instance.calls = 0 + return instance + + def __gt__(self, other: object) -> bool: + """Fail if FTE validation compares persisted subtype allocation.""" + del other + self.calls += 1 + raise TypeError("Decimal subtype comparison executed before exact-type validation") + + def __le__(self, other: object) -> bool: + """Fail if FTE validation compares persisted subtype allocation.""" + del other + self.calls += 1 + raise TypeError("Decimal subtype comparison executed before exact-type validation") + + def __add__(self, other: object) -> Decimal: + """Fail if portfolio aggregation adds persisted subtype allocation.""" + del other + self.calls += 1 + raise TypeError("Decimal subtype addition executed before exact-type validation") + + def __radd__(self, other: object) -> Decimal: + """Fail if portfolio aggregation reverse-adds persisted subtype allocation.""" + del other + self.calls += 1 + raise TypeError("Decimal subtype reverse addition executed before exact-type validation") + + +class _ReplayCursor: + """Return one scripted exact built-in idempotency row.""" + + def __init__(self, row: tuple[object, object]) -> None: + """Store the row without inspecting its scalar values.""" + self.row = row + + def execute(self, sql: str, parameters: tuple[object, ...] | None = None) -> None: + """Accept the two replay lookup statements without side effects.""" + del sql, parameters + + def fetchmany(self, size: int) -> list[tuple[object, object]]: + """Return the scripted exact row within the requested bound.""" + return [self.row][:size] + + +def test_generic_durable_uuid_rejects_subtype_before_identity_inspection() -> None: + """DB-returned UUID subtypes fail without executing subtype behavior.""" + value = _ExecutableUUID("0198a412-7100-7000-8000-000000000061") + + assert _is_operational_uuid(value) is False + + +def test_generic_durable_uuid_accepts_only_operational_exact_uuid_values() -> None: + """Exact Psycopg-compatible UUIDs remain valid except reserved sentinels.""" + assert _is_operational_uuid(UUID("0198a412-7100-7000-8000-000000000061")) is True + assert _is_operational_uuid(UUID(int=0)) is False + assert _is_operational_uuid(UUID(int=_MAX_UUID_INT)) is False + + +def test_generic_timestamp_rejects_custom_timezone_before_callback() -> None: + """Exact datetime values cannot delegate offset validation to caller code.""" + provider = _ExecutableTimezone() + value = datetime(2026, 9, 5, 0, 0, tzinfo=provider) + + assert _is_aware_datetime(value) is False + assert provider.calls == 0 + + +def test_generic_timestamp_rejects_datetime_subtype_before_callback() -> None: + """Executable datetime subtypes are not durable generic People evidence.""" + value = _ExecutableDatetime(2026, 9, 5, 0, 0, tzinfo=timezone.utc) + + assert _is_aware_datetime(value) is False + + +def test_generic_timestamp_accepts_exact_standard_library_timezones() -> None: + """Psycopg-compatible standard-library timezone materialization stays valid.""" + utc_value = datetime(2026, 9, 5, 0, 0, tzinfo=timezone.utc) + seoul_value = datetime(2026, 9, 5, 9, 0, tzinfo=ZoneInfo("Asia/Seoul")) + + assert _is_aware_datetime(utc_value) is True + assert _is_aware_datetime(seoul_value) is True + + +def test_generic_replay_digest_rejects_subtype_before_comparison() -> None: + """Persisted digest subtypes fail without executing equality behavior.""" + command = employment_command() + digest = _ExecutableDigest("persisted-untrusted-digest") + cursor: Any = _ReplayCursor((command.employment_record_id, digest)) + + try: + _replayed_record_id( + cursor, + command=command, + authorization=employment_authorization(), + ) + except PeopleMutationIntegrityError as error: + assert str(error) == "idempotency row is invalid" + else: + raise AssertionError("digest subtype was not rejected") + + assert digest.calls == 0 + + +def test_employment_projection_rejects_status_subtype_before_kernel_behavior() -> None: + """Persisted Employment status text must be inert before HRIS fact construction.""" + status = _ExecutableStatusText("active") + row = ( + UUID("0198a412-7100-7000-8000-000000000071"), + UUID("0198a412-7100-7000-8000-000000000072"), + UUID("0198a412-7100-7000-8000-000000000073"), + status, + "exclusive", + date(2026, 9, 5), + None, + datetime(2026, 9, 5, 0, 0, tzinfo=timezone.utc), + None, + ) + + try: + _employment_version_from_row(UUID("0198a412-7100-7000-8000-000000000070"), row) + except PeopleMutationIntegrityError as error: + assert str(error) == "employment version row is invalid" + else: + raise AssertionError("employment status subtype was not rejected") + + assert status.calls == 0 + + +def test_employment_projection_rejects_concurrency_subtype_before_kernel_behavior() -> None: + """Persisted concurrency text must be inert before exclusivity validation can hash it.""" + concurrency = _ExecutableStatusText("exclusive") + row = ( + UUID("0198a412-7100-7000-8000-000000000081"), + UUID("0198a412-7100-7000-8000-000000000082"), + UUID("0198a412-7100-7000-8000-000000000083"), + "active", + concurrency, + date(2026, 9, 5), + None, + datetime(2026, 9, 5, 0, 0, tzinfo=timezone.utc), + None, + ) + + try: + _employment_version_from_row(UUID("0198a412-7100-7000-8000-000000000080"), row) + except PeopleMutationIntegrityError as error: + assert str(error) == "employment version row is invalid" + else: + raise AssertionError("employment concurrency subtype was not rejected") + + assert concurrency.calls == 0 + + +def test_position_projection_rejects_status_subtype_before_kernel_behavior() -> None: + """Persisted Position status text must be exact before assignment validation can compare it.""" + status = _ExecutableStatusText("active") + row = ( + UUID("0198a412-7100-7000-8000-000000000091"), + UUID("0198a412-7100-7000-8000-000000000092"), + status, + date(2026, 9, 5), + None, + datetime(2026, 9, 5, 0, 0, tzinfo=timezone.utc), + None, + ) + + try: + _position_version_from_row(UUID("0198a412-7100-7000-8000-000000000090"), row) + except PeopleMutationIntegrityError as error: + assert str(error) == "position version row is invalid" + else: + raise AssertionError("position status subtype was not rejected") + + assert status.calls == 0 + + +def test_assignment_projection_rejects_decimal_subtype_before_fte_math() -> None: + """Persisted allocation Decimal must be exact before portfolio comparison or summation.""" + allocation = _ExecutableDecimal("0.5000") + row = ( + UUID("0198a412-7100-7000-8000-0000000000a1"), + UUID("0198a412-7100-7000-8000-0000000000a2"), + UUID("0198a412-7100-7000-8000-0000000000a3"), + UUID("0198a412-7100-7000-8000-0000000000a4"), + allocation, + date(2026, 9, 5), + None, + datetime(2026, 9, 5, 0, 0, tzinfo=timezone.utc), + None, + ) + + try: + _assignment_from_row(UUID("0198a412-7100-7000-8000-0000000000a0"), row) + except PeopleMutationIntegrityError as error: + assert str(error) == "assignment row is invalid" + else: + raise AssertionError("assignment allocation Decimal subtype was not rejected") + + assert allocation.calls == 0 diff --git a/services/people-api/tests/test_postgres_position_parent_uuid_runtime_integrity.py b/services/people-api/tests/test_postgres_position_parent_uuid_runtime_integrity.py new file mode 100644 index 000000000..96a9353c1 --- /dev/null +++ b/services/people-api/tests/test_postgres_position_parent_uuid_runtime_integrity.py @@ -0,0 +1,48 @@ +"""Runtime-integrity contract for durable Position parent identities.""" + +from uuid import UUID + +from orgmetra_people_api.mutations import PeopleMutationIntegrityError +from orgmetra_people_api.postgres_mutations import PostgresPeopleMutationPort +from test_people_mutations import JOB, ORGANIZATION, position_command +from test_postgres_people_mutations import ( + FakeConnection, + RECORDED_AT, + ScriptedCursor, + position_authorization, +) + + +class _ExecutableComparableUUID(UUID): + """Expose parent-identity comparison performed before an exact-type gate.""" + + def __eq__(self, other: object) -> bool: + """Fail if durable parent validation executes subtype equality.""" + del other + raise AssertionError("UUID subtype equality executed before exact-type validation") + + def __ne__(self, other: object) -> bool: + """Fail if durable parent validation executes subtype inequality.""" + del other + raise AssertionError("UUID subtype inequality executed before exact-type validation") + + +def test_position_parent_uuid_subtypes_reject_before_identity_comparison() -> None: + """Organization and Job UUID subtypes fail before their comparison hooks execute.""" + for parent_index, expected_parent in enumerate((ORGANIZATION, JOB)): + parent_uuid = _ExecutableComparableUUID(str(expected_parent)) + parent_row: list[object] = [ORGANIZATION, JOB, RECORDED_AT] + parent_row[parent_index] = parent_uuid + cursor = ScriptedCursor([[], [tuple(parent_row)]], []) + connection = FakeConnection(cursor) + port = PostgresPeopleMutationPort(lambda: connection) + + try: + port.create_position( + command=position_command(), + authorization=position_authorization(), + ) + except PeopleMutationIntegrityError as error: + assert str(error) == "position parent identity is invalid" + else: + raise AssertionError("position parent UUID subtype was not rejected")