feat(bascule-gateway): derive ShellClass at ceremony grant from posture
Read the cluster's operational posture level from the posture-current ConfigMap at ceremony grant time. Derive ShellClass via derive_shell_class() and stamp into the granted SessionScope. - Normal posture (5) -> ShellClass::System - Any DEFCON escalation -> ShellClass::Application - Fail-closed: missing ConfigMap -> Lockdown -> Application - posture_level_at_establishment stored for audit/breach comparison Signed-off-by: Tyler King <tking@guildhouse.dev> Signed-off-by: Tyler J King <tking727@gmail.com>
This commit is contained in:
parent
e28be3335d
commit
1a54cc3877
3 changed files with 66 additions and 1 deletions
|
|
@ -18,6 +18,9 @@ accord-core = { path = "../../guildhouse/services/accord-core" }
|
||||||
accord-opa = { path = "../../guildhouse/services/accord-opa" }
|
accord-opa = { path = "../../guildhouse/services/accord-opa" }
|
||||||
qm-core = { path = "../../guildhouse/services/qm-core" }
|
qm-core = { path = "../../guildhouse/services/qm-core" }
|
||||||
|
|
||||||
|
# Cross-workspace path dep — substrate governance types (for PostureLevel).
|
||||||
|
governance-types = { path = "../../substrate/crates/governance-types" }
|
||||||
|
|
||||||
# Kubernetes
|
# Kubernetes
|
||||||
kube = { workspace = true }
|
kube = { workspace = true }
|
||||||
k8s-openapi = { workspace = true }
|
k8s-openapi = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,23 @@ impl bascule_proto::bascule_v1::bascule_gateway_server::BasculeGateway for Bascu
|
||||||
};
|
};
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
CeremonyResponse::Granted(grant) => {
|
CeremonyResponse::Granted(mut grant) => {
|
||||||
|
// Derive ShellClass from the cluster's current posture level.
|
||||||
|
// Reads posture-current ConfigMap, maps level to PostureLevel,
|
||||||
|
// derives ShellClass, and stamps into the granted scope.
|
||||||
|
let posture_level = read_posture_level().await;
|
||||||
|
let shell_class = bascule_core::derive_shell_class(posture_level, None);
|
||||||
|
grant.granted_scope.shell_class = shell_class;
|
||||||
|
grant.granted_scope.posture_level_at_establishment =
|
||||||
|
Some(posture_level.to_wire());
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
ceremony_id = %grant.ceremony_id,
|
||||||
|
posture_level = ?posture_level,
|
||||||
|
shell_class = %shell_class,
|
||||||
|
"Session shell class derived at ceremony grant"
|
||||||
|
);
|
||||||
|
|
||||||
let session = self
|
let session = self
|
||||||
.session_manager
|
.session_manager
|
||||||
.create_session(&grant)
|
.create_session(&grant)
|
||||||
|
|
@ -403,6 +419,10 @@ fn proto_scope_to_core(proto: &bascule_proto::bascule_v1::SessionScope) -> Sessi
|
||||||
pathways: proto.pathways.iter().map(|p| parse_pathway(p)).collect(),
|
pathways: proto.pathways.iter().map(|p| parse_pathway(p)).collect(),
|
||||||
mutation_budget: proto.mutation_budget,
|
mutation_budget: proto.mutation_budget,
|
||||||
can_delegate: proto.can_delegate,
|
can_delegate: proto.can_delegate,
|
||||||
|
// ShellClass is server-derived from posture, not client-requested.
|
||||||
|
// Set to default here; stamped by the ceremony grant path.
|
||||||
|
shell_class: bascule_core::ShellClass::default(),
|
||||||
|
posture_level_at_establishment: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -497,3 +517,43 @@ fn core_global_to_proto(core: &GlobalScope) -> bascule_proto::bascule_v1::Global
|
||||||
can_view_topology: core.can_view_topology,
|
can_view_topology: core.can_view_topology,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Posture level reader ---
|
||||||
|
|
||||||
|
/// Read the cluster's current operational posture level from the
|
||||||
|
/// `posture-current` ConfigMap. Falls back to `PostureLevel::Lockdown`
|
||||||
|
/// (fail-closed) if the ConfigMap is missing or unreadable.
|
||||||
|
async fn read_posture_level() -> governance_types::PostureLevel {
|
||||||
|
use governance_types::PostureLevel;
|
||||||
|
|
||||||
|
let client = match kube::Client::try_default().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = %e, "kube client init failed for posture read");
|
||||||
|
return PostureLevel::Lockdown;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let namespace = std::env::var("ENFORCEMENT_NAMESPACE")
|
||||||
|
.unwrap_or_else(|_| "guildhouse-infra".into());
|
||||||
|
|
||||||
|
use k8s_openapi::api::core::v1::ConfigMap;
|
||||||
|
use kube::api::Api;
|
||||||
|
|
||||||
|
let api: Api<ConfigMap> = Api::namespaced(client, &namespace);
|
||||||
|
match api.get("posture-current").await {
|
||||||
|
Ok(cm) => {
|
||||||
|
let level_u8: u8 = cm
|
||||||
|
.data
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|d| d.get("level"))
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(1);
|
||||||
|
PostureLevel::from_wire(level_u8).unwrap_or(PostureLevel::Lockdown)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = %e, "posture-current ConfigMap read failed");
|
||||||
|
PostureLevel::Lockdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,8 @@ impl SessionManager {
|
||||||
pathways: vec![ChangePathway::DryRunOnly],
|
pathways: vec![ChangePathway::DryRunOnly],
|
||||||
mutation_budget: Some(0),
|
mutation_budget: Some(0),
|
||||||
can_delegate: false,
|
can_delegate: false,
|
||||||
|
shell_class: bascule_core::ShellClass::default(),
|
||||||
|
posture_level_at_establishment: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue