use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Action {
pub id: String,
pub label_key: String,
pub state: ActionState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ActionState {
Allowed,
Forbidden {
verb: String,
resource: String,
namespace: Option<String>,
},
Gated {
reason_key: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Status {
Ok,
Warning {
message_key: String,
},
Error {
message_key: String,
},
ReadOnly {
message_key: String,
},
}
pub fn action_verb(action_id: &str) -> &'static str {
match action_id {
"delete" => "delete",
"scale" | "restart" | "update" | "apply" | "edit" | "cordon" | "drain" | "uncordon" => {
"update"
}
"describe" | "logs" | "exec" | "port-forward" | "diagnose" => "get",
_ => "get",
}
}
pub fn downgrade_forbidden(
action: &mut Action,
verb_allowed: Option<bool>,
resource: &str,
namespace: Option<&str>,
) {
let Some(allowed) = verb_allowed else {
return;
};
if allowed {
return;
}
if !matches!(action.state, ActionState::Forbidden { .. }) {
action.state = ActionState::Forbidden {
verb: action_verb(&action.id).to_string(),
resource: resource.to_string(),
namespace: namespace.map(str::to_string),
};
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn action_verb_maps_read_write_and_delete() {
assert_eq!(action_verb("describe"), "get");
assert_eq!(action_verb("logs"), "get");
assert_eq!(action_verb("exec"), "get");
assert_eq!(action_verb("diagnose"), "get");
assert_eq!(action_verb("scale"), "update");
assert_eq!(action_verb("restart"), "update");
assert_eq!(action_verb("delete"), "delete");
assert_eq!(action_verb("something-new"), "get");
}
#[test]
fn downgrade_allowed_to_forbidden_carries_structured_reason() {
let mut a = Action {
id: "delete".into(),
label_key: "action.delete".into(),
state: ActionState::Allowed,
};
downgrade_forbidden(&mut a, Some(false), "clusters", Some("default"));
assert!(matches!(
a.state,
ActionState::Forbidden { ref verb, ref resource, ref namespace }
if verb == "delete" && resource == "clusters" && namespace.as_deref() == Some("default")
));
}
#[test]
fn downgrade_leaves_allowed_when_preflight_grants() {
let mut a = Action {
id: "describe".into(),
label_key: "action.describe".into(),
state: ActionState::Allowed,
};
downgrade_forbidden(&mut a, Some(true), "clusters", Some("default"));
assert!(matches!(a.state, ActionState::Allowed));
}
#[test]
fn downgrade_leaves_existing_forbidden_untouched() {
let mut a = Action {
id: "describe".into(),
label_key: "action.describe".into(),
state: ActionState::Forbidden {
verb: "get".into(),
resource: "clusters".into(),
namespace: Some("ns".into()),
},
};
downgrade_forbidden(&mut a, Some(false), "clusters", Some("default"));
assert!(matches!(
a.state,
ActionState::Forbidden { ref namespace, .. } if namespace.as_deref() == Some("ns")
));
}
#[test]
fn downgrade_is_a_noop_without_preflight() {
let mut a = Action {
id: "scale".into(),
label_key: "action.scale".into(),
state: ActionState::Allowed,
};
downgrade_forbidden(&mut a, None, "clusters", Some("default"));
assert!(matches!(a.state, ActionState::Allowed));
}
}