bascule-workspace/bascule-agent/src/namespace/identity.rs
Tyler King b1865a0627 initial: bascule v0.1.0
Bascule shell runtime workspace — governed shell access layer
for Substrate/Guildhouse FFC deployments.

Crates:
- bascule-agent: node agent with SSH server + command filtering
- bascule-core: audit, grant engine, ceremony types, session
- bascule-filter-core: log line filtering (stdio protocol)
- bascule-gateway: OIDC auth, session management, SAT validation
- bascule-node-agent: k8s DaemonSet agent (pod watcher, BPF manager)
- bascule-proto: protobuf definitions
- bascule-shell: governed SSH shell (commands, elevation, REPL)
- bascule-tail: chronicle log tail + fanout
- ceremony-engine: ceremony lifecycle (6 types + request/resolution)

172 tests passing.
Implements SBS-SPEC-0001 shell model.
Reference impl for SPEC-SHELLOPS-0001 Layer 1 (root shell).
2026-03-18 16:40:48 -04:00

109 lines
3.7 KiB
Rust

//! IDENTITY namespace (0x0002) — OIDC token validation + authorization.
use async_trait::async_trait;
use crate::shellstream::{identity, ShellstreamResponse};
use super::NamespaceHandler;
pub struct IdentityHandler {
dev_mode: bool,
}
impl IdentityHandler {
pub fn new(dev_mode: bool) -> Self {
Self { dev_mode }
}
}
#[async_trait]
impl NamespaceHandler for IdentityHandler {
async fn handle(
&self,
function_id: u16,
payload: &[u8],
session_id: &[u8; 16],
) -> ShellstreamResponse {
match function_id {
identity::AUTHENTICATE => self.authenticate(payload, session_id).await,
identity::AUTHORIZE => self.authorize(payload, session_id).await,
identity::WHOAMI => self.resolve(payload, session_id).await,
_ => ShellstreamResponse::error(
*session_id,
0,
&format!("Unknown IDENTITY function: {function_id:#06x}"),
),
}
}
}
impl IdentityHandler {
async fn authenticate(&self, payload: &[u8], session_id: &[u8; 16]) -> ShellstreamResponse {
let payload_parsed: serde_json::Value =
rmp_serde::from_slice(payload).unwrap_or(serde_json::Value::Null);
if self.dev_mode {
// Dev mode: accept any token, return synthetic identity
let response = rmp_serde::to_vec(&serde_json::json!({
"subject": "dev-user",
"email": "dev@guildhouse.local",
"issuer": "dev-mode",
"verified": true,
}))
.unwrap_or_default();
return ShellstreamResponse::ok(*session_id, 0, response);
}
// TODO: Validate JWT via OidcAuthProvider (Stage 3 — after moving to bascule-core)
let token = payload_parsed
.get("token")
.and_then(|v| v.as_str())
.unwrap_or("");
if token.is_empty() {
return ShellstreamResponse::denied(*session_id, 0, "No token provided");
}
// Placeholder: accept token and return minimal claims
let response = rmp_serde::to_vec(&serde_json::json!({
"subject": "unknown",
"verified": false,
"message": "OIDC validation not yet implemented",
}))
.unwrap_or_default();
ShellstreamResponse::ok(*session_id, 0, response)
}
async fn authorize(&self, payload: &[u8], session_id: &[u8; 16]) -> ShellstreamResponse {
if self.dev_mode {
let response = rmp_serde::to_vec(&serde_json::json!({
"authorized": true,
}))
.unwrap_or_default();
return ShellstreamResponse::ok(*session_id, 0, response);
}
let payload_parsed: serde_json::Value =
rmp_serde::from_slice(payload).unwrap_or(serde_json::Value::Null);
let subject = payload_parsed.get("subject").and_then(|v| v.as_str()).unwrap_or("");
let resource = payload_parsed.get("resource").and_then(|v| v.as_str()).unwrap_or("");
let action = payload_parsed.get("action").and_then(|v| v.as_str()).unwrap_or("");
tracing::debug!(subject, resource, action, "identity.authorize");
// Default: allow in soft mode
let response = rmp_serde::to_vec(&serde_json::json!({
"authorized": true,
}))
.unwrap_or_default();
ShellstreamResponse::ok(*session_id, 0, response)
}
async fn resolve(&self, _payload: &[u8], session_id: &[u8; 16]) -> ShellstreamResponse {
let response = rmp_serde::to_vec(&serde_json::json!({
"message": "resolve not yet implemented",
}))
.unwrap_or_default();
ShellstreamResponse::ok(*session_id, 0, response)
}
}