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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .ci/readability-baseline.env
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Generated by scripts/readability-ratchet.sh
PROD_RS_TOTAL=523
PROD_FILES_GT300=209
PROD_FILES_GT500=84
PROD_RS_TOTAL=524
PROD_FILES_GT300=211
PROD_FILES_GT500=87
PROD_FILES_GT1000=22
PROD_MAX_FILE_LINES=2829
PROD_MAX_FILE_PATH=crates/temper-server/src/storage/mod.rs
ALLOW_CLIPPY_COUNT=35
ALLOW_CLIPPY_COUNT=36
ALLOW_DEAD_CODE_COUNT=14
PROD_PRINTLN_COUNT=247
PROD_UNWRAP_CI_OK_COUNT=132
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,8 @@ Before deploying any spec change:
3. Entity actors hot-deploy without dropping existing state
4. OData endpoints respond correctly for all entity types
5. Telemetry emits WideEvents for all transitions

## Definition of done — review bar

- **Three independent fresh-context reviews before anything is "done".** Nothing counts as fully implemented until all three have reviewed it, each with NO prior context on the work: **Codex** (`codex exec --model gpt-5.6-sol -c model_reasoning_effort="high" --sandbox read-only`), **a second independent Codex Sol session**, and **an independent Fable** (a fresh Claude subagent, `model: fable`). Run **Greptile** on every PR too (`@greptile review` as a PR comment). Ask each for severity, `file:line`, and a concrete failure scenario, then fix everything they find — including findings that criticise your own fixes — and re-verify. They catch different classes: one finds bypass surface, one finds fail-open behaviour, one finds whether it actually runs. Two agreeing does not excuse skipping the third.
- **Test the production shape, not a convenient one.** A gate proven against a permissive or mock engine, and an end-to-end that only exercises the happy verb, are not proof — verify against the real policy/config set and probe the generic paths too (PATCH/PUT/DELETE, not only the named action).
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,8 @@ Before deploying any spec change:
3. Entity actors hot-deploy without dropping existing state
4. OData endpoints respond correctly for all entity types
5. Telemetry emits WideEvents for all transitions

## Definition of done — review bar

- **Three independent fresh-context reviews before anything is "done".** Nothing counts as fully implemented until all three have reviewed it, each with NO prior context on the work: **Codex** (`codex exec --model gpt-5.6-sol -c model_reasoning_effort="high" --sandbox read-only`), **a second independent Codex Sol session**, and **an independent Fable** (a fresh Claude subagent, `model: fable`). Run **Greptile** on every PR too (`@greptile review` as a PR comment). Ask each for severity, `file:line`, and a concrete failure scenario, then fix everything they find — including findings that criticise your own fixes — and re-verify. They catch different classes: one finds bypass surface, one finds fail-open behaviour, one finds whether it actually runs. Two agreeing does not excuse skipping the third.
- **Test the production shape, not a convenient one.** A gate proven against a permissive or mock engine, and an end-to-end that only exercises the happy verb, are not proof — verify against the real policy/config set and probe the generic paths too (PATCH/PUT/DELETE, not only the named action).
121 changes: 121 additions & 0 deletions Cargo.lock

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

65 changes: 65 additions & 0 deletions crates/temper-authz/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,71 @@ impl SecurityContext {
}
}

/// Construct security context from a JWT whose signature was verified
/// against a registered trusted issuer.
///
/// Like [`from_resolved_identity`](Self::from_resolved_identity), identity
/// comes from a platform-verified source, never self-declared headers, and
/// `agentTypeVerified` is set. Unlike it, this path carries `acting_for`
/// (the owning human behind an agent) and `role`, both taken from verified
/// token claims. See RFC-0002.
#[allow(clippy::too_many_arguments)]
pub fn from_verified_jwt(
principal_id: &str,
kind: PrincipalKind,
agent_type: Option<&str>,
acting_for: Option<&str>,
role: Option<&str>,
session_id: Option<&str>,
) -> Self {
let mut attributes = HashMap::new();
attributes.insert(
"agentTypeVerified".to_string(),
serde_json::Value::Bool(true),
);

let mut context_attrs = HashMap::new();
context_attrs.insert(
"agentId".to_string(),
serde_json::Value::String(principal_id.to_string()),
);
if let Some(at) = agent_type {
context_attrs.insert(
"agentType".to_string(),
serde_json::Value::String(at.to_string()),
);
}
context_attrs.insert(
"agentTypeVerified".to_string(),
serde_json::Value::Bool(true),
);
if let Some(af) = acting_for {
context_attrs.insert(
"actingFor".to_string(),
serde_json::Value::String(af.to_string()),
);
}
if let Some(sid) = session_id {
context_attrs.insert(
"sessionId".to_string(),
serde_json::Value::String(sid.to_string()),
);
}

SecurityContext {
principal: Principal {
id: principal_id.to_string(),
kind,
role: role.map(|r| r.to_string()),
acting_for: acting_for.map(|a| a.to_string()),
agent_type: agent_type.map(|a| a.to_string()),
attributes,
},
context_attrs,
correlation_id: uuid::Uuid::now_v7().to_string(),
}
}

/// Enrich security context with agent identity from self-declared headers.
///
/// **Deprecated**: Use `from_resolved_identity()` for credential-based identity.
Expand Down
23 changes: 22 additions & 1 deletion crates/temper-authz/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,13 @@ impl AuthzEngine {
/// so that Cedar evaluates to Allow for every principal kind (System or
/// otherwise). Used in tests and permissive dev environments.
pub fn permissive() -> Self {
let policy_set =
let mut policy_set =
PolicySet::from_str("permit(principal, action, resource);").unwrap_or_default();
// Even a permit-all fallback (e.g. the ARN-230 fail-open path) must keep
// the system-platform forbids — the god-mode identity entities
// (TrustedIssuer / PrincipalGeneration) stay System/Admin-only, so a
// fail-open tenant can never become an authz-takeover (ARN-255).
merge_system_platform_policy(&mut policy_set);
Self {
tenant_policies: RwLock::new(BTreeMap::new()),
fallback_policy_set: RwLock::new(CompiledPolicies::new(policy_set)),
Expand Down Expand Up @@ -680,6 +685,22 @@ impl AuthzEngine {
const SYSTEM_PLATFORM_POLICY: &str = r#"
@id("system-platform:broad-permit")
permit(principal is System, action, resource);

@id("system-platform:identity-entities-permit-trusted-issuer")
permit(principal, action, resource is TrustedIssuer)
when { principal is System || principal is Admin || (principal has agent_type && principal.agent_type == "operator") };

@id("system-platform:identity-entities-permit-principal-generation")
permit(principal, action, resource is PrincipalGeneration)
when { principal is System || principal is Admin || (principal has agent_type && principal.agent_type == "operator") };

@id("system-platform:protect-trusted-issuer")
forbid(principal, action, resource is TrustedIssuer)
unless { principal is System || principal is Admin || (principal has agent_type && principal.agent_type == "operator") };

@id("system-platform:protect-principal-generation")
forbid(principal, action, resource is PrincipalGeneration)
unless { principal is System || principal is Admin || (principal has agent_type && principal.agent_type == "operator") };
"#;

/// PolicyId prefix used for the built-in system-platform policies
Expand Down
Loading
Loading