systemprompt_security/authz/types/
request.rs1use std::borrow::Cow;
7use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10use systemprompt_identifiers::{
11 Actor, ActorKind, AgentId, ClientId, ContextId, McpToolName, ModelId, SessionId, TaskId,
12 TraceId, UserId,
13};
14
15use super::decision::DenyReason;
16use super::entity_ref::EntityRef;
17use crate::policy::types::AccessScope;
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct AuthzContext {
35 pub kind: Cow<'static, str>,
36 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
37 pub payload: serde_json::Value,
39}
40
41impl Default for AuthzContext {
42 fn default() -> Self {
43 Self::none()
44 }
45}
46
47impl AuthzContext {
48 pub const NONE_KIND: &'static str = "none";
49 pub const GATEWAY_INVOCATION_KIND: &'static str = "gateway.invocation";
50 pub const MCP_TOOL_CALL_KIND: &'static str = "mcp.tool_call";
51
52 #[must_use]
53 pub const fn none() -> Self {
54 Self {
55 kind: Cow::Borrowed(Self::NONE_KIND),
56 payload: serde_json::Value::Null,
57 }
58 }
59
60 #[must_use]
61 pub fn gateway_invocation(model: &ModelId) -> Self {
62 Self {
63 kind: Cow::Borrowed(Self::GATEWAY_INVOCATION_KIND),
64 payload: serde_json::json!({ "model": model.as_str() }),
65 }
66 }
67
68 #[must_use]
69 pub fn mcp_tool_call(tool: &McpToolName) -> Self {
70 Self {
71 kind: Cow::Borrowed(Self::MCP_TOOL_CALL_KIND),
72 payload: serde_json::json!({ "tool": tool.as_str() }),
73 }
74 }
75
76 #[must_use]
77 pub fn extension(kind: impl Into<Cow<'static, str>>, payload: serde_json::Value) -> Self {
79 Self {
80 kind: kind.into(),
81 payload,
82 }
83 }
84
85 #[must_use]
86 pub fn gateway_invocation_model(&self) -> Option<ModelId> {
87 if self.kind != Self::GATEWAY_INVOCATION_KIND {
88 return None;
89 }
90 self.payload
91 .get("model")
92 .and_then(|v| v.as_str())
93 .map(ModelId::new)
94 }
95
96 #[must_use]
97 pub fn mcp_tool_call_tool(&self) -> Option<McpToolName> {
98 if self.kind != Self::MCP_TOOL_CALL_KIND {
99 return None;
100 }
101 self.payload
102 .get("tool")
103 .and_then(|v| v.as_str())
104 .and_then(|tool| McpToolName::try_new(tool).ok())
105 }
106
107 #[must_use]
108 pub fn is_none(&self) -> bool {
109 self.kind == Self::NONE_KIND
110 }
111
112 pub const MARKETPLACE_FLOOR_KEY: &'static str = "marketplace.attribute_floor";
113
114 #[must_use]
115 pub fn with_marketplace_floor(&self, floor: &BTreeMap<String, serde_json::Value>) -> Self {
117 let mut payload = match self.payload.clone() {
118 serde_json::Value::Object(map) => map,
119 _ => serde_json::Map::new(),
120 };
121 let floor_value = floor
122 .iter()
123 .map(|(k, v)| (k.clone(), v.clone()))
124 .collect::<serde_json::Map<String, serde_json::Value>>();
125 payload.insert(
126 Self::MARKETPLACE_FLOOR_KEY.to_owned(),
127 serde_json::Value::Object(floor_value),
128 );
129 Self {
130 kind: self.kind.clone(),
131 payload: serde_json::Value::Object(payload),
132 }
133 }
134
135 #[must_use]
136 pub fn marketplace_floor(&self) -> Option<BTreeMap<String, serde_json::Value>> {
138 let obj = self.payload.get(Self::MARKETPLACE_FLOOR_KEY)?.as_object()?;
139 Some(
140 obj.iter()
141 .map(|(k, v)| (k.clone(), v.clone()))
142 .collect::<BTreeMap<String, serde_json::Value>>(),
143 )
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct AuthzRequest {
160 pub entity: EntityRef,
161 pub user_id: UserId,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub actor: Option<Actor>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub client_id: Option<ClientId>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub access_scope: Option<AccessScope>,
168 #[serde(default)]
169 pub roles: Vec<String>,
170 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
171 pub attributes: BTreeMap<String, serde_json::Value>,
173 pub trace_id: TraceId,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub session_id: Option<SessionId>,
176 #[serde(default)]
177 pub context: AuthzContext,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub context_id: Option<ContextId>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub task_id: Option<TaskId>,
182 #[serde(default, skip_serializing_if = "Vec::is_empty")]
183 pub act_chain: Vec<Actor>,
184}
185
186impl AuthzRequest {
187 #[must_use]
188 pub fn for_actor(mut self, actor: Actor) -> Self {
189 self.user_id = actor.user_id.clone();
190 self.actor = Some(actor);
191 self
192 }
193
194 #[must_use]
195 pub fn actor(&self) -> Actor {
196 self.actor
197 .clone()
198 .unwrap_or_else(|| Actor::user(self.user_id.clone()))
199 }
200
201 #[must_use]
204 pub fn verified_agent_id(&self) -> Option<&AgentId> {
205 match self.act_chain.first().map(|a| &a.kind) {
206 Some(ActorKind::Agent { agent_id }) => Some(agent_id),
207 _ => None,
208 }
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(tag = "decision", rename_all = "lowercase")]
214pub enum AuthzDecision {
215 Allow,
216 Deny { reason: DenyReason, policy: String },
217}