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 {
118 self.description = description.into();
119 self
120 }
121
122 pub fn allow(mut self, permission: Permission, topic_pattern: impl Into<String>) -> Self {
126 self.rules.push(Rule::new(permission, topic_pattern));
127 self
128 }
129
130 pub fn allow_publish(self, topic_pattern: impl Into<String>) -> Self {
134 self.allow(Permission::Publish, topic_pattern)
135 }
136
137 pub fn allow_subscribe(self, topic_pattern: impl Into<String>) -> Self {
139 self.allow(Permission::Subscribe, topic_pattern)
140 }
141
142 pub fn allows(&self, permission: Permission, topic: &str) -> bool {
144 self.rules
145 .iter()
146 .any(|rule| rule.permission == permission && rule.matches(topic))
147 }
148
149 fn redis_key(&self) -> String {
151 format!("mecha10:auth:policies:{}", self.role)
152 }
153
154 fn redis_list_key() -> String {
156 "mecha10:auth:policies:list".to_string()
157 }
158
159 fn redis_role_key(node_id: &str) -> String {
161 format!("mecha10:auth:roles:{}", node_id)
162 }
163}
164
165pub trait AuthExt {
167 fn register_policy(&self, policy: &Policy) -> impl std::future::Future<Output = Result<()>> + Send;
189
190 fn get_policy(&self, role: &str) -> impl std::future::Future<Output = Result<Option<Policy>>> + Send;
196
197 fn list_policies(&self) -> impl std::future::Future<Output = Result<Vec<Policy>>> + Send;
199
200 fn assign_role(&self, node_id: &str, role: &str) -> impl std::future::Future<Output = Result<()>> + Send;
207
208 fn get_role(&self, node_id: &str) -> impl std::future::Future<Output = Result<Option<String>>> + Send;
214
215 fn check_permission(
227 &self,
228 node_id: &str,
229 permission: Permission,
230 topic: &str,
231 ) -> impl std::future::Future<Output = Result<bool>> + Send;
232
233 fn delete_policy(&self, role: &str) -> impl std::future::Future<Output = Result<()>> + Send;
239
240 fn unassign_role(&self, node_id: &str) -> impl std::future::Future<Output = Result<()>> + Send;
246}
247
248impl AuthExt for Context {
249 async fn register_policy(&self, policy: &Policy) -> Result<()> {
250 #[cfg(feature = "messaging")]
251 {
252 use redis::AsyncCommands;
253
254 let mut conn = crate::redis_list_util::get_redis_connection().await?;
255
256 let policy_json = serde_json::to_string(&policy)
257 .map_err(|e| Mecha10Error::Other(format!("Failed to serialize policy: {}", e)))?;
258
259 let key = policy.redis_key();
260 conn.set::<_, _, ()>(&key, &policy_json)
261 .await
262 .map_err(|e| Mecha10Error::MessagingError {
263 message: format!("Failed to register policy: {}", e),
264 suggestion: "Check Redis connection".to_string(),
265 })?;
266
267 let list_key = Policy::redis_list_key();
268 conn.sadd::<_, _, ()>(&list_key, &policy.role)
269 .await
270 .map_err(|e| Mecha10Error::MessagingError {
271 message: format!("Failed to update policy list: {}", e),
272 suggestion: "Check Redis connection".to_string(),
273 })?;
274
275 Ok(())
276 }
277
278 #[cfg(not(feature = "messaging"))]
279 {
280 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
281 }
282 }
283
284 async fn get_policy(&self, role: &str) -> Result<Option<Policy>> {
285 #[cfg(feature = "messaging")]
286 {
287 use redis::AsyncCommands;
288
289 let mut conn = crate::redis_list_util::get_redis_connection().await?;
290
291 let key = format!("mecha10:auth:policies:{}", role);
292 let json: Option<String> = conn.get(&key).await.map_err(|e| Mecha10Error::MessagingError {
293 message: format!("Failed to get policy: {}", e),
294 suggestion: "Check Redis connection".to_string(),
295 })?;
296
297 if let Some(json) = json {
298 let policy = serde_json::from_str::<Policy>(&json)
299 .map_err(|e| Mecha10Error::Other(format!("Failed to parse policy: {}", e)))?;
300 Ok(Some(policy))
301 } else {
302 Ok(None)
303 }
304 }
305
306 #[cfg(not(feature = "messaging"))]
307 {
308 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
309 }
310 }
311
312 async fn list_policies(&self) -> Result<Vec<Policy>> {
317 #[cfg(feature = "messaging")]
318 {
319 crate::redis_list_util::collect_from_redis_set(&Policy::redis_list_key(), |role| async move {
320 self.get_policy(&role).await
321 })
322 .await
323 }
324
325 #[cfg(not(feature = "messaging"))]
326 {
327 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
328 }
329 }
330
331 async fn assign_role(&self, node_id: &str, role: &str) -> Result<()> {
332 #[cfg(feature = "messaging")]
333 {
334 use redis::AsyncCommands;
335
336 let mut conn = crate::redis_list_util::get_redis_connection().await?;
337
338 let key = Policy::redis_role_key(node_id);
339 conn.set::<_, _, ()>(&key, role)
340 .await
341 .map_err(|e| Mecha10Error::MessagingError {
342 message: format!("Failed to assign role: {}", e),
343 suggestion: "Check Redis connection".to_string(),
344 })?;
345
346 Ok(())
347 }
348
349 #[cfg(not(feature = "messaging"))]
350 {
351 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
352 }
353 }
354
355 async fn get_role(&self, node_id: &str) -> Result<Option<String>> {
356 #[cfg(feature = "messaging")]
357 {
358 let key = Policy::redis_role_key(node_id);
359 crate::redis_list_util::get_optional_from_redis(&key, "Failed to get role").await
360 }
361
362 #[cfg(not(feature = "messaging"))]
363 {
364 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
365 }
366 }
367
368 async fn check_permission(&self, node_id: &str, permission: Permission, topic: &str) -> Result<bool> {
369 let role = match self.get_role(node_id).await? {
370 Some(role) => role,
371 None => return Ok(false),
372 };
373
374 let policy = match self.get_policy(&role).await? {
375 Some(policy) => policy,
376 None => return Ok(false),
377 };
378
379 Ok(policy.allows(permission, topic))
380 }
381
382 async fn delete_policy(&self, role: &str) -> Result<()> {
383 #[cfg(feature = "messaging")]
384 {
385 use redis::AsyncCommands;
386
387 let mut conn = crate::redis_list_util::get_redis_connection().await?;
388
389 let key = format!("mecha10:auth:policies:{}", role);
390 conn.del::<_, ()>(&key)
391 .await
392 .map_err(|e| Mecha10Error::MessagingError {
393 message: format!("Failed to delete policy: {}", e),
394 suggestion: "Check Redis connection".to_string(),
395 })?;
396
397 let list_key = Policy::redis_list_key();
398 conn.srem::<_, _, ()>(&list_key, role)
399 .await
400 .map_err(|e| Mecha10Error::MessagingError {
401 message: format!("Failed to update policy list: {}", e),
402 suggestion: "Check Redis connection".to_string(),
403 })?;
404
405 Ok(())
406 }
407
408 #[cfg(not(feature = "messaging"))]
409 {
410 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
411 }
412 }
413
414 async fn unassign_role(&self, node_id: &str) -> Result<()> {
415 #[cfg(feature = "messaging")]
416 {
417 use redis::AsyncCommands;
418
419 let mut conn = crate::redis_list_util::get_redis_connection().await?;
420
421 let key = Policy::redis_role_key(node_id);
422 conn.del::<_, ()>(&key)
423 .await
424 .map_err(|e| Mecha10Error::MessagingError {
425 message: format!("Failed to unassign role: {}", e),
426 suggestion: "Check Redis connection".to_string(),
427 })?;
428
429 Ok(())
430 }
431
432 #[cfg(not(feature = "messaging"))]
433 {
434 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
435 }
436 }
437}
438
439pub fn matches_pattern(pattern: &str, topic: &str) -> bool {
458 crate::aggregated::pattern::matches_pattern(topic, pattern)
459}