systemprompt_security/authz/types/
decision.rs1use std::borrow::Cow;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5use systemprompt_identifiers::{McpToolName, PolicyId, SecretPatternId, UserId};
6use thiserror::Error;
7
8use super::entity_ref::EntityRef;
9use crate::policy::types::{AccessScope, RateLimitWindow, SecretLocation};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(tag = "kind", rename_all = "snake_case")]
16pub enum MatchedBy {
17 UserAllow,
18 RoleAllow {
19 role: String,
20 },
21 DefaultIncluded,
23 PolicyAllow {
25 policy_id: PolicyId,
26 detail: Cow<'static, str>,
27 },
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
39#[serde(tag = "kind", rename_all = "snake_case")]
40pub enum DenyReason {
41 #[error("user {user_id} explicitly denied for {entity}")]
42 UserDeny {
43 entity: EntityRef,
44 user_id: UserId,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 justification: Option<String>,
47 },
48 #[error("role {role} denied for {entity}")]
49 RoleDeny {
50 entity: EntityRef,
51 role: String,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 justification: Option<String>,
54 },
55 #[error(
56 "{entity}: not assigned to user {user_id} with roles {roles:?} (no allow rule; \
57 default_included = false). Add an allow rule in services/access-control/roles.yaml."
58 )]
59 NotAssigned {
60 entity: EntityRef,
61 user_id: UserId,
62 roles: Vec<String>,
63 },
64 #[error(
65 "{entity}: unknown to access control. Add an entity row via the publish pipeline or \
66 roles.yaml."
67 )]
68 UnknownEntity { entity: EntityRef },
69 #[error("authz hook unavailable for policy {policy}")]
70 HookUnavailable { policy: String },
71 #[error("{detail}")]
76 PolicyViolation {
77 policy: String,
78 detail: Cow<'static, str>,
79 },
80 #[error("secret detected: {pattern_name} at {location:?}")]
81 SecretLeak {
82 pattern_id: SecretPatternId,
83 pattern_name: Cow<'static, str>,
84 location: SecretLocation,
85 },
86 #[error("tool {tool} requires {required} scope")]
87 ScopeViolation {
88 tool: McpToolName,
89 required: AccessScope,
90 },
91 #[error("tool {tool} blocked by list {list_id}")]
92 ToolBlocked { tool: McpToolName, list_id: String },
93 #[error("rate limit {window:?} exceeded; retry after {retry_after_ms}ms")]
94 RateLimitExceeded {
95 window: RateLimitWindow,
96 retry_after_ms: u64,
97 },
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(tag = "decision", rename_all = "lowercase")]
102pub enum Decision {
103 Allow { matched_by: MatchedBy },
104 Deny { reason: DenyReason },
105}
106
107impl Decision {
108 #[must_use]
109 pub const fn tag(&self) -> DecisionTag {
110 match self {
111 Self::Allow { .. } => DecisionTag::Allow,
112 Self::Deny { .. } => DecisionTag::Deny,
113 }
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
124#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
125#[serde(rename_all = "lowercase")]
126pub enum DecisionTag {
127 Allow,
128 Deny,
129}
130
131impl DecisionTag {
132 #[must_use]
133 pub const fn as_str(self) -> &'static str {
134 match self {
135 Self::Allow => "allow",
136 Self::Deny => "deny",
137 }
138 }
139}
140
141impl fmt::Display for DecisionTag {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 f.write_str(self.as_str())
144 }
145}
146
147impl From<&super::request::AuthzDecision> for DecisionTag {
148 fn from(d: &super::request::AuthzDecision) -> Self {
149 match d {
150 super::request::AuthzDecision::Allow => Self::Allow,
151 super::request::AuthzDecision::Deny { .. } => Self::Deny,
152 }
153 }
154}