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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bxnet-ops"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"org-ops-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
|
|
@ -382,11 +390,14 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "guildhouse-ops"
|
name = "governance-types"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"hex",
|
||||||
"org-ops-core",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha1",
|
||||||
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -429,6 +440,12 @@ version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|
@ -838,11 +855,14 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
|
"governance-types",
|
||||||
|
"hex",
|
||||||
"rand",
|
"rand",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"tempfile",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
@ -1160,6 +1180,17 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ pub mod pkce;
|
||||||
pub mod playbook_commands;
|
pub mod playbook_commands;
|
||||||
pub mod score_fetcher;
|
pub mod score_fetcher;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
pub mod shell_class;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
pub mod gsap_client;
|
pub mod gsap_client;
|
||||||
|
|
||||||
|
|
@ -27,6 +28,7 @@ pub use config::OrgOpsConfig;
|
||||||
pub use playbook_commands::PlaybookCommands;
|
pub use playbook_commands::PlaybookCommands;
|
||||||
pub use display::SessionBanner;
|
pub use display::SessionBanner;
|
||||||
pub use git_commands::{GitConfig, GovernedGitCommands};
|
pub use git_commands::{GitConfig, GovernedGitCommands};
|
||||||
|
pub use shell_class::ShellClass;
|
||||||
pub use traits::{OrgCommands, RiskScorer};
|
pub use traits::{OrgCommands, RiskScorer};
|
||||||
|
|
||||||
/// The main entry point. Build with your config and commands, then call run().
|
/// 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(),
|
org_name: self.config.org_name.clone(),
|
||||||
trust_domain: self.config.trust_domain.clone(),
|
trust_domain: self.config.trust_domain.clone(),
|
||||||
bascule_endpoint: self.config.bascule_endpoint.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 {
|
for cmd in &self.commands {
|
||||||
if cmd.handles(name) {
|
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);
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SessionContext {
|
pub struct SessionContext {
|
||||||
pub org_name: String,
|
pub org_name: String,
|
||||||
pub trust_domain: String,
|
pub trust_domain: String,
|
||||||
pub bascule_endpoint: 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::session::SessionContext;
|
||||||
|
use crate::shell_class::ShellClass;
|
||||||
|
|
||||||
/// Implement this to add org-specific subcommands.
|
/// Implement this to add org-specific subcommands.
|
||||||
pub trait OrgCommands: Send + Sync {
|
pub trait OrgCommands: Send + Sync {
|
||||||
|
|
@ -10,6 +11,15 @@ pub trait OrgCommands: Send + Sync {
|
||||||
matches: &clap::ArgMatches,
|
matches: &clap::ArgMatches,
|
||||||
ctx: &SessionContext,
|
ctx: &SessionContext,
|
||||||
) -> anyhow::Result<()>;
|
) -> 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.
|
/// Implement this for custom risk scoring.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue