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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ boundaries above remain the target modular MSA architecture.
| `tepp_simulation` | known-truth temporal/event data generation |
| `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics |
| `tepp_api` | versioned DTO, schema, and export contracts |
| `purpose_authorization` | purpose-bound grants; blanket PII masking is not authorization |

No crate exposes placeholder production behavior in Task 1. This prevents an
empty façade from becoming a de facto public API before its invariants and tests
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- `purpose_authorization` purpose-bound grants: a grant authorizes one processing purpose for one principal, cannot be reused across purposes, cannot be replaced by blanket PII masking, and recovered purposes match known truth at a higher computed rate than a collapsed single-purpose assignment.
- `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number.
- `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011).
- `persistence_postgres` audit-event SQL contracts: append-only insert that refuses empty, oversized, or hostile `action_code` values before SQL is rendered.
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/purpose_authorization",
]
default-members = [
"crates/evidence_core",
Expand All @@ -23,6 +24,7 @@ default-members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/purpose_authorization",
]

[workspace.package]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ implemented in Rust.
## Current implementation state

This branch establishes the Task 1 Rust workspace and quality-gate foundation.
The ten bounded crates compile independently but intentionally expose no
The eleven bounded crates compile independently but intentionally expose no
placeholder production APIs. Domain behavior begins in Task 2 with immutable
evidence identifiers and source records.

Expand All @@ -22,6 +22,7 @@ crates/corpus_split
crates/tepp_simulation
crates/validation_core
crates/tepp_api
crates/purpose_authorization
```

## Local verification
Expand Down
20 changes: 20 additions & 0 deletions crates/purpose_authorization/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "purpose_authorization"
description = "Purpose-bound authorization grants that refuse blanket masking."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
readme.workspace = true
keywords.workspace = true
categories.workspace = true
publish = false

[dependencies]
uuid = { workspace = true }

[lints]
workspace = true
60 changes: 60 additions & 0 deletions crates/purpose_authorization/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! Fail-closed purpose-authorization errors.

use std::fmt;

/// A fail-closed purpose-authorization error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PurposeAuthorizationError {
/// A grant was used for a purpose it does not authorize.
CrossPurposeUse,
/// Blanket PII masking was offered as a substitute for authorization.
BlanketMaskIsNotAuthorization,
/// An unknown purpose wire name was supplied.
UnknownPurpose,
/// Purpose slices were empty or length-mismatched.
InvalidPurposePayload,
}

impl fmt::Display for PurposeAuthorizationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::CrossPurposeUse => "authorization grant used for a different purpose",
Self::BlanketMaskIsNotAuthorization => "blanket mask is not authorization",
Self::UnknownPurpose => "unknown processing purpose",
Self::InvalidPurposePayload => "invalid purpose payload",
};
formatter.write_str(message)
}
}

impl std::error::Error for PurposeAuthorizationError {}

#[cfg(test)]
mod tests {
use super::PurposeAuthorizationError;

#[test]
fn error_messages_are_stable() {
for (error, message) in [
(
PurposeAuthorizationError::CrossPurposeUse,
"authorization grant used for a different purpose",
),
(
PurposeAuthorizationError::BlanketMaskIsNotAuthorization,
"blanket mask is not authorization",
),
(
PurposeAuthorizationError::UnknownPurpose,
"unknown processing purpose",
),
(
PurposeAuthorizationError::InvalidPurposePayload,
"invalid purpose payload",
),
] {
assert_eq!(error.to_string(), message);
}
}
}
74 changes: 74 additions & 0 deletions crates/purpose_authorization/src/grant.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! Purpose-bound grants held by an opaque principal.

use crate::{PurposeAuthorizationError, PurposeCode, refuse_cross_purpose_use};
use uuid::Uuid;

/// Opaque principal that holds a purpose grant.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct PrincipalId(Uuid);

impl PrincipalId {
/// Reconstruct from a UUID.
#[must_use]
pub const fn from_uuid(value: Uuid) -> Self {
Self(value)
}

/// Borrow the UUID value.
#[must_use]
pub const fn as_uuid(self) -> Uuid {
self.0
}
}

/// One purpose-bound authorization grant.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AuthorizationGrant {
purpose: PurposeCode,
principal: PrincipalId,
}

impl AuthorizationGrant {
/// Bind a principal to one processing purpose.
#[must_use]
pub const fn new(purpose: PurposeCode, principal: PrincipalId) -> Self {
Self { purpose, principal }
}

/// Return the granted purpose.
#[must_use]
pub const fn purpose(self) -> PurposeCode {
self.purpose
}

/// Return the holding principal.
#[must_use]
pub const fn principal(self) -> PrincipalId {
self.principal
}

/// Authorize a requested purpose against this grant.
///
/// # Errors
///
/// Returns [`PurposeAuthorizationError::CrossPurposeUse`] when the
/// requested purpose differs.
pub fn authorize(self, requested: PurposeCode) -> Result<(), PurposeAuthorizationError> {
refuse_cross_purpose_use(self.purpose, requested)
}
}

