1#![deny(unsafe_code)]
20
21use std::str::FromStr;
22use std::sync::Arc;
23
24use async_trait::async_trait;
25use tokio::{self, sync::RwLock};
26
27use rmqtt::{
28 codec::v5::SubscribeAckReason,
29 context::ServerContext,
30 hook::{Handler, HookResult, Parameter, Register, ReturnType, Type},
31 macros::Plugin,
32 plugin::{PackageInfo, Plugin},
33 register,
34 types::{AuthResult, PublishAclResult, SubscribeAclResult, Topic},
35 Result,
36};
37
38use config::{Access, Control, PluginConfig, PH_C, PH_U};
39
40mod config;
41
42register!(AclPlugin::new);
43
44const CACHE_KEY: &str = "$SYS/ACL-CACHE-MAP";
45
46#[derive(Plugin)]
47struct AclPlugin {
48 scx: ServerContext,
49 register: Box<dyn Register>,
50 cfg: Arc<RwLock<PluginConfig>>,
51}
52
53impl AclPlugin {
54 #[inline]
55 async fn new<N: Into<String>>(scx: ServerContext, name: N) -> Result<Self> {
56 let name = name.into();
57 let cfg = scx.plugins.read_config_default::<PluginConfig>(&name);
58 log::info!("{name} AclPlugin cfg: {cfg:?}");
59 let cfg = Arc::new(RwLock::new(cfg?));
60 let register = scx.extends.hook_mgr().register();
61 Ok(Self { scx, register, cfg })
62 }
63}
64
65#[async_trait]
66impl Plugin for AclPlugin {
67 #[inline]
68 async fn init(&mut self) -> Result<()> {
69 log::info!("{} init", self.name());
70 let cfg = &self.cfg;
71 let priority = cfg.read().await.priority;
72 self.register.add_priority(Type::ClientConnected, priority, Box::new(AclHandler::new(cfg))).await;
73 self.register.add_priority(Type::ClientDisconnected, priority, Box::new(AclHandler::new(cfg))).await;
74 self.register.add_priority(Type::ClientAuthenticate, priority, Box::new(AclHandler::new(cfg))).await;
75 self.register
76 .add_priority(Type::ClientSubscribeCheckAcl, priority, Box::new(AclHandler::new(cfg)))
77 .await;
78 self.register
79 .add_priority(Type::MessagePublishCheckAcl, priority, Box::new(AclHandler::new(cfg)))
80 .await;
81 Ok(())
82 }
83
84 #[inline]
85 async fn get_config(&self) -> Result<serde_json::Value> {
86 self.cfg.read().await.to_json()
87 }
88
89 #[inline]
90 async fn load_config(&mut self) -> Result<()> {
91 let new_cfg = self.scx.plugins.read_config::<PluginConfig>(self.name())?;
92 *self.cfg.write().await = new_cfg;
93 log::debug!("load_config ok, {:?}", self.cfg);
94 Ok(())
95 }
96
97 #[inline]
98 async fn start(&mut self) -> Result<()> {
99 log::info!("{} start", self.name());
100 self.register.start().await;
101 Ok(())
102 }
103
104 #[inline]
105 async fn stop(&mut self) -> Result<bool> {
106 log::warn!("{} stop, the default ACL plug-in, it cannot be stopped", self.name());
107 Ok(false)
109 }
110}
111
112struct AclHandler {
113 cfg: Arc<RwLock<PluginConfig>>,
114}
115
116impl AclHandler {
117 fn new(cfg: &Arc<RwLock<PluginConfig>>) -> Self {
118 Self { cfg: cfg.clone() }
119 }
120}
121
122#[async_trait]
123impl Handler for AclHandler {
124 async fn hook(&self, param: &Parameter, acc: Option<HookResult>) -> ReturnType {
125 match param {
126 Parameter::ClientConnected(session) => {
127 let cfg = self.cfg.clone();
128 let client_id = session.id.client_id.clone();
129 let username = session.id.username.clone();
130 let extra_attrs = session.extra_attrs.clone();
131
132 let build_placeholders = async move {
133 for rule in cfg.read().await.rules() {
134 for ph_tf in &rule.topics.placeholders {
135 let mut tf = ph_tf.replace(PH_C, &client_id);
136 if let Some(un) = &username {
137 tf = tf.replace(PH_U, un);
138 } else {
139 tf = tf.replace(PH_U, "");
140 }
141 if let Err(e) = rule.add_topic_filter(&tf, client_id.clone()).await {
142 log::error!(
143 "acl config error, build_placeholders, add topic filter error, {e}"
144 );
145 }
146 log::debug!("topic filter: {tf}");
147 if let Some(caches) =
148 extra_attrs.write().await.get_default_mut(CACHE_KEY.into(), Vec::default)
149 {
150 caches.push(tf);
151 }
152 }
153
154 for eq_ph_t in &rule.topics.eq_placeholders {
155 let mut t = eq_ph_t.replace(PH_C, &client_id);
156 if let Some(un) = &username {
157 t = t.replace(PH_U, un);
158 } else {
159 t = t.replace(PH_U, "");
160 }
161 log::info!("eq topic: {t}");
162 rule.add_topic_to_eqs(t);
163 }
164
165 log::debug!("rule.access: {:?}", rule.access);
166 log::debug!("rule.users: {:?}", rule.users);
167 log::debug!("rule.control: {:?}", rule.control);
168 log::debug!("rule.topics.eqs: {:?}", rule.topics.eqs);
169 log::debug!("rule.topics.tree: {:?}", rule.topics.tree.read().await.list(100));
170 log::debug!("rule.topics.placeholders: {:?}", rule.topics.placeholders);
171 }
172 };
173 tokio::spawn(build_placeholders);
174 }
175
176 Parameter::ClientDisconnected(session, _reason) => {
177 if let Some(topic_filters) = session.extra_attrs.read().await.get::<Vec<String>>(CACHE_KEY) {
178 let client_id = session.id.client_id.clone();
179 for topic_filter in topic_filters {
180 for rule in self.cfg.read().await.rules() {
181 if let Err(e) = rule.remove_topic(topic_filter.as_str(), &client_id).await {
182 log::error!("remove topic filter error, {e}");
183 }
184 }
185 }
186 };
187 }
188
189 Parameter::ClientAuthenticate(connect_info) => {
190 log::debug!("ClientAuthenticate acl");
191 if matches!(
192 acc,
193 Some(HookResult::AuthResult(AuthResult::BadUsernameOrPassword))
194 | Some(HookResult::AuthResult(AuthResult::NotAuthorized))
195 ) {
196 return (false, acc);
197 }
198
199 for rule in self.cfg.read().await.rules() {
200 if !matches!(rule.control, Control::Connect | Control::All) {
201 continue;
202 }
203
204 let allow = matches!(rule.access, Access::Allow);
205 let (hit, superuser) = rule.hit(
206 connect_info.id(),
207 connect_info.password(),
208 Some(connect_info.proto_ver()),
209 allow,
210 );
211 if hit {
212 log::debug!("{:?} ClientAuthenticate, rule: {:?}", connect_info.id(), rule);
213 return if allow {
214 (false, Some(HookResult::AuthResult(AuthResult::Allow(superuser, None))))
215 } else {
216 (false, Some(HookResult::AuthResult(AuthResult::NotAuthorized)))
217 };
218 }
219 }
220 return (false, Some(HookResult::AuthResult(AuthResult::NotAuthorized)));
221 }
222
223 Parameter::ClientSubscribeCheckAcl(session, subscribe) => {
224 if let Some(HookResult::SubscribeAclResult(acl_result)) = &acc {
225 if acl_result.failure() {
226 return (false, acc);
227 }
228 }
229 let topic =
230 Topic::from_str(&subscribe.topic_filter).unwrap_or_else(|_| Topic::from(Vec::new()));
231 let topic_filter = &subscribe.topic_filter;
232 for (idx, rule) in self.cfg.read().await.rules().iter().enumerate() {
233 if !matches!(rule.control, Control::Subscribe | Control::Pubsub | Control::All) {
234 continue;
235 }
236
237 let allow = matches!(rule.access, Access::Allow);
238 let (hit, _) =
239 rule.hit(&session.id, session.password(), session.protocol().await.ok(), allow);
240 if !hit {
241 continue;
242 }
243 if !rule.topics.is_match(&topic, topic_filter, &session.id.client_id).await {
244 continue;
245 }
246 log::debug!(
247 "{:?} ClientSubscribeCheckAcl, {}, is_match ok: topic_filter: {}",
248 session.id,
249 idx,
250 topic_filter
251 );
252 return if allow {
253 (
254 false,
255 Some(HookResult::SubscribeAclResult(SubscribeAclResult::new_success(
256 subscribe.opts.qos(),
257 None,
258 ))),
259 )
260 } else {
261 (
262 false,
263 Some(HookResult::SubscribeAclResult(SubscribeAclResult::new_failure(
264 SubscribeAckReason::UnspecifiedError,
265 ))),
266 )
267 };
268 }
269 return (
270 false,
271 Some(HookResult::SubscribeAclResult(SubscribeAclResult::new_failure(
272 SubscribeAckReason::UnspecifiedError,
273 ))),
274 );
275 }
276
277 Parameter::MessagePublishCheckAcl(session, publish) => {
278 if let Some(HookResult::PublishAclResult(acl_res)) = &acc {
279 if acl_res.is_rejected() {
280 return (false, acc);
281 }
282 }
283
284 let topic_str = &publish.topic;
285 let topic = Topic::from_str(topic_str).unwrap_or_else(|_| Topic::from(Vec::new()));
286 let disconnect_if_pub_rejected = self.cfg.read().await.disconnect_if_pub_rejected;
287 for (idx, rule) in self.cfg.read().await.rules().iter().enumerate() {
288 if !matches!(rule.control, Control::Publish | Control::Pubsub | Control::All) {
289 continue;
290 }
291
292 let allow = matches!(rule.access, Access::Allow);
293 let (hit, _) =
294 rule.hit(&session.id, session.password(), session.protocol().await.ok(), allow);
295 if !hit {
296 continue;
297 }
298 if !rule.topics.is_match(&topic, topic_str, &session.id.client_id).await {
299 continue;
300 }
301 log::debug!(
302 "{:?} MessagePublishCheckAcl, {}, is_match ok: topic_str: {}",
303 session.id,
304 idx,
305 topic_str
306 );
307 return if allow {
308 (false, Some(HookResult::PublishAclResult(PublishAclResult::allow())))
309 } else {
310 (
311 false,
312 Some(HookResult::PublishAclResult(PublishAclResult::rejected(
313 disconnect_if_pub_rejected,
314 None,
315 ))),
316 )
317 };
318 }
319 return (
320 false,
321 Some(HookResult::PublishAclResult(PublishAclResult::rejected(
322 disconnect_if_pub_rejected,
323 None,
324 ))),
325 );
326 }
327 _ => {
328 log::error!("parameter is: {param:?}");
329 }
330 }
331 (true, acc)
332 }
333}