systemprompt_security/authz/types/
kinds.rs1use std::borrow::Cow;
7use std::fmt;
8use std::str::FromStr;
9
10use serde::{Deserialize, Serialize};
11
12use crate::authz::error::AuthzError;
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
32#[serde(transparent)]
33pub struct RuleType(Cow<'static, str>);
34
35impl RuleType {
36 pub const USER: Self = Self(Cow::Borrowed("user"));
37 pub const ROLE: Self = Self(Cow::Borrowed("role"));
38
39 pub fn extension(slug: impl Into<Cow<'static, str>>) -> Result<Self, AuthzError> {
40 let slug = slug.into();
41 if !is_extension_slug(slug.as_bytes()) {
42 return Err(AuthzError::InvalidRuleType(slug.into_owned()));
43 }
44 Ok(Self(slug))
45 }
46
47 #[must_use]
48 pub const fn extension_static(slug: &'static str) -> Self {
49 assert!(
50 is_extension_slug(slug.as_bytes()),
51 "extension rule type must be [a-z0-9_], not edged by `_`, and not `user` or `role`"
52 );
53 Self(Cow::Borrowed(slug))
54 }
55
56 #[must_use]
57 pub fn as_str(&self) -> &str {
58 &self.0
59 }
60}
61
62const fn is_extension_slug(slug: &[u8]) -> bool {
63 if matches!(slug, [] | [b'_', ..] | [.., b'_'])
64 || bytes_eq(slug, b"user")
65 || bytes_eq(slug, b"role")
66 {
67 return false;
68 }
69 let mut rest = slug;
70 while let [byte, tail @ ..] = rest {
71 if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'_') {
72 return false;
73 }
74 rest = tail;
75 }
76 true
77}
78
79const fn bytes_eq(left: &[u8], right: &[u8]) -> bool {
80 if left.len() != right.len() {
81 return false;
82 }
83 let (mut left, mut right) = (left, right);
84 while let ([a, left_tail @ ..], [b, right_tail @ ..]) = (left, right) {
85 if *a != *b {
86 return false;
87 }
88 left = left_tail;
89 right = right_tail;
90 }
91 true
92}
93
94impl fmt::Display for RuleType {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(self.as_str())
97 }
98}
99
100impl From<&str> for RuleType {
101 fn from(s: &str) -> Self {
102 match s {
103 "user" => Self::USER,
104 "role" => Self::ROLE,
105 other => Self(Cow::Owned(other.to_owned())),
106 }
107 }
108}
109
110impl sqlx::Type<sqlx::Postgres> for RuleType {
111 fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
112 <str as sqlx::Type<sqlx::Postgres>>::type_info()
113 }
114
115 fn compatible(ty: &<sqlx::Postgres as sqlx::Database>::TypeInfo) -> bool {
116 <str as sqlx::Type<sqlx::Postgres>>::compatible(ty)
117 }
118}
119
120impl<'q> sqlx::Encode<'q, sqlx::Postgres> for RuleType {
121 fn encode_by_ref(
122 &self,
123 buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer,
124 ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
125 <&str as sqlx::Encode<'q, sqlx::Postgres>>::encode(self.as_str(), buf)
126 }
127}
128
129impl<'r> sqlx::Decode<'r, sqlx::Postgres> for RuleType {
130 fn decode(
131 value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
132 ) -> Result<Self, sqlx::error::BoxDynError> {
133 let raw = <&str as sqlx::Decode<'r, sqlx::Postgres>>::decode(value)?;
134 Ok(Self::from(raw))
135 }
136}
137
138#[derive(
139 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, sqlx::Type,
140)]
141#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
142#[serde(rename_all = "lowercase")]
143pub enum Access {
144 Allow,
145 Deny,
146}
147
148impl fmt::Display for Access {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.write_str(match *self {
151 Self::Allow => "allow",
152 Self::Deny => "deny",
153 })
154 }
155}
156
157impl FromStr for Access {
158 type Err = AuthzError;
159
160 fn from_str(s: &str) -> Result<Self, Self::Err> {
161 match s {
162 "allow" => Ok(Self::Allow),
163 "deny" => Ok(Self::Deny),
164 other => Err(AuthzError::InvalidAccess(other.to_owned())),
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
170#[serde(rename_all = "snake_case")]
171pub enum EntityKind {
172 GatewayRoute,
173 McpServer,
174 Plugin,
175 Agent,
176 Marketplace,
177 Skill,
178 Hook,
179 SlackWorkspace,
180 SlackChannel,
181 TeamsTenant,
182 TeamsConversation,
183}
184
185impl EntityKind {
186 pub const ALL: &'static [Self] = &[
187 Self::GatewayRoute,
188 Self::McpServer,
189 Self::Plugin,
190 Self::Agent,
191 Self::Marketplace,
192 Self::Skill,
193 Self::Hook,
194 Self::SlackWorkspace,
195 Self::SlackChannel,
196 Self::TeamsTenant,
197 Self::TeamsConversation,
198 ];
199
200 pub const fn as_str(self) -> &'static str {
201 match self {
202 Self::GatewayRoute => "gateway_route",
203 Self::McpServer => "mcp_server",
204 Self::Plugin => "plugin",
205 Self::Agent => "agent",
206 Self::Marketplace => "marketplace",
207 Self::Skill => "skill",
208 Self::Hook => "hook",
209 Self::SlackWorkspace => "slack_workspace",
210 Self::SlackChannel => "slack_channel",
211 Self::TeamsTenant => "teams_tenant",
212 Self::TeamsConversation => "teams_conversation",
213 }
214 }
215}
216
217impl FromStr for EntityKind {
218 type Err = AuthzError;
219
220 fn from_str(s: &str) -> Result<Self, Self::Err> {
221 match s {
222 "gateway_route" => Ok(Self::GatewayRoute),
223 "mcp_server" => Ok(Self::McpServer),
224 "plugin" => Ok(Self::Plugin),
225 "agent" => Ok(Self::Agent),
226 "marketplace" => Ok(Self::Marketplace),
227 "skill" => Ok(Self::Skill),
228 "hook" => Ok(Self::Hook),
229 "slack_workspace" => Ok(Self::SlackWorkspace),
230 "slack_channel" => Ok(Self::SlackChannel),
231 "teams_tenant" => Ok(Self::TeamsTenant),
232 "teams_conversation" => Ok(Self::TeamsConversation),
233 other => Err(AuthzError::UnknownEntityKind(other.to_owned())),
234 }
235 }
236}
237
238impl fmt::Display for EntityKind {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 f.write_str(self.as_str())
241 }
242}
243
244impl sqlx::Type<sqlx::Postgres> for EntityKind {
245 fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
246 <str as sqlx::Type<sqlx::Postgres>>::type_info()
247 }
248
249 fn compatible(ty: &<sqlx::Postgres as sqlx::Database>::TypeInfo) -> bool {
250 <str as sqlx::Type<sqlx::Postgres>>::compatible(ty)
251 }
252}
253
254impl<'q> sqlx::Encode<'q, sqlx::Postgres> for EntityKind {
255 fn encode_by_ref(
256 &self,
257 buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer,
258 ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
259 <&str as sqlx::Encode<'q, sqlx::Postgres>>::encode(self.as_str(), buf)
260 }
261}
262
263impl<'r> sqlx::Decode<'r, sqlx::Postgres> for EntityKind {
264 fn decode(
265 value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
266 ) -> Result<Self, sqlx::error::BoxDynError> {
267 let raw = <&str as sqlx::Decode<'r, sqlx::Postgres>>::decode(value)?;
268 Ok(Self::from_str(raw)?)
269 }
270}