systemprompt_security/policy/
types.rs1use std::fmt;
11use std::str::FromStr;
12use std::sync::Arc;
13
14use serde::{Deserialize, Serialize};
15use systemprompt_identifiers::{McpToolName, PolicyId, SessionId, UserId};
16
17use crate::authz::error::AuthzError;
18use crate::authz::types::Decision;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct SecretLocation {
26 pub kind: String,
27 pub path: String,
28}
29
30impl SecretLocation {
31 pub fn new(kind: impl Into<String>, path: impl Into<String>) -> Self {
32 Self {
33 kind: kind.into(),
34 path: path.into(),
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct RateLimitWindow {
42 pub name: String,
43 pub seconds: u64,
44 pub limit: u64,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(tag = "kind", rename_all = "snake_case")]
52pub enum AgentScope {
53 User { user_id: UserId },
54 System,
55}
56
57impl AgentScope {
58 #[must_use]
59 pub const fn user_id(&self) -> Option<&UserId> {
60 match self {
61 Self::User { user_id } => Some(user_id),
62 Self::System => None,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
80#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
81#[serde(rename_all = "lowercase")]
82pub enum AccessScope {
83 Admin,
84 User,
85 Unknown,
86}
87
88impl AccessScope {
89 #[must_use]
90 pub const fn as_str(self) -> &'static str {
91 match self {
92 Self::Admin => "admin",
93 Self::User => "user",
94 Self::Unknown => "unknown",
95 }
96 }
97}
98
99impl fmt::Display for AccessScope {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.write_str(self.as_str())
102 }
103}
104
105impl FromStr for AccessScope {
106 type Err = AuthzError;
107
108 fn from_str(s: &str) -> Result<Self, Self::Err> {
109 match s {
110 "admin" => Ok(Self::Admin),
111 "user" => Ok(Self::User),
112 "unknown" | "" => Ok(Self::Unknown),
113 other => Err(AuthzError::Validation(format!(
114 "unknown access scope: {other}"
115 ))),
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(transparent)]
128pub struct McpToolInput(
129 serde_json::Value,
132);
133
134impl McpToolInput {
135 #[must_use]
136 pub const fn new(value: serde_json::Value) -> Self {
137 Self(value)
138 }
139
140 #[must_use]
141 pub const fn as_value(&self) -> &serde_json::Value {
142 &self.0
143 }
144
145 #[must_use]
146 pub fn as_str(&self, field: &str) -> Option<&str> {
147 self.0.get(field).and_then(serde_json::Value::as_str)
148 }
149
150 #[must_use]
151 pub fn as_path(&self, field: &str) -> Option<&str> {
152 self.as_str(field)
153 }
154}
155
156#[derive(Debug)]
159pub struct PolicyContext<'a> {
160 pub tool: McpToolName,
161 pub agent_scope: AgentScope,
162 pub access_scope: AccessScope,
163 pub session_id: &'a SessionId,
164 pub user_id: &'a UserId,
165 pub tool_input: &'a McpToolInput,
166}
167
168pub trait GovernancePolicy: Send + Sync + fmt::Debug {
174 fn id(&self) -> PolicyId;
175 fn name(&self) -> &'static str;
176 fn description(&self) -> &'static str;
177 fn evaluate(&self, ctx: &PolicyContext<'_>) -> Decision;
178}
179
180#[derive(Debug, Clone, Default)]
182pub struct GovernanceChain {
183 entries: Vec<Arc<dyn GovernancePolicy>>,
184}
185
186impl GovernanceChain {
187 #[must_use]
188 pub const fn new(entries: Vec<Arc<dyn GovernancePolicy>>) -> Self {
189 Self { entries }
190 }
191
192 pub fn push(&mut self, policy: Arc<dyn GovernancePolicy>) {
193 self.entries.push(policy);
194 }
195
196 #[must_use]
197 pub fn entries(&self) -> &[Arc<dyn GovernancePolicy>] {
198 &self.entries
199 }
200
201 #[must_use]
205 pub fn evaluate(&self, ctx: &PolicyContext<'_>) -> Decision {
206 for policy in &self.entries {
207 if let deny @ Decision::Deny { .. } = policy.evaluate(ctx) {
208 return deny;
209 }
210 }
211 Decision::Allow {
212 matched_by: crate::authz::types::MatchedBy::DefaultIncluded,
213 }
214 }
215}