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)]
47#[serde(rename_all = "lowercase")]
48pub enum Permission {
49 Publish,
50 Subscribe,
51}
52
53enum_str_method!(
54 #[allow(dead_code)]
55 fn as_str(Permission) -> &str {
56 Publish => "publish",
57 Subscribe => "subscribe",
58 }
59);
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Rule {
63 pub permission: Permission,
64
65 pub topic_pattern: String,
66}
67
68impl Rule {
69 pub fn new(permission: Permission, topic_pattern: impl Into<String>) -> Self {
71 Self {
72 permission,
73 topic_pattern: topic_pattern.into(),
74 }
75 }
76
77 pub fn matches(&self, topic: &str) -> bool {
79 matches_pattern(&self.topic_pattern, topic)
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Policy {
85 pub role: String,
86
87 pub rules: Vec<Rule>,
88
89 #[serde(default)]
90 pub description: String,
91}
92
93impl Policy {
94 pub fn new(role: impl Into<String>) -> Self {
96 Self {
97 role: role.into(),
98 rules: Vec::new(),
99 description: String::new(),
100 }
101 }
102
103 pub fn with_description(mut self, description: impl Into<String>) -> Self {
105 self.description = description.into();
106 self
107 }
108
109 pub fn allow(mut self, permission: Permission, topic_pattern: impl Into<String>) -> Self {
111 self.rules.push(Rule::new(permission, topic_pattern));
112 self
113 }
114
115 pub fn allow_publish(self, topic_pattern: impl Into<String>) -> Self {
117 self.allow(Permission::Publish, topic_pattern)
118 }
119
120 pub fn allow_subscribe(self, topic_pattern: impl Into<String>) -> Self {
122 self.allow(Permission::Subscribe, topic_pattern)
123 }
124
125 pub fn allows(&self, permission: Permission, topic: &str) -> bool {
127 self.rules
128 .iter()
129 .any(|rule| rule.permission == permission && rule.matches(topic))
130 }
131
132 fn redis_key(&self) -> String {
134 format!("mecha10:auth:policies:{}", self.role)
135 }
136
137 fn redis_list_key() -> String {
139 "mecha10:auth:policies:list".to_string()
140 }
141
142 fn redis_role_key(node_id: &str) -> String {
144 format!("mecha10:auth:roles:{}", node_id)
145 }
146}
147
148pub trait AuthExt {
149 fn register_policy(&self, policy: &Policy) -> impl std::future::Future<Output = Result<()>> + Send;
171
172 fn get_policy(&self, role: &str) -> impl std::future::Future<Output = Result<Option<Policy>>> + Send;
178
179 fn list_policies(&self) -> impl std::future::Future<Output = Result<Vec<Policy>>> + Send;
181
182 fn assign_role(&self, node_id: &str, role: &str) -> impl std::future::Future<Output = Result<()>> + Send;
189
190 fn get_role(&self, node_id: &str) -> impl std::future::Future<Output = Result<Option<String>>> + Send;
196
197 fn check_permission(
209 &self,
210 node_id: &str,
211 permission: Permission,
212 topic: &str,
213 ) -> impl std::future::Future<Output = Result<bool>> + Send;
214
215 fn delete_policy(&self, role: &str) -> impl std::future::Future<Output = Result<()>> + Send;
221
222 fn unassign_role(&self, node_id: &str) -> impl std::future::Future<Output = Result<()>> + Send;
228}
229
230impl AuthExt for Context {
231 async fn register_policy(&self, policy: &Policy) -> Result<()> {
232 #[cfg(feature = "messaging")]
233 {
234 use redis::AsyncCommands;
235
236 let mut conn = crate::redis_list_util::get_redis_connection().await?;
237
238 let policy_json = serde_json::to_string(&policy)
239 .map_err(|e| Mecha10Error::Other(format!("Failed to serialize policy: {}", e)))?;
240
241 let key = policy.redis_key();
242 conn.set::<_, _, ()>(&key, &policy_json)
243 .await
244 .map_err(|e| Mecha10Error::MessagingError {
245 message: format!("Failed to register policy: {}", e),
246 suggestion: "Check Redis connection".to_string(),
247 })?;
248
249 let list_key = Policy::redis_list_key();
250 conn.sadd::<_, _, ()>(&list_key, &policy.role)
251 .await
252 .map_err(|e| Mecha10Error::MessagingError {
253 message: format!("Failed to update policy list: {}", e),
254 suggestion: "Check Redis connection".to_string(),
255 })?;
256
257 Ok(())
258 }
259
260 #[cfg(not(feature = "messaging"))]
261 {
262 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
263 }
264 }
265
266 async fn get_policy(&self, role: &str) -> Result<Option<Policy>> {
267 #[cfg(feature = "messaging")]
268 {
269 use redis::AsyncCommands;
270
271 let mut conn = crate::redis_list_util::get_redis_connection().await?;
272
273 let key = format!("mecha10:auth:policies:{}", role);
274 let json: Option<String> = conn.get(&key).await.map_err(|e| Mecha10Error::MessagingError {
275 message: format!("Failed to get policy: {}", e),
276 suggestion: "Check Redis connection".to_string(),
277 })?;
278
279 if let Some(json) = json {
280 let policy = serde_json::from_str::<Policy>(&json)
281 .map_err(|e| Mecha10Error::Other(format!("Failed to parse policy: {}", e)))?;
282 Ok(Some(policy))
283 } else {
284 Ok(None)
285 }
286 }
287
288 #[cfg(not(feature = "messaging"))]
289 {
290 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
291 }
292 }
293
294 async fn list_policies(&self) -> Result<Vec<Policy>> {
295 #[cfg(feature = "messaging")]
296 {
297 crate::redis_list_util::collect_from_redis_set(&Policy::redis_list_key(), |role| async move {
298 self.get_policy(&role).await
299 })
300 .await
301 }
302
303 #[cfg(not(feature = "messaging"))]
304 {
305 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
306 }
307 }
308
309 async fn assign_role(&self, node_id: &str, role: &str) -> Result<()> {
310 #[cfg(feature = "messaging")]
311 {
312 use redis::AsyncCommands;
313
314 let mut conn = crate::redis_list_util::get_redis_connection().await?;
315
316 let key = Policy::redis_role_key(node_id);
317 conn.set::<_, _, ()>(&key, role)
318 .await
319 .map_err(|e| Mecha10Error::MessagingError {
320 message: format!("Failed to assign role: {}", e),
321 suggestion: "Check Redis connection".to_string(),
322 })?;
323
324 Ok(())
325 }
326
327 #[cfg(not(feature = "messaging"))]
328 {
329 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
330 }
331 }
332
333 async fn get_role(&self, node_id: &str) -> Result<Option<String>> {
334 #[cfg(feature = "messaging")]
335 {
336 let key = Policy::redis_role_key(node_id);
337 crate::redis_list_util::get_optional_from_redis(&key, "Failed to get role").await
338 }
339
340 #[cfg(not(feature = "messaging"))]
341 {
342 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
343 }
344 }
345
346 async fn check_permission(&self, node_id: &str, permission: Permission, topic: &str) -> Result<bool> {
347 let role = match self.get_role(node_id).await? {
348 Some(role) => role,
349 None => return Ok(false),
350 };
351
352 let policy = match self.get_policy(&role).await? {
353 Some(policy) => policy,
354 None => return Ok(false),
355 };
356
357 Ok(policy.allows(permission, topic))
358 }
359
360 async fn delete_policy(&self, role: &str) -> Result<()> {
361 #[cfg(feature = "messaging")]
362 {
363 use redis::AsyncCommands;
364
365 let mut conn = crate::redis_list_util::get_redis_connection().await?;
366
367 let key = format!("mecha10:auth:policies:{}", role);
368 conn.del::<_, ()>(&key)
369 .await
370 .map_err(|e| Mecha10Error::MessagingError {
371 message: format!("Failed to delete policy: {}", e),
372 suggestion: "Check Redis connection".to_string(),
373 })?;
374
375 let list_key = Policy::redis_list_key();
376 conn.srem::<_, _, ()>(&list_key, role)
377 .await
378 .map_err(|e| Mecha10Error::MessagingError {
379 message: format!("Failed to update policy list: {}", e),
380 suggestion: "Check Redis connection".to_string(),
381 })?;
382
383 Ok(())
384 }
385
386 #[cfg(not(feature = "messaging"))]
387 {
388 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
389 }
390 }
391
392 async fn unassign_role(&self, node_id: &str) -> Result<()> {
393 #[cfg(feature = "messaging")]
394 {
395 use redis::AsyncCommands;
396
397 let mut conn = crate::redis_list_util::get_redis_connection().await?;
398
399 let key = Policy::redis_role_key(node_id);
400 conn.del::<_, ()>(&key)
401 .await
402 .map_err(|e| Mecha10Error::MessagingError {
403 message: format!("Failed to unassign role: {}", e),
404 suggestion: "Check Redis connection".to_string(),
405 })?;
406
407 Ok(())
408 }
409
410 #[cfg(not(feature = "messaging"))]
411 {
412 Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
413 }
414 }
415}
416
417pub fn matches_pattern(pattern: &str, topic: &str) -> bool {
436 crate::aggregated::pattern::matches_pattern(topic, pattern)
437}