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)]
31#[serde(transparent)]
32pub struct RuleType(Cow<'static, str>);
33
34impl RuleType {
35 pub const USER: Self = Self(Cow::Borrowed("user"));
36 pub const ROLE: Self = Self(Cow::Borrowed("role"));
37
38 pub fn extension(slug: impl Into<Cow<'static, str>>) -> Result<Self, AuthzError> {
39 let slug = slug.into();
40 let well_formed = !slug.is_empty()
41 && !slug.starts_with('_')
42 && !slug.ends_with('_')
43 && slug
44 .chars()
45 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
46 if !well_formed || slug == Self::USER.as_str() || slug == Self::ROLE.as_str() {
47 return Err(AuthzError::InvalidRuleType(slug.into_owned()));
48 }
49 Ok(Self(slug))
50 }
51
52 #[must_use]
53 pub fn as_str(&self) -> &str {
54 &self.0
55 }
56}
57
58impl fmt::Display for RuleType {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str(self.as_str())
61 }
62}
63
64impl From<&str> for RuleType {
65 fn from(s: &str) -> Self {
66 match s {
67 "user" => Self::USER,
68 "role" => Self::ROLE,
69 other => Self(Cow::Owned(other.to_owned())),
70 }
71 }
72}
73
74impl sqlx::Type<sqlx::Postgres> for RuleType {
75 fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
76 <str as sqlx::Type<sqlx::Postgres>>::type_info()
77 }
78
79 fn compatible(ty: &<sqlx::Postgres as sqlx::Database>::TypeInfo) -> bool {
80 <str as sqlx::Type<sqlx::Postgres>>::compatible(ty)
81 }
82}
83
84impl<'q> sqlx::Encode<'q, sqlx::Postgres> for RuleType {
85 fn encode_by_ref(
86 &self,
87 buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer,
88 ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
89 <&str as sqlx::Encode<'q, sqlx::Postgres>>::encode(self.as_str(), buf)
90 }
91}
92
93impl<'r> sqlx::Decode<'r, sqlx::Postgres> for RuleType {
94 fn decode(
95 value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
96 ) -> Result<Self, sqlx::error::BoxDynError> {
97 let raw = <&str as sqlx::Decode<'r, sqlx::Postgres>>::decode(value)?;
98 Ok(Self::from(raw))
99 }
100}
101
102#[derive(
103 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, sqlx::Type,
104)]
105#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
106#[serde(rename_all = "lowercase")]
107pub enum Access {
108 Allow,
109 Deny,
110}
111
112impl fmt::Display for Access {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 f.write_str(match *self {
115 Self::Allow => "allow",
116 Self::Deny => "deny",
117 })
118 }
119}
120
121impl FromStr for Access {
122 type Err = AuthzError;
123
124 fn from_str(s: &str) -> Result<Self, Self::Err> {
125 match s {
126 "allow" => Ok(Self::Allow),
127 "deny" => Ok(Self::Deny),
128 other => Err(AuthzError::InvalidAccess(other.to_owned())),
129 }
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum EntityKind {
136 GatewayRoute,
137 McpServer,
138 Plugin,
139 Agent,
140 Marketplace,
141 Skill,
142 Hook,
143 SlackWorkspace,
144 SlackChannel,
145 TeamsTenant,
146 TeamsConversation,
147}
148
149impl EntityKind {
150 pub const ALL: &'static [Self] = &[
151 Self::GatewayRoute,
152 Self::McpServer,
153 Self::Plugin,
154 Self::Agent,
155 Self::Marketplace,
156 Self::Skill,
157 Self::Hook,
158 Self::SlackWorkspace,
159 Self::SlackChannel,
160 Self::TeamsTenant,
161 Self::TeamsConversation,
162 ];
163
164 pub const fn as_str(self) -> &'static str {
165 match self {
166 Self::GatewayRoute => "gateway_route",
167 Self::McpServer => "mcp_server",
168 Self::Plugin => "plugin",
169 Self::Agent => "agent",
170 Self::Marketplace => "marketplace",
171 Self::Skill => "skill",
172 Self::Hook => "hook",
173 Self::SlackWorkspace => "slack_workspace",
174 Self::SlackChannel => "slack_channel",
175 Self::TeamsTenant => "teams_tenant",
176 Self::TeamsConversation => "teams_conversation",
177 }
178 }
179}
180
181impl FromStr for EntityKind {
182 type Err = AuthzError;
183
184 fn from_str(s: &str) -> Result<Self, Self::Err> {
185 match s {
186 "gateway_route" => Ok(Self::GatewayRoute),
187 "mcp_server" => Ok(Self::McpServer),
188 "plugin" => Ok(Self::Plugin),
189 "agent" => Ok(Self::Agent),
190 "marketplace" => Ok(Self::Marketplace),
191 "skill" => Ok(Self::Skill),
192 "hook" => Ok(Self::Hook),
193 "slack_workspace" => Ok(Self::SlackWorkspace),
194 "slack_channel" => Ok(Self::SlackChannel),
195 "teams_tenant" => Ok(Self::TeamsTenant),
196 "teams_conversation" => Ok(Self::TeamsConversation),
197 other => Err(AuthzError::Validation(format!(
198 "unknown entity_type: {other}"
199 ))),
200 }
201 }
202}
203
204impl fmt::Display for EntityKind {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 f.write_str(self.as_str())
207 }
208}
209
210impl sqlx::Type<sqlx::Postgres> for EntityKind {
211 fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
212 <str as sqlx::Type<sqlx::Postgres>>::type_info()
213 }
214
215 fn compatible(ty: &<sqlx::Postgres as sqlx::Database>::TypeInfo) -> bool {
216 <str as sqlx::Type<sqlx::Postgres>>::compatible(ty)
217 }
218}
219
220impl<'q> sqlx::Encode<'q, sqlx::Postgres> for EntityKind {
221 fn encode_by_ref(
222 &self,
223 buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer,
224 ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
225 <&str as sqlx::Encode<'q, sqlx::Postgres>>::encode(self.as_str(), buf)
226 }
227}
228
229impl<'r> sqlx::Decode<'r, sqlx::Postgres> for EntityKind {
230 fn decode(
231 value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
232 ) -> Result<Self, sqlx::error::BoxDynError> {
233 let raw = <&str as sqlx::Decode<'r, sqlx::Postgres>>::decode(value)?;
234 Ok(Self::from_str(raw)?)
235 }
236}