featherbit/plugins/native/
multi_auth.rs1use async_trait::async_trait;
13use bytes::Bytes;
14use std::collections::HashMap;
15use std::sync::Arc;
16
17use crate::context::{Context, GatewayError};
18use crate::plugins::resources::PluginResources;
19use crate::plugins::{create_plugin, Plugin, PluginExecutionError, PluginResult};
20
21pub struct MultiAuthPlugin {
37 sub_plugins: Vec<Box<dyn Plugin>>,
39}
40
41impl MultiAuthPlugin {
42 pub fn from_config(
62 config: &HashMap<String, serde_json::Value>,
63 resources: &Arc<PluginResources>,
64 ) -> Result<Self, String> {
65 let entries = config
66 .get("auth_plugins")
67 .and_then(|v| v.as_array())
68 .ok_or("multi-auth plugin requires an 'auth_plugins' array")?;
69
70 if entries.is_empty() {
71 return Err("multi-auth 'auth_plugins' must not be empty".to_string());
72 }
73
74 let mut sub_plugins = Vec::with_capacity(entries.len());
75 for (idx, entry) in entries.iter().enumerate() {
76 let obj = entry.as_object().ok_or_else(|| {
77 format!(
78 "multi-auth 'auth_plugins[{}]' must be a single-key map {{plugin-type: config}}",
79 idx
80 )
81 })?;
82 if obj.len() != 1 {
83 return Err(format!(
84 "multi-auth 'auth_plugins[{}]' must have exactly one key (the plugin type)",
85 idx
86 ));
87 }
88 let (plugin_type, inner) = obj.iter().next().unwrap();
89 let inner_config: HashMap<String, serde_json::Value> = inner
90 .as_object()
91 .map(|m| m.clone().into_iter().collect())
92 .unwrap_or_default();
93 let plugin = create_plugin(plugin_type, &inner_config, resources)
94 .map_err(|e| format!("multi-auth sub-plugin '{}': {}", plugin_type, e))?;
95 sub_plugins.push(plugin);
96 }
97
98 Ok(Self { sub_plugins })
99 }
100
101 fn reject(ctx: Context) -> PluginResult {
105 let mut ctx = ctx;
106 ctx.response.status_code = 401;
107 ctx.response.body =
108 Bytes::from(r#"{"error": "unauthorized", "message": "Authorization Failed"}"#);
109 ctx.response.headers.insert(
110 "content-type".to_string(),
111 vec!["application/json".to_string()],
112 );
113 Err(PluginExecutionError {
114 context: ctx,
115 error: GatewayError {
116 node_id: String::new(),
117 code: "MULTI_AUTH_FAILED".to_string(),
118 message: "all authentication methods failed".to_string(),
119 metadata: HashMap::new(),
120 },
121 })
122 }
123}
124
125#[async_trait]
126impl Plugin for MultiAuthPlugin {
127 fn plugin_type(&self) -> &str {
128 "multi-auth"
129 }
130
131 async fn execute(
132 &self,
133 ctx: Context,
134 named_inputs: &HashMap<String, serde_json::Value>,
135 ) -> PluginResult {
136 let original_response = ctx.response.clone();
139 let mut ctx = ctx;
140
141 for sub in &self.sub_plugins {
142 match sub.execute(ctx, named_inputs).await {
143 Ok(output) => return Ok(output),
144 Err(err) => {
145 ctx = err.context;
146 ctx.response = original_response.clone();
147 }
148 }
149 }
150
151 Self::reject(ctx)
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::context::{GatewayRequest, Protocol};
159
160 fn ctx_with_key(key: Option<&str>) -> Context {
161 let mut headers = HashMap::new();
162 if let Some(k) = key {
163 headers.insert("x-api-key".to_string(), vec![k.to_string()]);
164 }
165 Context::new(GatewayRequest {
166 method: "GET".into(),
167 path: "/".into(),
168 host: "h".into(),
169 scheme: "http".into(),
170 headers,
171 query_params: HashMap::new(),
172 body: Bytes::new(),
173 remote_addr: "1.2.3.4:5".into(),
174 protocol: Protocol::Http1,
175 })
176 }
177
178 fn two_key_auth_config() -> HashMap<String, serde_json::Value> {
180 let mut config = HashMap::new();
181 config.insert(
182 "auth_plugins".to_string(),
183 serde_json::json!([
184 { "key-auth": { "keys": ["alpha"] } },
185 { "key-auth": { "keys": ["beta"] } }
186 ]),
187 );
188 config
189 }
190
191 #[tokio::test]
192 async fn test_second_sub_plugin_succeeds() {
193 let plugin =
194 MultiAuthPlugin::from_config(&two_key_auth_config(), &PluginResources::empty())
195 .unwrap();
196 let out = plugin
198 .execute(ctx_with_key(Some("beta")), &HashMap::new())
199 .await
200 .unwrap();
201 assert_eq!(out.context.response.status_code, 0);
203 }
204
205 #[tokio::test]
206 async fn test_all_fail_rejects_401() {
207 let plugin =
208 MultiAuthPlugin::from_config(&two_key_auth_config(), &PluginResources::empty())
209 .unwrap();
210 let err = plugin
211 .execute(ctx_with_key(Some("gamma")), &HashMap::new())
212 .await
213 .unwrap_err();
214 assert_eq!(err.error.code, "MULTI_AUTH_FAILED");
215 assert_eq!(err.context.response.status_code, 401);
216 }
217
218 #[test]
219 fn test_requires_auth_plugins() {
220 assert!(MultiAuthPlugin::from_config(&HashMap::new(), &PluginResources::empty()).is_err());
221 }
222
223 #[test]
224 fn test_rejects_bad_sub_config() {
225 let mut config = HashMap::new();
227 config.insert(
228 "auth_plugins".to_string(),
229 serde_json::json!([{ "key-auth": {} }]),
230 );
231 assert!(MultiAuthPlugin::from_config(&config, &PluginResources::empty()).is_err());
232 }
233}