1use std::collections::HashMap;
16use std::sync::Arc;
17
18use serde::{Deserialize, Serialize};
19
20use crate::context::Context;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ConsumerConfig {
35 pub name: String,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub group: Option<String>,
40 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
43 pub labels: HashMap<String, String>,
44 #[serde(default)]
47 pub credentials: HashMap<String, serde_json::Value>,
48}
49
50#[derive(Debug)]
52pub struct Consumer {
53 pub name: String,
54 pub group: Option<String>,
55 pub labels: HashMap<String, String>,
56 pub credentials: HashMap<String, serde_json::Value>,
57}
58
59#[derive(Default)]
68pub struct ConsumerStore {
69 by_name: HashMap<String, Arc<Consumer>>,
70 by_credential: HashMap<String, HashMap<String, Arc<Consumer>>>,
72}
73
74fn primary_credential_field(auth_type: &str) -> &'static str {
76 match auth_type {
77 "basic-auth" => "username",
78 "hmac-auth" => "access_key",
79 _ => "key",
80 }
81}
82
83impl ConsumerStore {
84 pub fn from_config(consumers: &[ConsumerConfig]) -> Result<Self, String> {
87 let mut store = Self::default();
88
89 for config in consumers {
90 if config.name.trim().is_empty() {
91 return Err("consumer with empty name".to_string());
92 }
93 let consumer = Arc::new(Consumer {
94 name: config.name.clone(),
95 group: config.group.clone(),
96 labels: config.labels.clone(),
97 credentials: config.credentials.clone(),
98 });
99
100 if store
101 .by_name
102 .insert(consumer.name.clone(), consumer.clone())
103 .is_some()
104 {
105 return Err(format!("duplicate consumer name '{}'", consumer.name));
106 }
107
108 for (auth_type, credential) in &config.credentials {
109 let field = primary_credential_field(auth_type);
110 let value = credential
111 .get(field)
112 .and_then(|v| v.as_str())
113 .ok_or_else(|| {
114 format!(
115 "consumer '{}': credential '{}' is missing string field '{}'",
116 consumer.name, auth_type, field
117 )
118 })?;
119
120 let index = store.by_credential.entry(auth_type.clone()).or_default();
121 if let Some(existing) = index.insert(value.to_string(), consumer.clone()) {
122 return Err(format!(
123 "consumers '{}' and '{}' share the same {} credential",
124 existing.name, consumer.name, auth_type
125 ));
126 }
127 }
128 }
129
130 Ok(store)
131 }
132
133 pub fn find_by_credential(&self, auth_type: &str, value: &str) -> Option<Arc<Consumer>> {
135 self.by_credential.get(auth_type)?.get(value).cloned()
136 }
137
138 pub fn get(&self, name: &str) -> Option<Arc<Consumer>> {
140 self.by_name.get(name).cloned()
141 }
142
143 #[allow(dead_code)] pub fn len(&self) -> usize {
146 self.by_name.len()
147 }
148
149 #[allow(dead_code)]
151 pub fn is_empty(&self) -> bool {
152 self.by_name.is_empty()
153 }
154}
155
156pub fn attach_consumer(ctx: &mut Context, consumer: &Consumer, auth_type: &str) {
164 ctx.message.insert(
165 "consumer.name".to_string(),
166 serde_json::json!(consumer.name),
167 );
168 if let Some(ref group) = consumer.group {
169 ctx.message
170 .insert("consumer.group".to_string(), serde_json::json!(group));
171 }
172 if !consumer.labels.is_empty() {
173 ctx.message.insert(
174 "consumer.labels".to_string(),
175 serde_json::json!(consumer.labels),
176 );
177 }
178 ctx.message.insert(
179 "consumer.auth_type".to_string(),
180 serde_json::json!(auth_type),
181 );
182
183 ctx.request.headers.insert(
184 "x-consumer-username".to_string(),
185 vec![consumer.name.clone()],
186 );
187 if let Some(custom_id) = consumer.labels.get("custom_id") {
188 ctx.message.insert(
189 "consumer.custom_id".to_string(),
190 serde_json::json!(custom_id),
191 );
192 ctx.request
193 .headers
194 .insert("x-consumer-custom-id".to_string(), vec![custom_id.clone()]);
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn config(json: serde_json::Value) -> Vec<ConsumerConfig> {
203 serde_json::from_value(json).expect("valid consumer config")
204 }
205
206 #[test]
207 fn test_store_lookup_and_attach() {
208 let store = ConsumerStore::from_config(&config(serde_json::json!([
209 {
210 "name": "alice",
211 "group": "partners",
212 "labels": { "custom_id": "42", "tier": "gold" },
213 "credentials": {
214 "key-auth": { "key": "alice-key" },
215 "basic-auth": { "username": "alice", "password": "pw" }
216 }
217 },
218 { "name": "bob", "credentials": { "key-auth": { "key": "bob-key" } } }
219 ])))
220 .unwrap();
221
222 assert_eq!(store.len(), 2);
223 let alice = store.find_by_credential("key-auth", "alice-key").unwrap();
224 assert_eq!(alice.name, "alice");
225 assert!(store.find_by_credential("key-auth", "nope").is_none());
226 assert!(store.find_by_credential("jwt-auth", "alice-key").is_none());
227 assert_eq!(
228 store
229 .find_by_credential("basic-auth", "alice")
230 .unwrap()
231 .name,
232 "alice"
233 );
234 assert_eq!(store.get("bob").unwrap().name, "bob");
235
236 let mut ctx = crate::context::Context::new(crate::context::GatewayRequest {
238 method: "GET".into(),
239 path: "/".into(),
240 host: "h".into(),
241 scheme: "http".into(),
242 headers: HashMap::new(),
243 query_params: HashMap::new(),
244 body: bytes::Bytes::new(),
245 remote_addr: "1.2.3.4:5".into(),
246 protocol: crate::context::Protocol::Http1,
247 });
248 attach_consumer(&mut ctx, &alice, "key-auth");
249 assert_eq!(
250 ctx.message.get("consumer.name"),
251 Some(&serde_json::json!("alice"))
252 );
253 assert_eq!(
254 ctx.message.get("consumer.group"),
255 Some(&serde_json::json!("partners"))
256 );
257 assert_eq!(
258 ctx.message.get("consumer.custom_id"),
259 Some(&serde_json::json!("42"))
260 );
261 assert_eq!(
262 ctx.message.get("consumer.auth_type"),
263 Some(&serde_json::json!("key-auth"))
264 );
265 assert_eq!(
266 ctx.request.headers.get("x-consumer-username"),
267 Some(&vec!["alice".to_string()])
268 );
269 assert_eq!(
270 ctx.request.headers.get("x-consumer-custom-id"),
271 Some(&vec!["42".to_string()])
272 );
273 }
274
275 #[test]
276 fn test_store_rejects_duplicates_and_malformed() {
277 assert!(ConsumerStore::from_config(&config(serde_json::json!([
279 { "name": "a" }, { "name": "a" }
280 ])))
281 .is_err());
282
283 assert!(ConsumerStore::from_config(&config(serde_json::json!([
285 { "name": "a", "credentials": { "key-auth": { "key": "same" } } },
286 { "name": "b", "credentials": { "key-auth": { "key": "same" } } }
287 ])))
288 .is_err());
289
290 assert!(ConsumerStore::from_config(&config(serde_json::json!([
292 { "name": "a", "credentials": { "key-auth": { "wrong": "x" } } }
293 ])))
294 .is_err());
295 }
296}