feat(org-ops): enforce ShellClass at command dispatch
Add required_shell_class() to OrgCommands trait with Application default (backward compatible). GSH dispatch checks session ShellClass against command requirement before execution. - ShellClass enum (local, lightweight — avoids bascule-core dep) - SessionContext enriched with shell_class and posture_level - Clear error on insufficient shell class directs operator to reconnect via attested host for System access - All existing commands work unchanged (Application default) Signed-off-by: Tyler King <tking@guildhouse.dev> Signed-off-by: Tyler J King <tking727@gmail.com>
This commit is contained in:
parent
20286ce0d8
commit
323617d6cc
5 changed files with 148 additions and 3 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
|
@ -91,6 +91,14 @@ version = "3.20.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bxnet-ops"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"org-ops-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
|
|
@ -382,11 +390,14 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "guildhouse-ops"
|
||||
name = "governance-types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"org-ops-core",
|
||||
"hex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -429,6 +440,12 @@ version = "0.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
|
|
@ -838,11 +855,14 @@ dependencies = [
|
|||
"anyhow",
|
||||
"base64",
|
||||
"clap",
|
||||
"governance-types",
|
||||
"hex",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
]
|
||||
|
|
@ -1160,6 +1180,17 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub mod pkce;
|
|||
pub mod playbook_commands;
|
||||
pub mod score_fetcher;
|
||||
pub mod session;
|
||||
pub mod shell_class;
|
||||
pub mod traits;
|
||||
pub mod gsap_client;
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ pub use config::OrgOpsConfig;
|
|||
pub use playbook_commands::PlaybookCommands;
|
||||
pub use display::SessionBanner;
|
||||
pub use git_commands::{GitConfig, GovernedGitCommands};
|
||||
pub use shell_class::ShellClass;
|
||||
pub use traits::{OrgCommands, RiskScorer};
|
||||
|
||||
/// The main entry point. Build with your config and commands, then call run().
|
||||
|
|
@ -88,9 +90,22 @@ impl OrgOps {
|
|||
org_name: self.config.org_name.clone(),
|
||||
trust_domain: self.config.trust_domain.clone(),
|
||||
bascule_endpoint: self.config.bascule_endpoint.clone(),
|
||||
shell_class: shell_class::ShellClass::default(),
|
||||
posture_level: 5, // Normal — default for local/unconnected
|
||||
};
|
||||
for cmd in &self.commands {
|
||||
if cmd.handles(name) {
|
||||
let required = cmd.required_shell_class();
|
||||
if !ctx.shell_class.satisfies(required) {
|
||||
anyhow::bail!(
|
||||
"Command '{}' requires {} shell, but session is {}. \
|
||||
Reconnect to a host with attested posture \
|
||||
(TPM + IMA verified) to get a System session.",
|
||||
name,
|
||||
required,
|
||||
ctx.shell_class
|
||||
);
|
||||
}
|
||||
return cmd.handle(name, sub, &ctx);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
// Copyright 2026 Guildhouse Dev
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::shell_class::ShellClass;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionContext {
|
||||
pub org_name: String,
|
||||
pub trust_domain: String,
|
||||
pub bascule_endpoint: String,
|
||||
/// Shell class for this session (Application or System).
|
||||
/// Populated from Bascule SessionScope when connected via gsh.
|
||||
/// Defaults to Application for local/unconnected usage.
|
||||
pub shell_class: ShellClass,
|
||||
/// Posture level wire value (1-5) at session establishment.
|
||||
/// Defaults to 5 (Normal) for local/unconnected usage.
|
||||
pub posture_level: u8,
|
||||
}
|
||||
|
|
|
|||
77
org-ops-core/src/shell_class.rs
Normal file
77
org-ops-core/src/shell_class.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2026 Guildhouse Dev
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Shell class for org-ops dispatch enforcement.
|
||||
//!
|
||||
//! Mirrors `bascule_core::ShellClass` without pulling in the full
|
||||
//! bascule-core dependency tree. The canonical definition lives in
|
||||
//! bascule-core; this is a lightweight duplicate for the CLI layer.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Classification of a governed shell session.
|
||||
///
|
||||
/// - `Application` — software operations (deploy, query, playbooks).
|
||||
/// - `System` — host operations (kernel, firmware, network config).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ShellClass {
|
||||
Application,
|
||||
System,
|
||||
}
|
||||
|
||||
impl Default for ShellClass {
|
||||
fn default() -> Self {
|
||||
ShellClass::Application
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ShellClass {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ShellClass::Application => write!(f, "application"),
|
||||
ShellClass::System => write!(f, "system"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellClass {
|
||||
/// Returns true if `required` is satisfied by `self`.
|
||||
///
|
||||
/// System satisfies both System and Application requirements.
|
||||
/// Application satisfies only Application requirements.
|
||||
pub fn satisfies(&self, required: ShellClass) -> bool {
|
||||
match (self, required) {
|
||||
(ShellClass::System, _) => true,
|
||||
(ShellClass::Application, ShellClass::Application) => true,
|
||||
(ShellClass::Application, ShellClass::System) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_application() {
|
||||
assert_eq!(ShellClass::default(), ShellClass::Application);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_satisfies_both() {
|
||||
assert!(ShellClass::System.satisfies(ShellClass::System));
|
||||
assert!(ShellClass::System.satisfies(ShellClass::Application));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_only_satisfies_application() {
|
||||
assert!(ShellClass::Application.satisfies(ShellClass::Application));
|
||||
assert!(!ShellClass::Application.satisfies(ShellClass::System));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
assert_eq!(format!("{}", ShellClass::Application), "application");
|
||||
assert_eq!(format!("{}", ShellClass::System), "system");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::session::SessionContext;
|
||||
use crate::shell_class::ShellClass;
|
||||
|
||||
/// Implement this to add org-specific subcommands.
|
||||
pub trait OrgCommands: Send + Sync {
|
||||
|
|
@ -10,6 +11,15 @@ pub trait OrgCommands: Send + Sync {
|
|||
matches: &clap::ArgMatches,
|
||||
ctx: &SessionContext,
|
||||
) -> anyhow::Result<()>;
|
||||
|
||||
/// Minimum shell class required to execute commands from this module.
|
||||
///
|
||||
/// Default: `Application` — backward compatible, all existing modules
|
||||
/// work without changes. Override to `System` for modules that perform
|
||||
/// host-level operations (kernel, firmware, network config).
|
||||
fn required_shell_class(&self) -> ShellClass {
|
||||
ShellClass::Application
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement this for custom risk scoring.
|
||||
|
|
|
|||
Loading…
Reference in a new issue