Skip to content
Merged
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
10 changes: 10 additions & 0 deletions crates/registry-relay/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@
`/ready` is always excluded because appending a readiness audit record after
its zero-backlog comparison would invalidate the next readiness probe.

- BREAKING: Governed reads now ignore `x-registry-subject-ref`,
`x-registry-relationship`, `x-registry-on-behalf-of`, and
`x-registry-credential-format` unless the authenticated principal has the
exact `registry:trust:<field>:<value>` scope. These optional trust-context
fields are now scope-gated before policy evaluation. Audit records retain
ordinary route scopes in `scopes_used`, replace each value-bearing trust
scope with a field-bound
`registry:trust:<field>:hmac-sha256:<digest>` handle under the deployment
audit key, and record authenticated trust-context field names in
`pdp_trust_provenance` without their values.
- BREAKING: Removed Relay-local credential issuance before 1.0. Relay no
longer accepts `provenance` or entity `publicschema` config, no longer serves
`/.well-known/did.json`, `/schemas/{claim_type}/{version}`, or
Expand Down
38 changes: 38 additions & 0 deletions crates/registry-relay/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,44 @@ Data-Purpose: https://data.example.gov/purposes/service-intake-check

Use stable, reviewable purpose IRIs. Do not put secrets, bearer tokens, or personal data in this header; it is recorded in audit logs.

## Governed request context

Registry Relay treats client-supplied Policy Decision Point (PDP) context as
untrusted. The supported client-supplied trust-context headers are listed
below. Relay passes each one to the PDP only when the authenticated principal
has the exact `registry:trust:<scope_field>:<header_value>` scope.

| Header | Scope field | Classification |
| --- | --- | --- |
| `x-registry-trust-jurisdiction` | `jurisdiction` | Scope-gated |
| `x-registry-trust-assurance` | `assurance` | Scope-gated |
| `x-registry-trust-legal-basis` | `legal_basis` | Scope-gated |
| `x-registry-trust-consent` | `consent` | Scope-gated |
| `x-registry-subject-ref` | `subject_ref` | Scope-gated |
| `x-registry-relationship` | `relationship` | Scope-gated |
| `x-registry-on-behalf-of` | `on_behalf_of` | Scope-gated |
| `x-registry-credential-format` | `requested_credential_format` | Scope-gated |
| `x-registry-source-observed-age-seconds` | `source_observed_age_seconds` | Scope-gated |
| `x-registry-source-observed-at-unix-seconds` | `source_observed_at_unix_seconds` | Scope-gated |
Comment thread
jeremi marked this conversation as resolved.

Audit records retain ordinary route scopes unchanged in `scopes_used`. Each
value-bearing trust scope is replaced by its canonical field-bound form,
`registry:trust:<field>:hmac-sha256:<digest>`, computed under the deployment
audit key. This preserves pseudonymous authorization evidence without storing
the raw value. `pdp_trust_provenance` separately records authenticated field
names without their values, including a malformed authenticated freshness
field when parsing denies the request before PDP evaluation.

An absent or nonmatching scope makes the header absent from PDP context. A
policy that requires the field or matches its value then denies the request.
`Data-Purpose` remains a caller-stated purpose, not proof of identity,
delegation, legal basis, consent, or source freshness.

Policy authors must not make a Permit decision depend on unauthenticated
request context. Use only server-derived context or values authenticated by an
exact-value trust scope. An adapter that adds another client-supplied
trust-context field must apply the same guard before the PDP evaluates it.

## Metadata, catalog, and OpenAPI

`GET /metadata/catalog` and `GET /metadata/dcat/bregdcat-ap` return only datasets visible to the authenticated principal's metadata scopes.
Expand Down
6 changes: 3 additions & 3 deletions crates/registry-relay/src/api/aggregates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use serde_json::{json, Value};
use tokio::sync::watch;

