systemprompt_security/authz/types/
decision.rs1use std::borrow::Cow;
7use std::fmt;
8
9use serde::{Deserialize, Serialize};
10use systemprompt_identifiers::{McpToolName, PolicyId, SecretPatternId, UserId};
11use thiserror::Error;
12
13use super::entity_ref::EntityRef;
14use super::kinds::RuleType;
15use crate::policy::types::{AccessScope, RateLimitWindow, SecretLocation};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(tag = "kind", rename_all = "snake_case")]
22pub enum MatchedBy {
23 UserAllow,
24 RoleAllow {
25 role: String,
26 },
27 AttributeAllow {
30 rule_type: RuleType,
31 value: String,
32 },
33 DefaultIncluded,
34 PolicyAllow {
35 policy_id: PolicyId,
36 detail: Cow<'static, str>,
37 },
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
49#[serde(tag = "kind", rename_all = "snake_case")]
50pub enum DenyReason {
51 #[error("user {user_id} explicitly denied for {entity}")]
52 UserDeny {
53 entity: EntityRef,
54 user_id: UserId,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 justification: Option<String>,
57 },
58 #[error("role {role} denied for {entity}")]
59 RoleDeny {
60 entity: EntityRef,
61 role: String,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 justification: Option<String>,
64 },
65 #[error("{rule_type} {value} denied for {entity}")]
69 AttributeDeny {
70 entity: EntityRef,
71 rule_type: RuleType,
72 value: String,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 justification: Option<String>,
75 },
76 #[error(
77 "{entity}: not assigned to user {user_id} with roles {roles:?} (no allow rule; \
78 default_included = false). Add an allow rule in services/access-control/roles.yaml."
79 )]
80 NotAssigned {
81 entity: EntityRef,
82 user_id: UserId,
83 roles: Vec<String>,
84 },
85 #[error(
86 "{entity}: unknown to access control. Add an entity row via the publish pipeline or \
87 roles.yaml."
88 )]
89 UnknownEntity { entity: EntityRef },
90 #[error("authz hook unavailable for policy {policy}")]
91 HookUnavailable { policy: String },
92 #[error("{detail}")]
97 PolicyViolation {
98 policy: String,
99 detail: Cow<'static, str>,
100 },
101 #[error("secret detected: {pattern_name} at {location:?}")]
102 SecretLeak {
103 pattern_id: SecretPatternId,
104 pattern_name: Cow<'static, str>,
105 location: SecretLocation,
106 },
107 #[error("tool {tool} requires {required} scope")]
108 ScopeViolation {
109 tool: McpToolName,
110 required: AccessScope,
111 },
112 #[error("tool {tool} blocked by list {list_id}")]
113 ToolBlocked { tool: McpToolName, list_id: String },
114 #[error("rate limit {window:?} exceeded; retry after {retry_after_ms}ms")]
115 RateLimitExceeded {
116 window: RateLimitWindow,
117 retry_after_ms: u64,
118 },
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(tag = "decision", rename_all = "lowercase")]
123pub enum Decision {
124 Allow { matched_by: MatchedBy },
125 Deny { reason: DenyReason },
126}
127
128impl Decision {
129 #[must_use]
130 pub const fn tag(&self) -> DecisionTag {
131 match self {
132 Self::Allow { .. } => DecisionTag::Allow,
133 Self::Deny { .. } => DecisionTag::Deny,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
145#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
146#[serde(rename_all = "lowercase")]
147pub enum DecisionTag {
148 Allow,
149 Deny,
150}
151
152impl DecisionTag {
153 #[must_use]
154 pub const fn as_str(self) -> &'static str {
155 match self {
156 Self::Allow => "allow",
157 Self::Deny => "deny",
158 }
159 }
160}
161
162impl fmt::Display for DecisionTag {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 f.write_str(self.as_str())
165 }
166}
167
168impl From<&super::request::AuthzDecision> for DecisionTag {
169 fn from(d: &super::request::AuthzDecision) -> Self {
170 match d {
171 super::request::AuthzDecision::Allow => Self::Allow,
172 super::request::AuthzDecision::Deny { .. } => Self::Deny,
173 }
174 }
175}