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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `GET /v1/exports/by-idempotency/{idempotency_key}/request` returns the stored naruon export-authorization request on `tepp-loopback` (ADR 0099). Dual identity of stored-request GET (`export_id`). Zero and ambiguous matches fail closed. `tepp.scientific_acceptance.v1` never appears. LineageWeave refused. `NaruonLiveService` stays POST-only. Does not re-open cancel lineages. Not GAP-010 Figma/export, not persistence.
1 change: 1 addition & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| Unicode canonical-identity doctoring | [`docs/research/unicode-canonical-identity.md`](docs/research/unicode-canonical-identity.md) |
| Export idempotency-key lookup HTTP doctoring | [`docs/research/export-idempotency-lookup-http.md`](docs/research/export-idempotency-lookup-http.md) |
| Export idempotency-key lookup CLI doctoring | [`docs/research/export-idempotency-lookup-cli.md`](docs/research/export-idempotency-lookup-cli.md) |
| Export idempotency-key lookup stored-request GET doctoring | [`docs/research/export-idempotency-lookup-stored-request-http.md`](docs/research/export-idempotency-lookup-stored-request-http.md) |
| Change history | [`CHANGELOG.md`](CHANGELOG.md) |

## Maturity vocabulary
Expand Down
46 changes: 46 additions & 0 deletions crates/tepp_api/src/analysis_run_live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
//! boundaries needed by Naruon and `LineageWeave`. Naruon may also POST and
//! GET `/v1/exports/{export_id}` for metric-free purpose-bound retrieval
//! and `GET /v1/exports/by-idempotency/{idempotency_key}` for key lookup.
//! `GET /v1/exports/by-idempotency/{idempotency_key}/request` returns the stored
//! export-authorization request of that unique accepted export.
//! It accepts transport acknowledgements, temporal evidence context, and
//! export identities only; completed psychometric results remain outside this
//! crate.
Expand All @@ -18,6 +20,10 @@ use crate::export_idempotency_lookup_http::{
ExportIdempotencyLookup, export_idempotency_lookup_path_key,
refuse_metrics_on_export_idempotency_lookup_payload,
};
use crate::export_idempotency_lookup_stored_request_http::{
export_idempotency_lookup_stored_request_path_key,
refuse_metrics_on_export_lookup_stored_request_payload,
};
use crate::lineageweave_http::{
LINEAGEWEAVE_CONSUMER_CODE, NARUON_CONSUMER_CODE, consumer_is_supported,
};
Expand Down Expand Up @@ -167,6 +173,12 @@ impl AnalysisRunLiveService {
let (method, path) = parse_request_line(lines.next().unwrap_or(""))?;
let headers = parse_headers(&mut lines)?;
if method == "GET" {
if matches!(
export_idempotency_lookup_stored_request_path_key(path),
Ok(_) | Err(ApiError::LimitExceeded)
) {
return self.lookup_export_stored_request_by_idempotency(path, &headers, body);
}
if matches!(
export_idempotency_lookup_path_key(path),
Ok(_) | Err(ApiError::LimitExceeded)
Expand Down Expand Up @@ -392,6 +404,40 @@ impl AnalysisRunLiveService {
Ok(json_response(200, "OK", response_body))
}

fn lookup_export_stored_request_by_idempotency(
&self,
path: &str,
headers: &HashMap<String, String>,
body: &str,
) -> Result<NaruonLiveResponse, ApiError> {
let idempotency_key = export_idempotency_lookup_stored_request_path_key(path)?;
if !body.trim().is_empty() {
return Err(ApiError::InvalidWirePayload);
}
let consumer = require_headers(headers, self.bound_addr, false)?;
if consumer != NARUON_CONSUMER_CODE {
return Err(ApiError::InvalidWirePayload);
}
refuse_metrics_on_export_lookup_stored_request_payload(body)?;
let prefix = format!("{consumer}\u{1f}");
let mut matches: Vec<&StoredExport> = self
.authorized_exports
.iter()
.filter(|(replay_key, stored)| {
replay_key.starts_with(&prefix)
&& stored.retrieval.idempotency_key == idempotency_key
})
Comment on lines +417 to +429

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 Idempotency keys bypass tenant access

A caller with an idempotency key can use lookup_export_stored_request_by_idempotency across all Naruon tenants. The response exposes the matching tenant and principal without caller binding.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified as valid after the strict-ancestor fold into #466 and repaired there rather than leaving the finding as a blocker. RED 8f38c2708771ead5ca197b65b3c8973ac7aebfea requires the unscoped stored-request lookup to fail closed and not echo tenant/principal/artifact identity. GREEN 45754fdb46d4a2c84db9e440f5eb0001c2f26d10 quarantines the route: serialized tenant_workspace_id/principal_id are forbidden on this response boundary and the client builder returns AuthorizationDenied until an authenticated tenant+principal binding exists. The surviving landing vehicle is #466; do not reactivate by trusting caller-controlled scope headers.

.map(|(_, stored)| stored)
.collect();
if matches.len() != 1 {
return Err(ApiError::InvalidWirePayload);
}
let stored = matches.remove(0);
let response_body = crate::wire::to_json(&stored.request)?;
refuse_metrics_on_export_lookup_stored_request_payload(&response_body)?;
Ok(json_response(200, "OK", response_body))
}

fn response_from_error(&mut self, error: ApiError) -> NaruonLiveResponse {
let request_id = format!("analysis-run-live-{}", self.next_request_serial);
self.next_request_serial += 1;
Expand Down
Loading
Loading