1use crate::context::Context;
42use crate::enum_str_method;
43use crate::error::{Mecha10Error, Result};
44use serde::{Deserialize, Serialize};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "lowercase")]
49pub enum Permission {
50 Publish,
52 Subscribe,
54}
55
56enum_str_method!(
57 #[allow(dead_code)]
58 fn as_str(Permission) -> &str {
59 Publish => "publish",
60 Subscribe => "subscribe",
61 }
62);
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct Rule {
67 pub permission: Permission,
69
70 pub topic_pattern: String,
72}
73
74impl Rule {
75 pub fn new(permission: Permission, topic_pattern: impl Into<String>) -> Self {
77 Self {
78 permission,
79 topic_pattern: topic_pattern.into(),
80 }
81 }
82
83 pub fn matches(&self, topic: &str) -> bool {
85 matches_pattern(&self.topic_pattern, topic)
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct Policy {
92 pub role: String,
94
95 pub rules: Vec<Rule>,
97
98 #[serde(default)]
100 pub description: String,
101}
102
103impl Policy {
104 pub fn new(role: impl Into<String>) -> Self {
106 Self {
107 role: role.into(),
108 rules: Vec::new(),
109 description: String::new(),
110 }
111 }
112
113 pub fn with_description(mut self, description: impl Into<String>) -> Self {
115 self.description = description.into();
116 self
117 }
118
119 pub fn allow(mut self, permission: Permission, topic_pattern: impl Into<String>) -> Self {
121 self.rules.push(Rule::new(permission, topic_pattern));
122 self
123 }
124
125 pub fn allow_publish(self, topic_pattern: impl Into<String>) -> Self {
127 self.allow(Permission::Publish, topic_pattern)
128 }
129
130 pub fn allow_subscribe(self, topic_pattern: impl Into<String>) -> Self {
132 self.allow(Permission::Subscribe, topic_pattern)
133 }
134
135 pub fn allows(&self, permission: Permission, topic: &str) -> bool {
137 self.rules
138 .iter()
139 .any(|rule| rule.permission == permission && rule.matches(topic))
140 }
141
142 fn redis_key(&self) -> String {
144 format!("mecha10:auth:policies:{}", self.role)
145 }
146
147 fn redis_list_key() -> String {
149 "mecha10:auth:policies:list".to_string()
150 }
151
152 fn redis_role_key(node_id: &str) -> String {
154 format!("mecha10:auth:roles:{}", node_id)
155 }
156}
157
158pub trait AuthExt {
160 fn register_policy(&self, policy: &Policy) -> impl std::future::Future<Output = Result<()>> + Send;
182
183 fn get_policy(&self, role: &str) -> impl std::future::Future<Output = Result<Option<Policy>>> + Send;
189
190 fn list_policies(&self) -> impl std::future::Future<Output = Result<Vec<Policy>>> + Send;
192
193 fn assign_role(&self, node_id: &str, role: &str) -> impl std::future::Future<Output = Result<()>> + Send;
200
201 fn get_role(&self, node_id: &str) -> impl std::future::Future<Output = Result<Option<String>>> + Send;
207
208 fn check_permission(
220 &self,
221 node_id: &str,
222 permission: Permission,
223 topic: &str,
224 ) -> impl std::future::Future<Output = Result<bool>> + Send;
225
226 fn delete_policy(&self, role: &str) -> impl std::future::Future<Output = Result<()>> + Send;
232
233 fn unassign_role(&self, node_id: &str) -> impl std::future::Future<Output = Result<()>> + Send;
239}
240
241impl AuthExt for Context {
242 async fn register_policy(&self, policy: &Policy) -> Result<()> {
243 #[cfg(feature = "messaging")]
244 {
245 use redis::AsyncCommands;
246
247 let redis_url = Context::get_redis_url()?;
248 let client = redis::Client::open(redis_url.as_str()).map_err(|e| Mecha10Error::MessagingError {
249 message: format!("Failed to connect to Redis: {}", e),
250 suggestion: "Ensure Redis is running".to_string(),
251 })?;
252
253 let mut conn =
254 client
255 .get_multiplexed_async_connection()
256 .await
257 .map_err(|e| Mecha10Error::MessagingError {
258 message: format!("Failed to get Redis connection: {}", e),
259 suggestion: "Ensure Redis is running".to_string(),
260 })?;
261
262 let policy_json = serde_json::to_string(&policy)
264 .map_err(|e| Mecha10Error::Other(format!("Failed to serialize policy: {}", e)))?;
265
266 let key = policy.redis_key();
268 conn.set::<_, _, ()>(&key, &policy_json)
269 .await
270 .map_err(|e| Mecha10Error::MessagingError {
271 message: format!("Failed to register policy: {}", e),
272 suggestion: "Check Redis connection".to_string(),
273 })?;
274
275 let list_key = Policy::redis_list_key();
277 conn.sadd::<_, _, ()>(&list_key, &policy.role)
278 .await
279 .map_err(|e| Mecha10Error::MessagingError {
280 message: format!("Failed to update policy list: {}", e),
281 suggestion: "Check Redis connection".to_string(),
282 })?;
283
284 Ok(())
285 }
286
287 #[cfg(not(feature = "messaging"))]
288 {
289 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
290 }
291 }
292
293 async fn get_policy(&self, role: &str) -> Result<Option<Policy>> {
294 #[cfg(feature = "messaging")]
295 {
296 use redis::AsyncCommands;
297
298 let redis_url = Context::get_redis_url()?;
299 let client = redis::Client::open(redis_url.as_str()).map_err(|e| Mecha10Error::MessagingError {
300 message: format!("Failed to connect to Redis: {}", e),
301 suggestion: "Ensure Redis is running".to_string(),
302 })?;
303
304 let mut conn =
305 client
306 .get_multiplexed_async_connection()
307 .await
308 .map_err(|e| Mecha10Error::MessagingError {
309 message: format!("Failed to get Redis connection: {}", e),
310 suggestion: "Ensure Redis is running".to_string(),
311 })?;
312
313 let key = format!("mecha10:auth:policies:{}", role);
314 let json: Option<String> = conn.get(&key).await.map_err(|e| Mecha10Error::MessagingError {
315 message: format!("Failed to get policy: {}", e),
316 suggestion: "Check Redis connection".to_string(),
317 })?;
318
319 if let Some(json) = json {
320 let policy = serde_json::from_str::<Policy>(&json)
321 .map_err(|e| Mecha10Error::Other(format!("Failed to parse policy: {}", e)))?;
322 Ok(Some(policy))
323 } else {
324 Ok(None)
325 }
326 }
327
328 #[cfg(not(feature = "messaging"))]
329 {
330 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
331 }
332 }
333
334 async fn list_policies(&self) -> Result<Vec<Policy>> {
335 #[cfg(feature = "messaging")]
336 {
337 crate::redis_list_util::collect_from_redis_set(&Policy::redis_list_key(), |role| async move {
338 self.get_policy(&role).await
339 })
340 .await
341 }
342
343 #[cfg(not(feature = "messaging"))]
344 {
345 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
346 }
347 }
348
349 async fn assign_role(&self, node_id: &str, role: &str) -> Result<()> {
350 #[cfg(feature = "messaging")]
351 {
352 use redis::AsyncCommands;
353
354 let redis_url = Context::get_redis_url()?;
355 let client = redis::Client::open(redis_url.as_str()).map_err(|e| Mecha10Error::MessagingError {
356 message: format!("Failed to connect to Redis: {}", e),
357 suggestion: "Ensure Redis is running".to_string(),
358 })?;
359
360 let mut conn =
361 client
362 .get_multiplexed_async_connection()
363 .await
364 .map_err(|e| Mecha10Error::MessagingError {
365 message: format!("Failed to get Redis connection: {}", e),
366 suggestion: "Ensure Redis is running".to_string(),
367 })?;
368
369 let key = Policy::redis_role_key(node_id);
370 conn.set::<_, _, ()>(&key, role)
371 .await
372 .map_err(|e| Mecha10Error::MessagingError {
373 message: format!("Failed to assign role: {}", e),
374 suggestion: "Check Redis connection".to_string(),
375 })?;
376
377 Ok(())
378 }
379
380 #[cfg(not(feature = "messaging"))]
381 {
382 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
383 }
384 }
385
386 async fn get_role(&self, node_id: &str) -> Result<Option<String>> {
387 #[cfg(feature = "messaging")]
388 {
389 use redis::AsyncCommands;
390
391 let redis_url = Context::get_redis_url()?;
392 let client = redis::Client::open(redis_url.as_str()).map_err(|e| Mecha10Error::MessagingError {
393 message: format!("Failed to connect to Redis: {}", e),
394 suggestion: "Ensure Redis is running".to_string(),
395 })?;
396
397 let mut conn =
398 client
399 .get_multiplexed_async_connection()
400 .await
401 .map_err(|e| Mecha10Error::MessagingError {
402 message: format!("Failed to get Redis connection: {}", e),
403 suggestion: "Ensure Redis is running".to_string(),
404 })?;
405
406 let key = Policy::redis_role_key(node_id);
407 let role: Option<String> = conn.get(&key).await.map_err(|e| Mecha10Error::MessagingError {
408 message: format!("Failed to get role: {}", e),
409 suggestion: "Check Redis connection".to_string(),
410 })?;
411
412 Ok(role)
413 }
414
415 #[cfg(not(feature = "messaging"))]
416 {
417 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
418 }
419 }
420
421 async fn check_permission(&self, node_id: &str, permission: Permission, topic: &str) -> Result<bool> {
422 let role = match self.get_role(node_id).await? {
424 Some(role) => role,
425 None => return Ok(false), };
427
428 let policy = match self.get_policy(&role).await? {
430 Some(policy) => policy,
431 None => return Ok(false), };
433
434 Ok(policy.allows(permission, topic))
436 }
437
438 async fn delete_policy(&self, role: &str) -> Result<()> {
439 #[cfg(feature = "messaging")]
440 {
441 use redis::AsyncCommands;
442
443 let redis_url = Context::get_redis_url()?;
444 let client = redis::Client::open(redis_url.as_str()).map_err(|e| Mecha10Error::MessagingError {
445 message: format!("Failed to connect to Redis: {}", e),
446 suggestion: "Ensure Redis is running".to_string(),
447 })?;
448
449 let mut conn =
450 client
451 .get_multiplexed_async_connection()
452 .await
453 .map_err(|e| Mecha10Error::MessagingError {
454 message: format!("Failed to get Redis connection: {}", e),
455 suggestion: "Ensure Redis is running".to_string(),
456 })?;
457
458 let key = format!("mecha10:auth:policies:{}", role);
460 conn.del::<_, ()>(&key)
461 .await
462 .map_err(|e| Mecha10Error::MessagingError {
463 message: format!("Failed to delete policy: {}", e),
464 suggestion: "Check Redis connection".to_string(),
465 })?;
466
467 let list_key = Policy::redis_list_key();
469 conn.srem::<_, _, ()>(&list_key, role)
470 .await
471 .map_err(|e| Mecha10Error::MessagingError {
472 message: format!("Failed to update policy list: {}", e),
473 suggestion: "Check Redis connection".to_string(),
474 })?;
475
476 Ok(())
477 }
478
479 #[cfg(not(feature = "messaging"))]
480 {
481 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
482 }
483 }
484
485 async fn unassign_role(&self, node_id: &str) -> Result<()> {
486 #[cfg(feature = "messaging")]
487 {
488 use redis::AsyncCommands;
489
490 let redis_url = Context::get_redis_url()?;
491 let client = redis::Client::open(redis_url.as_str()).map_err(|e| Mecha10Error::MessagingError {
492 message: format!("Failed to connect to Redis: {}", e),
493 suggestion: "Ensure Redis is running".to_string(),
494 })?;
495
496 let mut conn =
497 client
498 .get_multiplexed_async_connection()
499 .await
500 .map_err(|e| Mecha10Error::MessagingError {
501 message: format!("Failed to get Redis connection: {}", e),
502 suggestion: "Ensure Redis is running".to_string(),
503 })?;
504
505 let key = Policy::redis_role_key(node_id);
506 conn.del::<_, ()>(&key)
507 .await
508 .map_err(|e| Mecha10Error::MessagingError {
509 message: format!("Failed to unassign role: {}", e),
510 suggestion: "Check Redis connection".to_string(),
511 })?;
512
513 Ok(())
514 }
515
516 #[cfg(not(feature = "messaging"))]
517 {
518 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
519 }
520 }
521}
522
523pub fn matches_pattern(pattern: &str, topic: &str) -> bool {
542 crate::aggregated::pattern::matches_pattern(topic, pattern)
544}