use gwk_domain::envelope::{Actor, CommandEnvelope};
use sqlx::PgConnection;
use crate::project::Refusal;
const RISK_CLASS: &[(&str, &str)] = &[
("issue_command", "stop"),
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Decision {
Unclassified,
Allow { action_class: &'static str },
Page { action_class: &'static str },
}
impl Decision {
pub(crate) fn action_class(&self) -> Option<&'static str> {
match self {
Self::Unclassified => None,
Self::Allow { action_class } | Self::Page { action_class } => Some(action_class),
}
}
}
pub(crate) fn class_of(command_type: &str) -> Option<&'static str> {
RISK_CLASS
.iter()
.find(|(name, _)| *name == command_type)
.map(|(_, class)| *class)
}
pub(crate) async fn evaluate(
conn: &mut PgConnection,
envelope: &CommandEnvelope,
command_type: &str,
subject: &str,
) -> Result<Decision, Refusal> {
let Some(action_class) = class_of(command_type) else {
return Ok(Decision::Unclassified);
};
let allowed = matching_grant(conn, &envelope.actor, action_class, subject, envelope).await?;
Ok(if allowed {
Decision::Allow { action_class }
} else {
Decision::Page { action_class }
})
}
async fn matching_grant(
conn: &mut PgConnection,
actor: &Actor,
action_class: &str,
subject: &str,
envelope: &CommandEnvelope,
) -> Result<bool, Refusal> {
let found: Option<String> = sqlx::query_scalar(
"SELECT id FROM gwk.authority_grant \
WHERE action_class = $1 \
AND grantee = $2::jsonb \
AND (scope IS NULL OR scope = $3) \
AND revoked_at IS NULL \
AND (expires_at IS NULL OR expires_at > $4::timestamptz) \
LIMIT 1",
)
.bind(action_class)
.bind(
serde_json::to_value(actor)
.map_err(|e| Refusal::storage(format!("serialize actor: {e}")))?,
)
.bind(subject)
.bind(envelope.issued_at.as_str())
.fetch_optional(conn)
.await
.map_err(|e| Refusal::storage(format!("read authority grants: {e}")))?;
Ok(found.is_some())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_the_named_commands_are_gated() {
assert_eq!(class_of("issue_command"), Some("stop"));
assert_eq!(class_of("create_task"), None);
assert_eq!(class_of("transition_attempt"), None);
}
#[test]
fn the_bootstrap_commands_are_not_gated() {
assert_eq!(class_of("grant_authority"), None);
assert_eq!(class_of("revoke_authority"), None);
assert_eq!(class_of("activate_kernel"), None);
}
#[test]
fn an_unclassified_decision_carries_no_class() {
assert_eq!(Decision::Unclassified.action_class(), None);
assert_eq!(
Decision::Page {
action_class: "stop"
}
.action_class(),
Some("stop")
);
}
}