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