1use uuid::Uuid;
2
3#[derive(Debug, Clone)]
11pub struct Permission {
12 tenant_id: Option<Uuid>,
15
16 resource_pattern: String,
21
22 resource_id: Option<Uuid>,
25
26 action: String,
29}
30
31impl serde::Serialize for Permission {
32 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
33 where
34 S: serde::Serializer,
35 {
36 let tenant_id_str = self
37 .tenant_id
38 .map_or_else(|| "*".to_owned(), |id| id.to_string());
39 let resource_id_str = self
40 .resource_id
41 .map_or_else(|| "*".to_owned(), |id| id.to_string());
42 let s = format!(
43 "{}:{}:{}:{}",
44 tenant_id_str, self.resource_pattern, resource_id_str, self.action
45 );
46 serializer.serialize_str(&s)
47 }
48}
49
50impl<'de> serde::Deserialize<'de> for Permission {
51 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52 where
53 D: serde::Deserializer<'de>,
54 {
55 let s = String::deserialize(deserializer)?;
56 let parts: Vec<&str> = s.splitn(4, ':').collect();
57
58 if parts.len() != 4 {
59 return Err(serde::de::Error::custom(format!(
60 "Expected format 'tenant_id:resource_pattern:resource_id:action', got: {s}"
61 )));
62 }
63
64 let tenant_id = if parts[0] == "*" {
65 None
66 } else {
67 Some(Uuid::parse_str(parts[0]).map_err(serde::de::Error::custom)?)
68 };
69
70 let resource_id = if parts[2] == "*" {
71 None
72 } else {
73 Some(Uuid::parse_str(parts[2]).map_err(serde::de::Error::custom)?)
74 };
75
76 let action = parts[3];
77 if !action
78 .chars()
79 .all(|c| c.is_ascii_alphanumeric() || c == '_')
80 {
81 return Err(serde::de::Error::custom(format!(
82 "Action must contain only alphanumeric characters and underscores, got: {action}"
83 )));
84 }
85
86 Ok(Permission {
87 tenant_id,
88 resource_pattern: parts[1].to_owned(),
89 resource_id,
90 action: action.to_owned(),
91 })
92 }
93}
94
95impl Permission {
96 #[must_use]
97 pub fn builder() -> PermissionBuilder {
98 PermissionBuilder::default()
99 }
100
101 #[must_use]
102 pub fn tenant_id(&self) -> Option<Uuid> {
103 self.tenant_id
104 }
105
106 #[must_use]
107 pub fn resource_pattern(&self) -> &str {
108 &self.resource_pattern
109 }
110
111 #[must_use]
112 pub fn resource_id(&self) -> Option<Uuid> {
113 self.resource_id
114 }
115
116 #[must_use]
117 pub fn action(&self) -> &str {
118 &self.action
119 }
120}
121
122#[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) -> anyhow::Result<Permission> {
164 let resource_pattern = self
165 .resource_pattern
166 .ok_or_else(|| anyhow::anyhow!("resource_pattern is required"))?;
167
168 let action = self
169 .action
170 .ok_or_else(|| anyhow::anyhow!("action is required"))?;
171
172 if !action
174 .chars()
175 .all(|c| c.is_ascii_alphanumeric() || c == '_')
176 {
177 return Err(anyhow::anyhow!(
178 "Action must contain only alphanumeric characters and underscores, got: {action}"
179 ));
180 }
181
182 Ok(Permission {
183 tenant_id: self.tenant_id,
184 resource_pattern,
185 resource_id: self.resource_id,
186 action,
187 })
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use uuid::Uuid;
195
196 #[test]
197 fn test_permission_builder_with_tenant_id() {
198 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
199 let permission = Permission::builder()
200 .tenant_id(tenant_id)
201 .resource_pattern("gts.x.core.events.topic.v1~vendor.*")
202 .action("publish")
203 .build()
204 .unwrap();
205
206 assert_eq!(permission.tenant_id(), Some(tenant_id));
207 assert_eq!(
208 permission.resource_pattern(),
209 "gts.x.core.events.topic.v1~vendor.*"
210 );
211 assert_eq!(permission.action(), "publish");
212 }
213
214 #[test]
215 fn test_permission_builder_without_tenant_id() {
216 let permission = Permission::builder()
217 .resource_pattern("file_parser")
218 .action("edit")
219 .build()
220 .unwrap();
221
222 assert_eq!(permission.tenant_id(), None);
223 assert_eq!(permission.resource_pattern(), "file_parser");
224 assert_eq!(permission.action(), "edit");
225 }
226
227 #[test]
228 fn test_permission_builder_missing_resource_pattern() {
229 let result = Permission::builder().action("edit").build();
230 assert!(result.is_err());
231 assert!(
232 result
233 .unwrap_err()
234 .to_string()
235 .contains("resource_pattern is required")
236 );
237 }
238
239 #[test]
240 fn test_permission_builder_missing_action() {
241 let result = Permission::builder()
242 .resource_pattern("file_parser")
243 .build();
244 assert!(result.is_err());
245 assert!(
246 result
247 .unwrap_err()
248 .to_string()
249 .contains("action is required")
250 );
251 }
252
253 #[test]
254 fn test_serialize_permission_with_tenant_id() {
255 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
256 let permission = Permission::builder()
257 .tenant_id(tenant_id)
258 .resource_pattern("gts.x.core.events.topic.v1~vendor.*")
259 .action("publish")
260 .build()
261 .unwrap();
262
263 let serialized = serde_json::to_string(&permission).unwrap();
264 assert_eq!(
265 serialized,
266 r#""550e8400-e29b-41d4-a716-446655440000:gts.x.core.events.topic.v1~vendor.*:*:publish""#
267 );
268 }
269
270 #[test]
271 fn test_serialize_permission_without_tenant_id() {
272 let permission = Permission::builder()
273 .resource_pattern("file_parser")
274 .action("edit")
275 .build()
276 .unwrap();
277
278 let serialized = serde_json::to_string(&permission).unwrap();
279 assert_eq!(serialized, r#""*:file_parser:*:edit""#);
280 }
281
282 #[test]
283 fn test_deserialize_permission_with_tenant_id() {
284 let json = r#""550e8400-e29b-41d4-a716-446655440000:gts.x.core.events.topic.v1~vendor.*:*:publish""#;
285 let permission: Permission = serde_json::from_str(json).unwrap();
286
287 let expected_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
288 assert_eq!(permission.tenant_id(), Some(expected_id));
289 assert_eq!(
290 permission.resource_pattern(),
291 "gts.x.core.events.topic.v1~vendor.*"
292 );
293 assert_eq!(permission.resource_id(), None);
294 assert_eq!(permission.action(), "publish");
295 }
296
297 #[test]
298 fn test_deserialize_permission_without_tenant_id() {
299 let json = r#""*:file_parser:*:edit""#;
300 let permission: Permission = serde_json::from_str(json).unwrap();
301
302 assert_eq!(permission.tenant_id(), None);
303 assert_eq!(permission.resource_pattern(), "file_parser");
304 assert_eq!(permission.resource_id(), None);
305 assert_eq!(permission.action(), "edit");
306 }
307
308 #[test]
309 fn test_deserialize_permission_invalid_action_with_colons() {
310 let json = r#""*:file_parser:*:action:with:colons""#;
311 let result: Result<Permission, _> = serde_json::from_str(json);
312
313 assert!(result.is_err());
314 let err = result.unwrap_err();
315 assert!(
316 err.to_string()
317 .contains("Action must contain only alphanumeric characters and underscores")
318 );
319 }
320
321 #[test]
322 fn test_deserialize_permission_invalid_action_with_special_chars() {
323 let json = r#""*:file_parser:*:edit-action""#;
324 let result: Result<Permission, _> = serde_json::from_str(json);
325
326 assert!(result.is_err());
327 let err = result.unwrap_err();
328 assert!(
329 err.to_string()
330 .contains("Action must contain only alphanumeric characters and underscores")
331 );
332 }
333
334 #[test]
335 fn test_permission_builder_invalid_action() {
336 let result = Permission::builder()
337 .resource_pattern("file_parser")
338 .action("invalid:action")
339 .build();
340
341 assert!(result.is_err());
342 let err = result.unwrap_err();
343 assert!(
344 err.to_string()
345 .contains("Action must contain only alphanumeric characters and underscores")
346 );
347 }
348
349 #[test]
350 fn test_deserialize_permission_invalid_format_missing_parts() {
351 let json = r#""invalid:format""#;
352 let result: Result<Permission, _> = serde_json::from_str(json);
353
354 assert!(result.is_err());
355 let err = result.unwrap_err();
356 assert!(
357 err.to_string()
358 .contains("Expected format 'tenant_id:resource_pattern:resource_id:action'")
359 );
360 }
361
362 #[test]
363 fn test_deserialize_permission_invalid_uuid() {
364 let json = r#""not-a-uuid:file_parser:edit""#;
365 let result: Result<Permission, _> = serde_json::from_str(json);
366
367 assert!(result.is_err());
368 }
369
370 #[test]
371 fn test_serialize_deserialize_roundtrip_with_tenant_id() {
372 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
373 let original = Permission::builder()
374 .tenant_id(tenant_id)
375 .resource_pattern("gts.x.core.events.type.v1~")
376 .action("edit")
377 .build()
378 .unwrap();
379
380 let serialized = serde_json::to_string(&original).unwrap();
381 let deserialized: Permission = serde_json::from_str(&serialized).unwrap();
382
383 assert_eq!(deserialized.tenant_id(), original.tenant_id());
384 assert_eq!(deserialized.resource_pattern(), original.resource_pattern());
385 assert_eq!(deserialized.action(), original.action());
386 }
387
388 #[test]
389 fn test_serialize_deserialize_roundtrip_without_tenant_id() {
390 let original = Permission::builder()
391 .resource_pattern("gts.x.core.events.topic.v1~*")
392 .action("subscribe")
393 .build()
394 .unwrap();
395
396 let serialized = serde_json::to_string(&original).unwrap();
397 let deserialized: Permission = serde_json::from_str(&serialized).unwrap();
398
399 assert_eq!(deserialized.tenant_id(), original.tenant_id());
400 assert_eq!(deserialized.resource_pattern(), original.resource_pattern());
401 assert_eq!(deserialized.action(), original.action());
402 }
403
404 #[test]
405 fn test_serialize_list_of_permissions() {
406 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
407 let permissions = vec![
408 Permission::builder()
409 .tenant_id(tenant_id)
410 .resource_pattern("gts.x.core.events.topic.v1~vendor.*")
411 .action("publish")
412 .build()
413 .unwrap(),
414 Permission::builder()
415 .resource_pattern("file_parser")
416 .action("edit")
417 .build()
418 .unwrap(),
419 ];
420
421 let serialized = serde_json::to_string(&permissions).unwrap();
422 let deserialized: Vec<Permission> = serde_json::from_str(&serialized).unwrap();
423
424 assert_eq!(deserialized.len(), 2);
425 assert_eq!(deserialized[0].tenant_id(), Some(tenant_id));
426 assert_eq!(
427 deserialized[0].resource_pattern(),
428 "gts.x.core.events.topic.v1~vendor.*"
429 );
430 assert_eq!(deserialized[0].action(), "publish");
431 assert_eq!(deserialized[1].tenant_id(), None);
432 assert_eq!(deserialized[1].resource_pattern(), "file_parser");
433 assert_eq!(deserialized[1].action(), "edit");
434 }
435
436 #[test]
437 fn test_permission_builder_with_resource_id() {
438 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
439 let resource_id = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440002").unwrap();
440
441 let permission = Permission::builder()
442 .tenant_id(tenant_id)
443 .resource_pattern("gts.x.core.events.type.v1~")
444 .resource_id(resource_id)
445 .action("edit")
446 .build()
447 .unwrap();
448
449 assert_eq!(permission.tenant_id(), Some(tenant_id));
450 assert_eq!(permission.resource_pattern(), "gts.x.core.events.type.v1~");
451 assert_eq!(permission.resource_id(), Some(resource_id));
452 assert_eq!(permission.action(), "edit");
453 }
454
455 #[test]
456 fn test_serialize_permission_with_resource_id() {
457 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
458 let resource_id = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440002").unwrap();
459
460 let permission = Permission::builder()
461 .tenant_id(tenant_id)
462 .resource_pattern("gts.x.core.events.type.v1~")
463 .resource_id(resource_id)
464 .action("edit")
465 .build()
466 .unwrap();
467
468 let serialized = serde_json::to_string(&permission).unwrap();
469 assert_eq!(
470 serialized,
471 r#""550e8400-e29b-41d4-a716-446655440000:gts.x.core.events.type.v1~:660e8400-e29b-41d4-a716-446655440002:edit""#
472 );
473 }
474
475 #[test]
476 fn test_deserialize_permission_with_resource_id() {
477 let json = r#""550e8400-e29b-41d4-a716-446655440000:gts.x.core.events.type.v1~:660e8400-e29b-41d4-a716-446655440002:edit""#;
478 let permission: Permission = serde_json::from_str(json).unwrap();
479
480 let expected_tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
481 let expected_resource_id = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440002").unwrap();
482
483 assert_eq!(permission.tenant_id(), Some(expected_tenant_id));
484 assert_eq!(permission.resource_pattern(), "gts.x.core.events.type.v1~");
485 assert_eq!(permission.resource_id(), Some(expected_resource_id));
486 assert_eq!(permission.action(), "edit");
487 }
488
489 #[test]
490 fn test_serialize_deserialize_roundtrip_with_resource_id() {
491 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
492 let resource_id = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440002").unwrap();
493
494 let original = Permission::builder()
495 .tenant_id(tenant_id)
496 .resource_pattern("gts.x.core.events.type.v1~")
497 .resource_id(resource_id)
498 .action("edit")
499 .build()
500 .unwrap();
501
502 let serialized = serde_json::to_string(&original).unwrap();
503 let deserialized: Permission = serde_json::from_str(&serialized).unwrap();
504
505 assert_eq!(deserialized.tenant_id(), original.tenant_id());
506 assert_eq!(deserialized.resource_pattern(), original.resource_pattern());
507 assert_eq!(deserialized.resource_id(), original.resource_id());
508 assert_eq!(deserialized.action(), original.action());
509 }
510
511 #[test]
512 fn test_permission_with_wildcard_tenant_and_specific_resource() {
513 let resource_id = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440002").unwrap();
514
515 let permission = Permission::builder()
516 .resource_pattern("gts.x.core.events.topic.v1~vendor.*")
517 .resource_id(resource_id)
518 .action("publish")
519 .build()
520 .unwrap();
521
522 assert_eq!(permission.tenant_id(), None);
523 assert_eq!(permission.resource_id(), Some(resource_id));
524
525 let serialized = serde_json::to_string(&permission).unwrap();
526 assert_eq!(
527 serialized,
528 r#""*:gts.x.core.events.topic.v1~vendor.*:660e8400-e29b-41d4-a716-446655440002:publish""#
529 );
530 }
531}