use crate::api::governed::{
attach_pdp_audit, require_governed_read_access, GovernedAccessError, GovernedReadDecision,
GovernedRedactionProjection, GovernedRequestInfo,
attach_governed_error_audit, attach_pdp_audit, require_governed_read_access,
GovernedAccessError, GovernedReadDecision, GovernedRedactionProjection, GovernedRequestInfo,
};
use crate::audit::{AuditContextExt, ErrorCodeExt};
use crate::auth::scopes::require_scope;
Expand Down Expand Up @@ -695,7 +695,7 @@ fn aggregate_access_error_response(
aggregate_id: Some(path.aggregate_id.clone()),
..AuditContextExt::default()
});
attach_pdp_audit(&mut audit_context, error.pdp_audit.as_ref());
attach_governed_error_audit(&mut audit_context, &error);
let mut response = error.error.into_response();
if let Some(context) = audit_context {
response.extensions_mut().insert(context);
Expand Down
10 changes: 8 additions & 2 deletions crates/registry-relay/src/api/attribute_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;

use crate::api::governed::{
attach_pdp_audit, purpose_header_value, require_governed_read_access, GovernedAccessError,
GovernedRedactionProjection, GovernedRequestInfo,
attach_pdp_audit, attach_pdp_trust_provenance, purpose_header_value,
require_governed_read_access, GovernedAccessError, GovernedRedactionProjection,
GovernedRequestInfo,
};
use crate::attribute_release::AttributeReleaseEvaluator;
use crate::audit::AuditContextExt;
Expand Down Expand Up @@ -137,6 +138,7 @@ async fn resolve(
cardinality_outcome: Some("one".to_string()),
availability_class: Some("available".to_string()),
pdp_audit: success.pdp_audit,
pdp_trust_provenance: BTreeSet::new(),
},
);
with_release_cache_headers(response, success_max_age)
Expand Down Expand Up @@ -173,6 +175,7 @@ struct ResolveAudit {
cardinality_outcome: Option<String>,
availability_class: Option<String>,
pdp_audit: Option<PdpDecisionAudit>,
pdp_trust_provenance: BTreeSet<String>,
}

/// Error carrying the audit context accumulated up to the point of failure, so
Expand Down Expand Up @@ -205,6 +208,7 @@ impl ResolveRunError {
cardinality_outcome,
availability_class,
pdp_audit,
pdp_trust_provenance: BTreeSet::new(),
},
}
}
Expand All @@ -225,6 +229,7 @@ impl From<GovernedAccessError> for ResolveRunError {
error: error.error,
audit: ResolveAudit {
pdp_audit: error.pdp_audit,
pdp_trust_provenance: error.pdp_trust_provenance,
..ResolveAudit::default()
},
}
Expand Down Expand Up @@ -844,6 +849,7 @@ fn with_audit_context(mut response: Response, route: &RouteState, audit: Resolve
..AuditContextExt::default()
});
attach_pdp_audit(&mut context, audit.pdp_audit.as_ref());
attach_pdp_trust_provenance(&mut context, &audit.pdp_trust_provenance);
if let Some(context) = context {
response.extensions_mut().insert(context);
}
Expand Down
8 changes: 4 additions & 4 deletions crates/registry-relay/src/api/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ use serde_json::{json, Value};
use tokio::sync::watch;

use crate::api::governed::{
attach_pdp_audit, entity_etag as governed_entity_etag, governed_cache_variant,
require_governed_read_access, strong_etag as governed_strong_etag, GovernedAccessError,
GovernedReadDecision, GovernedRedactionProjection, GovernedRequestInfo,
attach_governed_error_audit, attach_pdp_audit, entity_etag as governed_entity_etag,
governed_cache_variant, require_governed_read_access, strong_etag as governed_strong_etag,
GovernedAccessError, GovernedReadDecision, GovernedRedactionProjection, GovernedRequestInfo,
};
use crate::audit::{AuditContextExt, ErrorCodeExt};
use crate::auth::scopes::require_scope;
Expand Down Expand Up @@ -1047,7 +1047,7 @@ fn access_error_response(
error: GovernedAccessError,
mut context: Option<AuditContextExt>,
) -> Response {
attach_pdp_audit(&mut context, error.pdp_audit.as_ref());
attach_governed_error_audit(&mut context, &error);
with_optional_audit_context(error.error.into_response(), context)
}

Expand Down
Loading
Loading