featherbit/plugins/native/
acl.rs1use async_trait::async_trait;
16use bytes::Bytes;
17use std::collections::HashMap;
18
19use crate::context::Context;
20use crate::plugins::{Plugin, PluginOutput, PluginResult};
21use crate::vars::template::Template;
22
23pub struct AclPlugin {
32 allowed_by: Vec<String>,
34 denied_by: Vec<String>,
36 rejected_code: u16,
38 rejected_msg: Option<Template>,
42}
43
44impl AclPlugin {
45 pub fn from_config(config: &HashMap<String, serde_json::Value>) -> Result<Self, String> {
65 let string_list = |key: &str| -> Vec<String> {
66 config
67 .get(key)
68 .and_then(|v| v.as_array())
69 .map(|seq| {
70 seq.iter()
71 .filter_map(|v| v.as_str().map(String::from))
72 .collect()
73 })
74 .unwrap_or_default()
75 };
76
77 let allowed_by = string_list("allowed_by");
78 let denied_by = string_list("denied_by");
79
80 if allowed_by.is_empty() && denied_by.is_empty() {
81 return Err("acl: at least one of 'allowed_by' or 'denied_by' is required".to_string());
82 }
83
84 let rejected_code = config
85 .get("rejected_code")
86 .and_then(|v| v.as_u64())
87 .map(|c| c as u16)
88 .unwrap_or(403);
89
90 let rejected_msg = config
91 .get("rejected_msg")
92 .and_then(|v| v.as_str())
93 .map(|s| Template::parse(s).0);
96
97 Ok(Self {
98 allowed_by,
99 denied_by,
100 rejected_code,
101 rejected_msg,
102 })
103 }
104
105 fn reject(&self, mut ctx: Context, status: u16, message: String) -> PluginResult {
108 ctx.response.status_code = status;
109 ctx.response.body = Bytes::from(serde_json::json!({ "message": message }).to_string());
110 ctx.response.headers.insert(
111 "content-type".to_string(),
112 vec!["application/json".to_string()],
113 );
114 Ok(PluginOutput::on_port(ctx, "denied"))
115 }
116}
117
118#[async_trait]
119impl Plugin for AclPlugin {
120 fn plugin_type(&self) -> &str {
121 "acl"
122 }
123
124 async fn execute(&self, ctx: Context) -> PluginResult {
125 if ctx
127 .message
128 .get("consumer.name")
129 .and_then(|v| v.as_str())
130 .is_none()
131 {
132 return self.reject(ctx, 401, "Missing authentication.".to_string());
133 }
134
135 let group = ctx
136 .message
137 .get("consumer.group")
138 .and_then(|v| v.as_str())
139 .map(String::from);
140
141 let reject_msg = |ctx: &Context| {
142 self.rejected_msg
143 .as_ref()
144 .map(|t| t.render(ctx).into_owned())
145 .unwrap_or_else(|| "The consumer is forbidden.".to_string())
146 };
147
148 if let Some(ref g) = group {
150 if self.denied_by.contains(g) {
151 let msg = reject_msg(&ctx);
152 return self.reject(ctx, self.rejected_code, msg);
153 }
154 }
155
156 if !self.allowed_by.is_empty() {
158 let allowed = group.as_ref().is_some_and(|g| self.allowed_by.contains(g));
159 if !allowed {
160 let msg = reject_msg(&ctx);
161 return self.reject(ctx, self.rejected_code, msg);
162 }
163 }
164
165 Ok(PluginOutput::success(ctx))
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
173
174 fn ctx(consumer_name: Option<&str>, group: Option<&str>) -> Context {
175 let mut message = HashMap::new();
176 if let Some(n) = consumer_name {
177 message.insert("consumer.name".to_string(), serde_json::json!(n));
178 }
179 if let Some(g) = group {
180 message.insert("consumer.group".to_string(), serde_json::json!(g));
181 }
182 Context {
183 request: GatewayRequest {
184 method: "GET".to_string(),
185 path: "/".to_string(),
186 host: "h".to_string(),
187 scheme: "http".to_string(),
188 headers: HashMap::new(),
189 query_params: HashMap::new(),
190 body: Bytes::new(),
191 remote_addr: "1.2.3.4:5".to_string(),
192 protocol: Protocol::Http1,
193 },
194 response: GatewayResponse {
195 status_code: 0,
196 headers: HashMap::new(),
197 body: Bytes::new(),
198 stream: None,
199 },
200 message,
201 errors: Vec::new(),
202 }
203 }
204
205 fn plugin(config: serde_json::Value) -> AclPlugin {
206 let map: HashMap<String, serde_json::Value> = serde_json::from_value(config).unwrap();
207 AclPlugin::from_config(&map).unwrap()
208 }
209
210 #[tokio::test]
211 async fn test_allowed_by() {
212 let p = plugin(serde_json::json!({ "allowed_by": ["partners"] }));
213 assert!(p
214 .execute(ctx(Some("alice"), Some("partners")))
215 .await
216 .unwrap()
217 .port
218 .is_none());
219 let out = p
221 .execute(ctx(Some("alice"), Some("randoms")))
222 .await
223 .unwrap();
224 assert_eq!(out.port, Some("denied"));
225 assert_eq!(out.context.response.status_code, 403);
226 let out = p.execute(ctx(Some("alice"), None)).await.unwrap();
228 assert_eq!(out.port, Some("denied"));
229 }
230
231 #[tokio::test]
232 async fn test_denied_by_wins() {
233 let p = plugin(serde_json::json!({
234 "allowed_by": ["partners", "banned"],
235 "denied_by": ["banned"]
236 }));
237 assert!(p
238 .execute(ctx(Some("alice"), Some("partners")))
239 .await
240 .unwrap()
241 .port
242 .is_none());
243 let out = p.execute(ctx(Some("bob"), Some("banned"))).await.unwrap();
245 assert_eq!(out.port, Some("denied"));
246 }
247
248 #[tokio::test]
249 async fn test_denied_by_only() {
250 let p = plugin(serde_json::json!({ "denied_by": ["banned"] }));
251 assert!(p
253 .execute(ctx(Some("alice"), None))
254 .await
255 .unwrap()
256 .port
257 .is_none());
258 let out = p.execute(ctx(Some("bob"), Some("banned"))).await.unwrap();
259 assert_eq!(out.port, Some("denied"));
260 }
261
262 #[tokio::test]
263 async fn test_rejected_msg_renders_template() {
264 let p = plugin(serde_json::json!({
265 "allowed_by": ["partners"],
266 "rejected_msg": "denied group for {{request.path}}"
267 }));
268 let out = p
269 .execute(ctx(Some("alice"), Some("randoms")))
270 .await
271 .unwrap();
272 assert_eq!(out.port, Some("denied"));
273 let body: serde_json::Value = serde_json::from_slice(&out.context.response.body).unwrap();
274 assert_eq!(body["message"], "denied group for /");
275 }
276
277 #[tokio::test]
278 async fn test_no_consumer_attached_401() {
279 let p = plugin(serde_json::json!({ "allowed_by": ["partners"] }));
280 let out = p.execute(ctx(None, None)).await.unwrap();
281 assert_eq!(out.port, Some("denied"));
282 assert_eq!(out.context.response.status_code, 401);
283 }
284
285 #[test]
286 fn test_requires_a_list() {
287 let map: HashMap<String, serde_json::Value> =
288 serde_json::from_value(serde_json::json!({})).unwrap();
289 assert!(AclPlugin::from_config(&map).is_err());
290 }
291}