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
8 changes: 4 additions & 4 deletions contract/receipt.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"contract/schemas/result.payload.json": "1bd5261c36024a16810f262e65449f3cf81de819c1d8189abd68a75133cae8c8",
"contract/schemas/result.query.json": "b21f5cfb583971723afffe4fdd46ae0097bc462a1749ca6d21c93df020edad85",
"contract/schemas/result.reasoning.json": "28635fa0421d816cc61cea7af61f12b949b374fc7b808c7483d29e5857950d49",
"contract/schemas/result.security.json": "6f9eddc71dcedb51f65cb6b5a100478f7de09a429e894f100048198a0ad5f3bc",
"contract/schemas/result.security.json": "02b8ce8e2e11db3870f481e4fb60d467e74093d5c6429fe7179f5ba4d3173ec3",
"contract/schemas/result.storage.json": "a8f2cf40f9331e7a9688869d7c7363ab73e5ee940a6fd1ad9687b01453c98f55",
"contract/schemas/result.transactions.json": "0b088662b42178c85ec0ac106f83937b6d51c4ec9214fdb18ea5e6523b159b25",
"contract/scopes.json": "1cee561b95b664965005d01fe0e7a876932477aea9a0355b6f20e8072df0c9eb",
Expand All @@ -34,7 +34,7 @@
"epistemic_graph/contract/schemas/result.payload.json": "1bd5261c36024a16810f262e65449f3cf81de819c1d8189abd68a75133cae8c8",
"epistemic_graph/contract/schemas/result.query.json": "b21f5cfb583971723afffe4fdd46ae0097bc462a1749ca6d21c93df020edad85",
"epistemic_graph/contract/schemas/result.reasoning.json": "28635fa0421d816cc61cea7af61f12b949b374fc7b808c7483d29e5857950d49",
"epistemic_graph/contract/schemas/result.security.json": "6f9eddc71dcedb51f65cb6b5a100478f7de09a429e894f100048198a0ad5f3bc",
"epistemic_graph/contract/schemas/result.security.json": "02b8ce8e2e11db3870f481e4fb60d467e74093d5c6429fe7179f5ba4d3173ec3",
"epistemic_graph/contract/schemas/result.storage.json": "a8f2cf40f9331e7a9688869d7c7363ab73e5ee940a6fd1ad9687b01453c98f55",
"epistemic_graph/contract/schemas/result.transactions.json": "0b088662b42178c85ec0ac106f83937b6d51c4ec9214fdb18ea5e6523b159b25",
"epistemic_graph/contract/scopes.json": "1cee561b95b664965005d01fe0e7a876932477aea9a0355b6f20e8072df0c9eb",
Expand All @@ -58,7 +58,7 @@
"epistemic_graph/generated/ingestion.py": "2735ad462eb460209318ecd6e898b2e1de81cc92388ac37f27fb149afacfbc5f",
"epistemic_graph/generated/managed_index.py": "2359e4b871d4ef359c189ac8debb077fbc04cda00280d42bc6a84aacf3da04d1",
"epistemic_graph/generated/messaging.py": "0c00d8d93ed7fba40eeeb572c14c7354eaa33693e8acda369942ee9598907e1f",
"epistemic_graph/generated/models.py": "c52786f9b95f6d816b543dcf97c0bae8da8ceed8d0f2708767f4868d08518cec",
"epistemic_graph/generated/models.py": "a5f2de6b31f9d3243520871566005bf963468ab39d06fcf48ee201b2f67445a2",
"epistemic_graph/generated/policy_evolution.py": "c9a836d2777c9ce2991236db4656966598d0de3463a574f1b5b05780c9be2205",
"epistemic_graph/generated/query.py": "85db01171e8f4278b841872939cbed7d47d76415b5516b67dd83eaa8e16b3201",
"epistemic_graph/generated/rdf_report.py": "600708e31131a12a37ec4a890490bbd3a0806a16b4961b3a0916d213ef535d49",
Expand All @@ -71,7 +71,7 @@
"epistemic_graph/generated/transactions.py": "83174fd4a8d2032577c60bf61dacf08fbe3b5df8530da66a19d1b36eb9920335",
"epistemic_graph/generated/write_back.py": "cbd29e0308e7e095abf84d5f31a67ce88ea46c69f8f39a95369a03ce82c21df0"
},
"contract_digest": "cca301febcdb7c977970e932b68f48b621f8154df2a0c9594201130efe88bb10",
"contract_digest": "488f3feb732b2389f8f5fc2875f6cc61086d93b28f852bfd0bc9379ebeee504a",
"contract_version": 1,
"feature_profile": [
"contract",
Expand Down
79 changes: 78 additions & 1 deletion contract/schemas/result.security.json
Original file line number Diff line number Diff line change
Expand Up @@ -1371,7 +1371,7 @@
},
"PrincipalResolution": {
"additionalProperties": false,
"description": "What the local issuer puts in a principal's token.",
"description": "Current principal projection. The store uses `()`; public issuance requires\n`RequestContextClaims`, composed only after credential and policy validation.",
"properties": {
"groups": {
"items": {
Expand All @@ -1396,6 +1396,10 @@
"principal_id": {
"type": "string"
},
"request_context": {
"$ref": "#/$defs/RequestContextClaims",
"description": "Required in the public specialization; never a default authority."
},
"roles": {
"items": {
"type": "string"
Expand Down Expand Up @@ -1424,6 +1428,7 @@
}
},
"required": [
"request_context",
"principal_id",
"username",
"kind",
Expand Down Expand Up @@ -1515,6 +1520,78 @@
}
]
},
"RequestContextClaims": {
"additionalProperties": false,
"description": "Identity and policy claims carried by a verified request-context envelope.\n\nThis is the wire representation only. Callers must not treat a deserialized\nvalue as trusted until the server has verified the envelope MAC, deployment\naudience/tenant/policy version, request binding, and delegation chain. The\nserver exposes a separate, non-constructible `VerifiedRequestContext` wrapper\nafter those checks succeed.",
"properties": {
"agent_id": {
"description": "Effective agent identity used for ACL/RBAC decisions.",
"type": "string"
},
"audience": {
"description": "Intended service audience.",
"type": "string"
},
"delegation": {
"description": "Ordered delegation path from `principal` to `agent_id` (inclusive).\nEmpty means no delegation and therefore requires principal == agent_id.",
"items": {
"type": "string"
},
"type": "array"
},
"node": {
"description": "Target node id this envelope was minted for (ADR-3 / W1.9 node-bound\nenvelopes — ``reports/wave1/ADR-scale-trio.md``). `None` for a client\nthat predates node binding, or one that has not yet learned a target\nnode id; a single-node deployment never needs to set this. When\n`Some(id)`, the server exact-matches it against the receiving node's\nown identity before dispatch (`server::auth::validate_context_claims`)\n— a mismatch means the envelope was captured and replayed against a\ndifferent cluster member. `#[serde(default)]` so an envelope minted by\na client that has never heard of this claim deserializes exactly as\nbefore (absent, not a hard error).",
"type": [
"string",
"null"
]
},
"policy_version": {
"description": "Exact policy bundle/version under which the context was issued.",
"type": "string"
},
"principal": {
"description": "Authenticated subject that originated the request.",
"type": "string"
},
"priority": {
"description": "Advisory QoS admission-priority class this request declares (W2.4 —\nengine-native QoS lanes). One of the agent-utilities `PriorityClass`\nwire values `\"interactive\"` / `\"orchestration\"` / `\"hydration\"` /\n`\"background_ingestion\"`; any other or absent value is treated as the\norchestration default (high, never starved — matching au's\nuntagged-context semantics). Mapped to a [`crate`]-independent admission\nclass by the server's QoS scheduler (`server::qos::QosClass`).\n\nIt is **MAC-covered** (appended to `build_envelope_v2_bytes` as a\ndistinct tag-`2` optional trailer, after the tag-`1` node trailer) so a\nnoisy principal cannot forge a higher class to defeat the admission\nordering — the priority is bound to the signed envelope exactly like\nevery other claim. `#[serde(default)]` + presence-gated encoding keep an\nenvelope minted by a client that predates this claim byte-for-byte\nidentical to before (absent, not a hard error) — fully additive.",
"type": [
"string",
"null"
]
},
"roles": {
"description": "Identity-provider roles asserted for policy evaluation/audit.",
"items": {
"type": "string"
},
"type": "array"
},
"scopes": {
"description": "Fine-grained capabilities asserted for policy evaluation/audit.",
"items": {
"type": "string"
},
"type": "array"
},
"tenant": {
"description": "Tenant security boundary selected by the deployment.",
"type": "string"
}
},
"required": [
"principal",
"tenant",
"audience",
"agent_id",
"roles",
"scopes",
"policy_version",
"delegation"
],
"type": "object"
},
"ResetDelivery": {
"additionalProperties": false,
"description": "Where a self-service reset link goes (`None`: nowhere, uniformly).",
Expand Down
2 changes: 1 addition & 1 deletion crates/eg-capabilities/generated/catalog_digest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@

/// `contract/receipt.json`'s `contract_digest`.
pub const CONTRACT_CATALOG_DIGEST: &str =
"cca301febcdb7c977970e932b68f48b621f8154df2a0c9594201130efe88bb10";
"488f3feb732b2389f8f5fc2875f6cc61086d93b28f852bfd0bc9379ebeee504a";
12 changes: 11 additions & 1 deletion crates/eg-core/src/isolation/layer_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ impl IsolationLayer {
proof: &[u8],
) -> Result<Self, crate::rbac_persist::RbacPersistError> {
let store = crate::rbac_persist::RbacStore::open(dir, verifier, principal, proof)?;
Self::with_policy_store(std::sync::Arc::new(store))
}

/// Native adapter seam. It loads and validates the same complete durable
/// image as the directory opener. Atomic-store fault fixtures also use it.
#[doc(hidden)]
#[cfg(feature = "security")]
pub fn with_policy_store(
store: std::sync::Arc<dyn crate::rbac_persist::RbacPolicyStore>,
) -> Result<Self, crate::rbac_persist::RbacPersistError> {
let (rbac, identities, identity_bootstrap) = store.load()?;
if identity_bootstrap == crate::rbac_persist::IdentityBootstrapState::Pending
&& !holds_only_identity_store_state(&rbac, &identities)
Expand All @@ -49,7 +59,7 @@ impl IsolationLayer {
agents,
rbac,
identity_bootstrap,
persist: Some(std::sync::Arc::new(store)),
persist: Some(store),
})
}

Expand Down
28 changes: 26 additions & 2 deletions crates/eg-types/src/identity/projection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ use super::store::IdentityStore;
use super::{IdentityRefusal, RBAC_ROLE_PREFIX};
use crate::acl::{AgentIdentity, AgentRole, Grant, Role};

/// What the local issuer puts in a principal's token.
/// Current principal projection. The store uses `()`; public issuance requires
/// `RequestContextClaims`, composed only after credential and policy validation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "contract-schema", derive(schemars::JsonSchema))]
pub struct PrincipalResolution {
pub struct PrincipalResolution<C = ()> {
/// Required in the public specialization; never a default authority.
pub request_context: C,
pub principal_id: String,
pub username: String,
pub kind: UserKind,
Expand All @@ -40,6 +43,26 @@ pub struct PrincipalResolution {
pub session_mfa_pending: bool,
}

impl PrincipalResolution {
/// Attach server-derived authority without recomputing narrowed scopes.
pub fn with_request_context<C>(self, request_context: C) -> PrincipalResolution<C> {
PrincipalResolution {
request_context,
principal_id: self.principal_id,
username: self.username,
kind: self.kind,
status: self.status,
is_bootstrap: self.is_bootstrap,
roles: self.roles,
groups: self.groups,
scopes: self.scopes,
mfa_required: self.mfa_required,
mfa_enrolled: self.mfa_enrolled,
session_mfa_pending: self.session_mfa_pending,
}
}
}

/// The RBAC state the store owns.
#[derive(Debug, Clone, Default)]
pub struct RbacProjection {
Expand Down Expand Up @@ -121,6 +144,7 @@ impl IdentityStore {
.filter_map(|group| self.groups.get(group))
.any(|group| group.mfa_required);
Ok(PrincipalResolution {
request_context: (),
principal_id: principal_id.to_string(),
username: user.username.clone(),
kind: user.kind,
Expand Down
34 changes: 32 additions & 2 deletions crates/eg-types/src/identity/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ pub struct ResetDelivery {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
#[cfg_attr(feature = "contract-schema", derive(schemars::JsonSchema))]
pub enum IdentityReply {
pub enum IdentityReply<C = ()> {
/// A write that answers nothing but whether it changed state.
Done {
changed: bool,
Expand All @@ -207,7 +207,7 @@ pub enum IdentityReply {
},
User(UserView),
Users(Vec<UserView>),
Resolution(PrincipalResolution),
Resolution(PrincipalResolution<C>),
Authenticate(AuthenticateResult),
Sessions(Vec<SessionView>),
Roles(Vec<RoleRecord>),
Expand All @@ -225,3 +225,33 @@ pub enum IdentityReply {
/// `issue_password_reset`.
ResetDelivery(ResetDelivery),
}

impl IdentityReply {
/// Convert the raw store reply to the public authority-bearing reply.
/// Only resolution replies invoke the fallible producer.
pub fn try_with_request_context<C, E>(
self,
compose: impl FnOnce(PrincipalResolution) -> Result<PrincipalResolution<C>, E>,
) -> Result<IdentityReply<C>, E> {
Ok(match self {
Self::Config(value) => IdentityReply::Config(value),
Self::User(value) => IdentityReply::User(value),
Self::Users(value) => IdentityReply::Users(value),
Self::Authenticate(value) => IdentityReply::Authenticate(value),
Self::Sessions(value) => IdentityReply::Sessions(value),
Self::Roles(value) => IdentityReply::Roles(value),
Self::Groups(value) => IdentityReply::Groups(value),
Self::Idps(value) => IdentityReply::Idps(value),
Self::Audit(value) => IdentityReply::Audit(value),
Self::Sql(value) => IdentityReply::Sql(value),
Self::Provisioned(value) => IdentityReply::Provisioned(value),
Self::DirectoryGroup(value) => IdentityReply::DirectoryGroup(value),
Self::DirectoryGroups(value) => IdentityReply::DirectoryGroups(value),
Self::WebauthnCredentials(value) => IdentityReply::WebauthnCredentials(value),
Self::ResetDelivery(value) => IdentityReply::ResetDelivery(value),
Self::Done { changed } => IdentityReply::Done { changed },
Self::Principal { principal_id } => IdentityReply::Principal { principal_id },
Self::Resolution(value) => IdentityReply::Resolution(compose(value)?),
})
}
}
2 changes: 1 addition & 1 deletion crates/eg-types/src/result_contract/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,5 +223,5 @@ method_results! {
CheckAccess(CheckAccess) => Json<AccessDecision>;
// Every identity op answers one tagged reply; no reply variant can
// carry a hash, token or sealed secret.
Identity(Identity) => Json<crate::identity::IdentityReply>;
Identity(Identity) => Json<crate::identity::IdentityReply<crate::acl::RequestContextClaims>>;
}
8 changes: 4 additions & 4 deletions epistemic_graph/contract/receipt.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"contract/schemas/result.payload.json": "1bd5261c36024a16810f262e65449f3cf81de819c1d8189abd68a75133cae8c8",
"contract/schemas/result.query.json": "b21f5cfb583971723afffe4fdd46ae0097bc462a1749ca6d21c93df020edad85",
"contract/schemas/result.reasoning.json": "28635fa0421d816cc61cea7af61f12b949b374fc7b808c7483d29e5857950d49",
"contract/schemas/result.security.json": "6f9eddc71dcedb51f65cb6b5a100478f7de09a429e894f100048198a0ad5f3bc",
"contract/schemas/result.security.json": "02b8ce8e2e11db3870f481e4fb60d467e74093d5c6429fe7179f5ba4d3173ec3",
"contract/schemas/result.storage.json": "a8f2cf40f9331e7a9688869d7c7363ab73e5ee940a6fd1ad9687b01453c98f55",
"contract/schemas/result.transactions.json": "0b088662b42178c85ec0ac106f83937b6d51c4ec9214fdb18ea5e6523b159b25",
"contract/scopes.json": "1cee561b95b664965005d01fe0e7a876932477aea9a0355b6f20e8072df0c9eb",
Expand All @@ -34,7 +34,7 @@
"epistemic_graph/contract/schemas/result.payload.json": "1bd5261c36024a16810f262e65449f3cf81de819c1d8189abd68a75133cae8c8",
"epistemic_graph/contract/schemas/result.query.json": "b21f5cfb583971723afffe4fdd46ae0097bc462a1749ca6d21c93df020edad85",
"epistemic_graph/contract/schemas/result.reasoning.json": "28635fa0421d816cc61cea7af61f12b949b374fc7b808c7483d29e5857950d49",
"epistemic_graph/contract/schemas/result.security.json": "6f9eddc71dcedb51f65cb6b5a100478f7de09a429e894f100048198a0ad5f3bc",
"epistemic_graph/contract/schemas/result.security.json": "02b8ce8e2e11db3870f481e4fb60d467e74093d5c6429fe7179f5ba4d3173ec3",
"epistemic_graph/contract/schemas/result.storage.json": "a8f2cf40f9331e7a9688869d7c7363ab73e5ee940a6fd1ad9687b01453c98f55",
"epistemic_graph/contract/schemas/result.transactions.json": "0b088662b42178c85ec0ac106f83937b6d51c4ec9214fdb18ea5e6523b159b25",
"epistemic_graph/contract/scopes.json": "1cee561b95b664965005d01fe0e7a876932477aea9a0355b6f20e8072df0c9eb",
Expand All @@ -58,7 +58,7 @@
"epistemic_graph/generated/ingestion.py": "2735ad462eb460209318ecd6e898b2e1de81cc92388ac37f27fb149afacfbc5f",
"epistemic_graph/generated/managed_index.py": "2359e4b871d4ef359c189ac8debb077fbc04cda00280d42bc6a84aacf3da04d1",
"epistemic_graph/generated/messaging.py": "0c00d8d93ed7fba40eeeb572c14c7354eaa33693e8acda369942ee9598907e1f",
"epistemic_graph/generated/models.py": "c52786f9b95f6d816b543dcf97c0bae8da8ceed8d0f2708767f4868d08518cec",
"epistemic_graph/generated/models.py": "a5f2de6b31f9d3243520871566005bf963468ab39d06fcf48ee201b2f67445a2",
"epistemic_graph/generated/policy_evolution.py": "c9a836d2777c9ce2991236db4656966598d0de3463a574f1b5b05780c9be2205",
"epistemic_graph/generated/query.py": "85db01171e8f4278b841872939cbed7d47d76415b5516b67dd83eaa8e16b3201",
"epistemic_graph/generated/rdf_report.py": "600708e31131a12a37ec4a890490bbd3a0806a16b4961b3a0916d213ef535d49",
Expand All @@ -71,7 +71,7 @@
"epistemic_graph/generated/transactions.py": "83174fd4a8d2032577c60bf61dacf08fbe3b5df8530da66a19d1b36eb9920335",
"epistemic_graph/generated/write_back.py": "cbd29e0308e7e095abf84d5f31a67ce88ea46c69f8f39a95369a03ce82c21df0"
},
"contract_digest": "cca301febcdb7c977970e932b68f48b621f8154df2a0c9594201130efe88bb10",
"contract_digest": "488f3feb732b2389f8f5fc2875f6cc61086d93b28f852bfd0bc9379ebeee504a",
"contract_version": 1,
"feature_profile": [
"contract",
Expand Down
Loading
Loading