Improve auth explain diagnostics

This commit is contained in:
Eric Wendland 2026-05-22 14:28:44 +02:00
commit ee80ff780c
7 changed files with 516 additions and 46 deletions

View file

@ -3306,6 +3306,103 @@ fn can_sync_resource(
.allowed)
}
fn explain_auth_for_operator(
store: &Store,
subject: &str,
resource: &str,
capability: &str,
) -> Result<AuthExplanation, NodeError> {
let ops = load_auth_ops_for_resource(store, resource)?;
let mut explanation = geth_auth::explain_auth_ops(
&ops,
PrincipalId::new(subject.to_owned()),
ResourceId::new(resource.to_owned()),
Capability::new(capability.to_owned()),
);
if subject.starts_with("bearer:") {
return Ok(explanation);
}
let peer_card = store.get_peer_card(subject)?;
let keychain_view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(store)?);
let subject_node = NodeId::new(subject.to_owned());
let trusted_node = keychain_view.nodes.get(&subject_node);
match (trusted_node, peer_card) {
(None, Some(_)) => {
explanation.add_diagnostic("subject:discovered-only");
explanation.add_diagnostic("trust:missing");
if !explanation
.reason
.contains("discovery does not grant trust or authorization")
{
explanation.reason = format!(
"subject is a discovered peer candidate only; discovery does not grant trust or authorization; {}",
explanation.reason
);
}
}
(None, None) => {
explanation.add_diagnostic("subject:unknown");
explanation.add_diagnostic("trust:missing");
if !explanation.allowed {
explanation.reason = format!(
"subject is not present in the keychain and has no discovered peer card; {}",
explanation.reason
);
}
}
(Some(node), None) => {
explanation.add_diagnostic("subject:trusted-node");
if node.endpoints.is_empty() {
explanation.add_diagnostic("endpoint-binding:missing");
} else {
explanation.add_diagnostic("endpoint-binding:present");
explanation.add_diagnostic("discovery:missing-peer-card");
}
}
(Some(node), Some(stored_card)) => {
explanation.add_diagnostic("subject:trusted-node");
let card = serde_json::from_str::<PeerCard>(&stored_card.card_json).ok();
let matching_endpoint = card.as_ref().and_then(|card| {
card.endpoints
.iter()
.map(|endpoint| endpoint.endpoint_id.as_str())
.find(|endpoint| {
keychain_view
.endpoints
.get(*endpoint)
.is_some_and(|bound_node| bound_node == &node.id)
})
.map(ToOwned::to_owned)
});
if let Some(endpoint) = matching_endpoint {
explanation.add_diagnostic(format!("endpoint-binding:matched:{endpoint}"));
} else if node.endpoints.is_empty() {
explanation.add_diagnostic("endpoint-binding:missing");
if !explanation.allowed {
explanation.reason = format!(
"trusted node has no active endpoint binding; {}",
explanation.reason
);
}
} else {
explanation.add_diagnostic("endpoint-binding:missing-for-peer-card");
if !explanation.allowed {
explanation.reason = format!(
"discovered peer card has no endpoint candidate bound to the trusted node; {}",
explanation.reason
);
}
}
}
}
Ok(explanation)
}
fn bearer_proof(
bearer_secret: Option<String>,
resource: &str,
@ -3360,6 +3457,7 @@ fn explain_peer_or_bearer(
allowed: false,
reason,
evaluated_ops: peer_explanation.evaluated_ops,
diagnostics: vec!["subject:bearer-secret".to_owned()],
};
if proof.resource != resource_id {
@ -3425,6 +3523,11 @@ fn explain_peer_or_bearer(
"bearer proof allows this resource-scoped capability without granting node identity"
.to_owned(),
evaluated_ops: peer_explanation.evaluated_ops,
diagnostics: vec![
"subject:bearer-secret".to_owned(),
format!("bearer:active:{}", access.secret),
"capability:matched".to_owned(),
],
})
}
@ -6756,35 +6859,12 @@ pub fn handle_request(
subject,
resource,
capability,
} => {
let ops = load_auth_ops_for_resource(&store, &resource)?;
let discovered = store.get_peer_card(&subject)?.is_some();
if ops.is_empty() {
if discovered {
Ok(ControlResponse::AuthExplain(
AuthExplanation::discovered_candidate(subject, resource, capability),
))
} else {
Ok(ControlResponse::AuthExplain(AuthExplanation::stub(
subject, resource, capability,
)))
}
} else {
let mut explanation = geth_auth::explain_auth_ops(
&ops,
PrincipalId::new(subject.clone()),
ResourceId::new(resource.clone()),
Capability::new(capability.clone()),
);
if discovered && !explanation.allowed {
explanation.reason = format!(
"subject is a discovered peer candidate only; discovery does not grant trust or authorization; {}",
explanation.reason
);
}
Ok(ControlResponse::AuthExplain(explanation))
}
}
} => Ok(ControlResponse::AuthExplain(explain_auth_for_operator(
&store,
&subject,
&resource,
&capability,
)?)),
ControlRequest::AuthGrant {
subject,
resource,