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