#[cfg(test)]
mod tests {
use super::{AuthorizationGrant, PrincipalId};
use crate::PurposeCode;
use uuid::Uuid;

#[test]
fn grant_accessors_round_trip() {
let principal = PrincipalId::from_uuid(Uuid::from_u128(8));
let grant = AuthorizationGrant::new(PurposeCode::ExportFulfillment, principal);
assert_eq!(grant.purpose(), PurposeCode::ExportFulfillment);
assert_eq!(grant.principal().as_uuid(), Uuid::from_u128(8));
}
}
27 changes: 27 additions & 0 deletions crates/purpose_authorization/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::cast_precision_loss)]
//! Purpose-bound authorization grants that refuse blanket PII masking.
//!
//! A grant authorizes one processing purpose for one principal. It cannot be
//! reused for another purpose, and masking identifiers is not authorization
//! (ADR 0009).

mod error;
mod grant;
mod purpose;

/// Fail-closed purpose-authorization errors.
pub use error::PurposeAuthorizationError;
/// One purpose-bound grant.
pub use grant::AuthorizationGrant;
/// Opaque principal identity.
pub use grant::PrincipalId;
/// Closed processing-purpose vocabulary.
pub use purpose::PurposeCode;
/// Fraction of recovered purposes that match known truth.
pub use purpose::purpose_recovery_rate;
/// Refuse to treat blanket PII masking as authorization.
pub use purpose::refuse_blanket_mask_as_authorization;
/// Refuse to use a grant for a different purpose.
pub use purpose::refuse_cross_purpose_use;
121 changes: 121 additions & 0 deletions crates/purpose_authorization/src/purpose.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! Closed processing-purpose vocabulary and recovery.

use crate::PurposeAuthorizationError;

/// Closed processing-purpose vocabulary bound to TEPP retention purposes.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PurposeCode {
/// Psychometric and statistical analysis.
PsychometricAnalysis,
/// Legal or contractual preservation.
LegalPreservation,
/// Operations and audit review.
OperationsAudit,
/// Authorized export fulfillment.
ExportFulfillment,
}

impl PurposeCode {
/// Stable wire name.
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Self::PsychometricAnalysis => "psychometric_analysis",
Self::LegalPreservation => "legal_preservation",
Self::OperationsAudit => "operations_audit",
Self::ExportFulfillment => "export_fulfillment",
}
}

/// Parse a stable wire purpose name.
///
/// # Errors
///
/// Returns [`PurposeAuthorizationError::UnknownPurpose`] for unrecognized names.
pub fn from_wire_name(name: &str) -> Result<Self, PurposeAuthorizationError> {
match name {
"psychometric_analysis" => Ok(Self::PsychometricAnalysis),
"legal_preservation" => Ok(Self::LegalPreservation),
"operations_audit" => Ok(Self::OperationsAudit),
"export_fulfillment" => Ok(Self::ExportFulfillment),
_ => Err(PurposeAuthorizationError::UnknownPurpose),
}
}
}

/// Fraction of recovered purposes that match known truth.
///
/// # Errors
///
/// Returns [`PurposeAuthorizationError::InvalidPurposePayload`] when either
/// slice is empty or the lengths differ.
pub fn purpose_recovery_rate(
truth: &[PurposeCode],
decided: &[PurposeCode],
) -> Result<f64, PurposeAuthorizationError> {
if truth.is_empty() || truth.len() != decided.len() {
return Err(PurposeAuthorizationError::InvalidPurposePayload);
}
let mut matches = 0_u32;
for (truth_purpose, decided_purpose) in truth.iter().zip(decided) {
if truth_purpose == decided_purpose {
matches += 1;
}
}
Ok(f64::from(matches) / truth.len() as f64)
}

/// Explicit refusal to use a grant for a different purpose.
///
/// # Errors
///
/// Returns [`PurposeAuthorizationError::CrossPurposeUse`] when the purposes
/// differ.
pub fn refuse_cross_purpose_use(
granted: PurposeCode,
requested: PurposeCode,
) -> Result<(), PurposeAuthorizationError> {
if granted == requested {
Ok(())
} else {
Err(PurposeAuthorizationError::CrossPurposeUse)
}
}

/// Explicit refusal to treat blanket PII masking as authorization.
///
/// # Errors
///
/// Always returns [`PurposeAuthorizationError::BlanketMaskIsNotAuthorization`].
pub fn refuse_blanket_mask_as_authorization() -> Result<(), PurposeAuthorizationError> {
Err(PurposeAuthorizationError::BlanketMaskIsNotAuthorization)
}

#[cfg(test)]
mod tests {
use super::{PurposeCode, purpose_recovery_rate};
use crate::PurposeAuthorizationError;

#[test]
fn wire_names_round_trip() {
for purpose in [
PurposeCode::PsychometricAnalysis,
PurposeCode::LegalPreservation,
PurposeCode::OperationsAudit,
PurposeCode::ExportFulfillment,
] {
assert_eq!(
PurposeCode::from_wire_name(purpose.wire_name()).expect("round trip"),
purpose
);
}
assert_eq!(
PurposeCode::from_wire_name("marketing"),
Err(PurposeAuthorizationError::UnknownPurpose)
);
assert_eq!(
purpose_recovery_rate(&[PurposeCode::OperationsAudit], &[]),
Err(PurposeAuthorizationError::InvalidPurposePayload)
);
}
}
7 changes: 7 additions & 0 deletions crates/purpose_authorization/tests/crate_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Integration contract for the `purpose_authorization` package identity.

#[test]
fn package_identity_is_stable() {
let observed = std::hint::black_box(env!("CARGO_PKG_NAME"));
assert_eq!(observed, "purpose_authorization");
}
Loading
Loading