systemprompt_security/authz/types/
request.rs1use std::borrow::Cow;
2use std::collections::BTreeMap;
3
4use serde::{Deserialize, Serialize};
5use systemprompt_identifiers::{Actor, McpToolName, ModelId, SessionId, TraceId, UserId};
6
7use super::decision::DenyReason;
8use super::entity_ref::EntityRef;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct AuthzContext {
26 pub kind: Cow<'static, str>,
27 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
28 pub payload: serde_json::Value,
29}
30
31impl Default for AuthzContext {
32 fn default() -> Self {
33 Self::none()
34 }
35}
36
37impl AuthzContext {
38 pub const NONE_KIND: &'static str = "none";
39 pub const GATEWAY_INVOCATION_KIND: &'static str = "gateway.invocation";
40 pub const MCP_TOOL_CALL_KIND: &'static str = "mcp.tool_call";
41
42 #[must_use]
43 pub const fn none() -> Self {
44 Self {
45 kind: Cow::Borrowed(Self::NONE_KIND),
46 payload: serde_json::Value::Null,
47 }
48 }
49
50 #[must_use]
51 pub fn gateway_invocation(model: &ModelId) -> Self {
52 Self {
53 kind: Cow::Borrowed(Self::GATEWAY_INVOCATION_KIND),
54 payload: serde_json::json!({ "model": model.as_str() }),
55 }
56 }
57
58 #[must_use]
59 pub fn mcp_tool_call(tool: &McpToolName) -> Self {
60 Self {
61 kind: Cow::Borrowed(Self::MCP_TOOL_CALL_KIND),
62 payload: serde_json::json!({ "tool": tool.as_str() }),
63 }
64 }
65
66 #[must_use]
69 pub fn extension(kind: impl Into<Cow<'static, str>>, payload: serde_json::Value) -> Self {
70 Self {
71 kind: kind.into(),
72 payload,
73 }
74 }
75
76 #[must_use]
77 pub fn gateway_invocation_model(&self) -> Option<ModelId> {
78 if self.kind != Self::GATEWAY_INVOCATION_KIND {
79 return None;
80 }
81 self.payload
82 .get("model")
83 .and_then(|v| v.as_str())
84 .map(ModelId::new)
85 }
86
87 #[must_use]
88 pub fn mcp_tool_call_tool(&self) -> Option<McpToolName> {
89 if self.kind != Self::MCP_TOOL_CALL_KIND {
90 return None;
91 }
92 self.payload
93 .get("tool")
94 .and_then(|v| v.as_str())
95 .map(McpToolName::new)
96 }
97
98 #[must_use]
99 pub fn is_none(&self) -> bool {
100 self.kind == Self::NONE_KIND
101 }
102
103 pub const MARKETPLACE_FLOOR_KEY: &'static str = "marketplace.attribute_floor";
104
105 #[must_use]
112 pub fn with_marketplace_floor(&self, floor: &BTreeMap<String, serde_json::Value>) -> Self {
113 let mut payload = match self.payload.clone() {
114 serde_json::Value::Object(map) => map,
115 _ => serde_json::Map::new(),
116 };
117 let floor_value = floor
118 .iter()
119 .map(|(k, v)| (k.clone(), v.clone()))
120 .collect::<serde_json::Map<String, serde_json::Value>>();
121 payload.insert(
122 Self::MARKETPLACE_FLOOR_KEY.to_owned(),
123 serde_json::Value::Object(floor_value),
124 );
125 Self {
126 kind: self.kind.clone(),
127 payload: serde_json::Value::Object(payload),
128 }
129 }
130
131 #[must_use]
132 pub fn marketplace_floor(&self) -> Option<BTreeMap<String, serde_json::Value>> {
133 let obj = self.payload.get(Self::MARKETPLACE_FLOOR_KEY)?.as_object()?;
134 Some(
135 obj.iter()
136 .map(|(k, v)| (k.clone(), v.clone()))
137 .collect::<BTreeMap<String, serde_json::Value>>(),
138 )
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct AuthzRequest {
144 pub entity: EntityRef,
145 pub user_id: UserId,
146 #[serde(default)]
147 pub roles: Vec<String>,
148 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
152 pub attributes: BTreeMap<String, serde_json::Value>,
153 pub trace_id: TraceId,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub session_id: Option<SessionId>,
160 #[serde(default)]
161 pub context: AuthzContext,
162 #[serde(default, skip_serializing_if = "Vec::is_empty")]
166 pub act_chain: Vec<Actor>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(tag = "decision", rename_all = "lowercase")]
171pub enum AuthzDecision {
172 Allow,
173 Deny { reason: DenyReason, policy: String },
174}