1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
10pub struct Permission {
11 tenant_id: Option<Uuid>,
14
15 resource_pattern: String,
20
21 resource_id: Option<Uuid>,
24
25 action: String,
28}
29
30impl Permission {
31 #[must_use]
32 pub fn builder() -> PermissionBuilder {
33 PermissionBuilder::default()
34 }
35
36 #[must_use]
37 pub fn tenant_id(&self) -> Option<Uuid> {
38 self.tenant_id
39 }
40
41 #[must_use]
42 pub fn resource_pattern(&self) -> &str {
43 &self.resource_pattern
44 }
45
46 #[must_use]
47 pub fn resource_id(&self) -> Option<Uuid> {
48 self.resource_id
49 }
50
51 #[must_use]
52 pub fn action(&self) -> &str {
53 &self.action
54 }
55}
56
57impl serde::Serialize for Permission {
58 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
59 where
60 S: serde::Serializer,
61 {
62 let tenant_id_str = self
63 .tenant_id
64 .map_or_else(|| "*".to_owned(), |id| id.to_string());
65 let resource_id_str = self
66 .resource_id
67 .map_or_else(|| "*".to_owned(), |id| id.to_string());
68 let s = format!(
69 "{}:{}:{}:{}",
70 tenant_id_str, self.resource_pattern, resource_id_str, self.action
71 );
72 serializer.serialize_str(&s)
73 }
74}
75
76impl<'de> serde::Deserialize<'de> for Permission {
77 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78 where
79 D: serde::Deserializer<'de>,
80 {
81 let s = String::deserialize(deserializer)?;
82 let parts: Vec<&str> = s.splitn(4, ':').collect();
83
84 if parts.len() != 4 {
85 return Err(serde::de::Error::custom(format!(
86 "Expected format 'tenant_id:resource_pattern:resource_id:action', got: {s}"
87 )));
88 }
89
90 let tenant_id = if parts[0] == "*" {
91 None
92 } else {
93 Some(Uuid::parse_str(parts[0]).map_err(serde::de::Error::custom)?)
94 };
95
96 let resource_id = if parts[2] == "*" {
97 None
98 } else {
99 Some(Uuid::parse_str(parts[2]).map_err(serde::de::Error::custom)?)
100 };
101
102 let action = parts[3];
103 if !action
104 .chars()
105 .all(|c| c.is_ascii_alphanumeric() || c == '_')
106 {
107 return Err(serde::de::Error::custom(format!(
108 "Action must contain only alphanumeric characters and underscores, got: {action}"
109 )));
110 }
111
112 Ok(Permission {
113 tenant_id,
114 resource_pattern: parts[1].to_owned(),
115 resource_id,
116 action: action.to_owned(),
117 })
118 }
119}
120
121#[derive(Default)]
123pub struct PermissionBuilder {
124 tenant_id: Option<Uuid>,
125 resource_pattern: Option<String>,
126 resource_id: Option<Uuid>,
127 action: Option<String>,
128}
129
130impl PermissionBuilder {
131 #[must_use]
132 pub fn tenant_id(mut self, tenant_id: Uuid) -> Self {
133 self.tenant_id = Some(tenant_id);
134 self
135 }
136
137 #[must_use]
138 pub fn resource_pattern(mut self, resource_pattern: &str) -> Self {
139 self.resource_pattern = Some(resource_pattern.to_owned());
140 self
141 }
142
143 #[must_use]
144 pub fn resource_id(mut self, resource_id: Uuid) -> Self {
145 self.resource_id = Some(resource_id);
146 self
147 }
148
149 #[must_use]
150 pub fn action(mut self, action: &str) -> Self {
151 self.action = Some(action.to_owned());
152 self
153 }
154
155 pub fn build(self) -> Result<Permission, PermissionBuildError> {
164 let resource_pattern = self
165 .resource_pattern
166 .ok_or(PermissionBuildError::MissingResourcePattern)?;
167
168 let action = self.action.ok_or(PermissionBuildError::MissingAction)?;
169
170 if !action
172 .chars()
173 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '*')
174 {
175 return Err(PermissionBuildError::InvalidAction(action));
176 }
177
178 Ok(Permission {
179 tenant_id: self.tenant_id,
180 resource_pattern,
181 resource_id: self.resource_id,
182 action,
183 })
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum PermissionBuildError {
190 MissingResourcePattern,
191 MissingAction,
192 InvalidAction(String),
193}
194
195impl std::fmt::Display for PermissionBuildError {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 match self {
198 Self::MissingResourcePattern => write!(f, "resource_pattern is required"),
199 Self::MissingAction => write!(f, "action is required"),
200 Self::InvalidAction(action) => write!(
201 f,
202 "Action must contain only alphanumeric characters and underscores, got: {action}"
203 ),
204 }
205 }
206}
207
208impl std::error::Error for PermissionBuildError {}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct Claims {
213 pub issuer: String,
215
216 pub subject: Uuid,
218
219 pub audiences: Vec<String>,
221
222 #[serde(
224 skip_serializing_if = "Option::is_none",
225 with = "time::serde::rfc3339::option"
226 )]
227 pub expires_at: Option<OffsetDateTime>,
228
229 #[serde(
231 skip_serializing_if = "Option::is_none",
232 with = "time::serde::rfc3339::option"
233 )]
234 pub not_before: Option<OffsetDateTime>,
235
236 #[serde(
238 skip_serializing_if = "Option::is_none",
239 with = "time::serde::rfc3339::option"
240 )]
241 pub issued_at: Option<OffsetDateTime>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub jwt_id: Option<String>,
246
247 pub tenant_id: Uuid,
250
251 #[serde(default)]
253 pub permissions: Vec<Permission>,
254
255 #[serde(flatten)]
257 pub extras: serde_json::Map<String, serde_json::Value>,
258}
259
260impl Claims {
261 #[must_use]
263 pub fn is_expired(&self) -> bool {
264 if let Some(exp) = self.expires_at {
265 OffsetDateTime::now_utc() >= exp
266 } else {
267 false
268 }
269 }
270
271 #[must_use]
273 pub fn is_valid_yet(&self) -> bool {
274 if let Some(nbf) = self.not_before {
275 OffsetDateTime::now_utc() >= nbf
276 } else {
277 true
278 }
279 }
280}
281
282#[cfg(test)]
283#[cfg_attr(coverage_nightly, coverage(off))]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_expiration_check() {
289 let mut claims = Claims {
290 issuer: "test".to_owned(),
291 subject: Uuid::new_v4(),
292 audiences: vec!["api".to_owned()],
293 expires_at: Some(OffsetDateTime::now_utc() + time::Duration::hours(1)),
294 not_before: None,
295 issued_at: None,
296 jwt_id: None,
297 tenant_id: Uuid::new_v4(),
298 permissions: vec![],
299 extras: serde_json::Map::new(),
300 };
301
302 assert!(!claims.is_expired());
303
304 claims.expires_at = Some(OffsetDateTime::now_utc() - time::Duration::hours(1));
305 assert!(claims.is_expired());
306 }
307
308 #[test]
309 fn test_nbf_check() {
310 let mut claims = Claims {
311 subject: Uuid::new_v4(),
312 issuer: "test".to_owned(),
313 audiences: vec!["api".to_owned()],
314 expires_at: None,
315 not_before: Some(OffsetDateTime::now_utc() - time::Duration::hours(1)),
316 issued_at: None,
317 jwt_id: None,
318 tenant_id: Uuid::new_v4(),
319 permissions: vec![],
320 extras: serde_json::Map::new(),
321 };
322
323 assert!(claims.is_valid_yet());
324
325 claims.not_before = Some(OffsetDateTime::now_utc() + time::Duration::hours(1));
326 assert!(!claims.is_valid_yet());
327 }
328}