systemprompt_security/authz/types/
request.rs1use std::borrow::Cow;
7use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10use systemprompt_identifiers::{
11 Actor, ActorKind, ClientId, ContextId, McpToolName, ModelId, SessionId, TaskId, TraceId, UserId,
12};
13
14use super::decision::DenyReason;
15use super::entity_ref::EntityRef;
16use crate::policy::types::AccessScope;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct AuthzContext {
34 pub kind: Cow<'static, str>,
35 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
36 pub payload: serde_json::Value,
37}
38
39impl Default for AuthzContext {
40 fn default() -> Self {
41 Self::none()
42 }
43}
44
45impl AuthzContext {
46 pub const NONE_KIND: &'static str = "none";
47 pub const GATEWAY_INVOCATION_KIND: &'static str = "gateway.invocation";
48 pub const MCP_TOOL_CALL_KIND: &'static str = "mcp.tool_call";
49
50 #[must_use]
51 pub const fn none() -> Self {
52 Self {
53 kind: Cow::Borrowed(Self::NONE_KIND),
54 payload: serde_json::Value::Null,
55 }
56 }
57
58 #[must_use]
59 pub fn gateway_invocation(model: &ModelId) -> Self {
60 Self {
61 kind: Cow::Borrowed(Self::GATEWAY_INVOCATION_KIND),
62 payload: serde_json::json!({ "model": model.as_str() }),
63 }
64 }
65
66 #[must_use]
67 pub fn mcp_tool_call(tool: &McpToolName) -> Self {
68 Self {
69 kind: Cow::Borrowed(Self::MCP_TOOL_CALL_KIND),
70 payload: serde_json::json!({ "tool": tool.as_str() }),
71 }
72 }
73
74 #[must_use]
75 pub fn extension(kind: impl Into<Cow<'static, str>>, payload: serde_json::Value) -> Self {
76 Self {
77 kind: kind.into(),
78 payload,
79 }
80 }
81
82 #[must_use]
83 pub fn gateway_invocation_model(&self) -> Option<ModelId> {
84 if self.kind != Self::GATEWAY_INVOCATION_KIND {
85 return None;
86 }
87 self.payload
88 .get("model")
89 .and_then(|v| v.as_str())
90 .map(ModelId::new)
91 }
92
93 #[must_use]
94 pub fn mcp_tool_call_tool(&self) -> Option<McpToolName> {
95 if self.kind != Self::MCP_TOOL_CALL_KIND {
96 return None;
97 }
98 self.payload
99 .get("tool")
100 .and_then(|v| v.as_str())
101 .map(McpToolName::new)
102 }
103
104 #[must_use]
105 pub fn is_none(&self) -> bool {
106 self.kind == Self::NONE_KIND
107 }
108
109 pub const MARKETPLACE_FLOOR_KEY: &'static str = "marketplace.attribute_floor";
110
111 #[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)]
154pub struct AuthzRequest {
155 pub entity: EntityRef,
156 pub user_id: UserId,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub actor: Option<Actor>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub client_id: Option<ClientId>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub access_scope: Option<AccessScope>,
163 #[serde(default)]
164 pub roles: Vec<String>,
165 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
166 pub attributes: BTreeMap<String, serde_json::Value>,
167 pub trace_id: TraceId,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub session_id: Option<SessionId>,
170 #[serde(default)]
171 pub context: AuthzContext,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub context_id: Option<ContextId>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub task_id: Option<TaskId>,
176 #[serde(default, skip_serializing_if = "Vec::is_empty")]
177 pub act_chain: Vec<Actor>,
178}
179
180impl AuthzRequest {
181 #[must_use]
182 pub fn for_actor(mut self, actor: Actor) -> Self {
183 self.user_id = actor.user_id.clone();
184 self.actor = Some(actor);
185 self
186 }
187
188 #[must_use]
189 pub fn actor(&self) -> Actor {
190 self.actor
191 .clone()
192 .unwrap_or_else(|| Actor::user(self.user_id.clone()))
193 }
194
195 #[must_use]
199 pub fn verified_agent_id(&self) -> Option<&str> {
200 match self.act_chain.first().map(|a| &a.kind) {
201 Some(ActorKind::Agent { agent_id }) => Some(agent_id.as_str()),
202 _ => None,
203 }
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(tag = "decision", rename_all = "lowercase")]
209pub enum AuthzDecision {
210 Allow,
211 Deny { reason: DenyReason, policy: String },
212}