1use std::{
12 fmt, ptr,
13 sync::atomic::{fence, Ordering},
14};
15
16#[cfg(test)]
17use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
18
19#[cfg(test)]
20const MAX_AUTHORIZATION_BYTES: usize = 8 * 1024;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum Tier {
24 Anon,
25 Read,
26 Write,
27 Approve,
28}
29
30#[derive(Debug, Copy, Clone, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum AuthGate {
33 Read,
34 Write,
35 WriteApprove,
36 Delete,
37}
38
39#[derive(Clone)]
40pub struct Tokens {
41 pub(crate) read: Option<NonEmptyBytes>,
42 pub(crate) write: Option<NonEmptyBytes>,
43 pub(crate) approve: Option<NonEmptyBytes>,
44}
45
46#[derive(Clone)]
51pub(crate) struct NonEmptyBytes(Vec<u8>);
52
53impl NonEmptyBytes {
54 pub(crate) fn new(bytes: impl Into<Vec<u8>>) -> Option<Self> {
55 let bytes = bytes.into();
56 Self::is_valid(&bytes).then_some(Self(bytes))
57 }
58
59 pub(crate) fn is_valid(bytes: &[u8]) -> bool {
60 !bytes.is_empty()
61 && std::str::from_utf8(bytes)
62 .map(|value| !value.trim().is_empty())
63 .unwrap_or(true)
64 }
65
66 pub(crate) fn as_slice(&self) -> &[u8] {
67 &self.0
68 }
69
70 pub(crate) fn into_vec(mut self) -> Vec<u8> {
71 std::mem::take(&mut self.0)
72 }
73}
74
75impl Drop for NonEmptyBytes {
76 fn drop(&mut self) {
77 wipe_vec_allocation(&mut self.0);
78 }
79}
80
81impl fmt::Debug for NonEmptyBytes {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 f.write_str("NonEmptyBytes(..)")
84 }
85}
86
87fn wipe_vec_allocation(bytes: &mut Vec<u8>) {
88 let ptr = bytes.as_mut_ptr();
93 for index in 0..bytes.capacity() {
94 unsafe {
95 ptr::write_volatile(ptr.add(index), 0);
96 }
97 }
98 fence(Ordering::SeqCst);
99}
100
101impl Tokens {
102 #[cfg(test)]
107 pub fn from_env() -> Self {
108 Self {
109 read: nonempty_env("ELASTIK_READ_TOKEN"),
110 write: nonempty_env("ELASTIK_WRITE_TOKEN").or_else(|| nonempty_env("ELASTIK_TOKEN")),
111 approve: nonempty_env("ELASTIK_APPROVE_TOKEN"),
112 }
113 }
114
115 pub fn read_required(&self) -> bool {
116 self.read.is_some()
117 }
118
119 #[cfg(test)]
123 pub fn check(&self, authorization: Option<&str>) -> Tier {
124 let Some(value) = authorization else {
125 return Tier::Anon;
126 };
127 if value.len() > MAX_AUTHORIZATION_BYTES {
128 return Tier::Anon;
129 }
130 let Some((scheme, credentials)) = value.split_once(char::is_whitespace) else {
131 return Tier::Anon;
132 };
133 let credentials = credentials.trim();
134 if scheme.eq_ignore_ascii_case("Bearer") {
135 return self.check_token_bytes(credentials.as_bytes());
136 }
137 if scheme.eq_ignore_ascii_case("Basic") {
138 if let Ok(decoded) = B64.decode(credentials) {
139 if let Some(idx) = decoded.iter().position(|&b| b == b':') {
140 return self.check_token_bytes(&decoded[idx + 1..]);
141 }
142 }
143 }
144 Tier::Anon
145 }
146
147 pub(crate) fn check_token_bytes(&self, candidate: &[u8]) -> Tier {
148 if !NonEmptyBytes::is_valid(candidate) {
151 return Tier::Anon;
152 }
153 if let Some(t) = &self.approve {
155 if ct_eq(candidate, t.as_slice()) {
156 return Tier::Approve;
157 }
158 }
159 if let Some(t) = &self.write {
160 if ct_eq(candidate, t.as_slice()) {
161 return Tier::Write;
162 }
163 }
164 if let Some(t) = &self.read {
165 if ct_eq(candidate, t.as_slice()) {
166 return Tier::Read;
167 }
168 }
169 Tier::Anon
170 }
171}
172
173#[cfg(test)]
174fn nonempty_env(name: &str) -> Option<NonEmptyBytes> {
175 match std::env::var(name) {
176 Ok(s) => NonEmptyBytes::new(s.into_bytes()),
177 _ => None,
178 }
179}
180
181#[cfg(test)]
186pub fn env_set_but_empty(name: &str) -> bool {
187 match std::env::var(name) {
188 Ok(s) => s.trim().is_empty(),
189 Err(_) => false,
190 }
191}
192
193pub fn ct_eq(a: &[u8], b: &[u8]) -> bool {
197 if a.len() != b.len() {
198 return false;
199 }
200 let mut diff: u8 = 0;
201 for (x, y) in a.iter().zip(b.iter()) {
202 diff |= x ^ y;
203 }
204 diff == 0
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use std::sync::{Mutex, OnceLock};
211
212 fn env_lock() -> &'static Mutex<()> {
213 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
214 LOCK.get_or_init(|| Mutex::new(()))
215 }
216
217 fn bearer(token: &str) -> String {
218 format!("{} {token}", "Bearer")
219 }
220
221 fn token(bytes: &[u8]) -> NonEmptyBytes {
222 NonEmptyBytes::new(bytes.to_vec()).unwrap()
223 }
224
225 struct EnvGuard {
226 read: Option<String>,
227 write: Option<String>,
228 legacy_write: Option<String>,
229 approve: Option<String>,
230 }
231
232 impl EnvGuard {
233 fn capture() -> Self {
234 Self {
235 read: std::env::var("ELASTIK_READ_TOKEN").ok(),
236 write: std::env::var("ELASTIK_WRITE_TOKEN").ok(),
237 legacy_write: std::env::var("ELASTIK_TOKEN").ok(),
238 approve: std::env::var("ELASTIK_APPROVE_TOKEN").ok(),
239 }
240 }
241 }
242
243 impl Drop for EnvGuard {
244 fn drop(&mut self) {
245 match &self.read {
246 Some(v) => std::env::set_var("ELASTIK_READ_TOKEN", v),
247 None => std::env::remove_var("ELASTIK_READ_TOKEN"),
248 }
249 match &self.write {
250 Some(v) => std::env::set_var("ELASTIK_WRITE_TOKEN", v),
251 None => std::env::remove_var("ELASTIK_WRITE_TOKEN"),
252 }
253 match &self.legacy_write {
254 Some(v) => std::env::set_var("ELASTIK_TOKEN", v),
255 None => std::env::remove_var("ELASTIK_TOKEN"),
256 }
257 match &self.approve {
258 Some(v) => std::env::set_var("ELASTIK_APPROVE_TOKEN", v),
259 None => std::env::remove_var("ELASTIK_APPROVE_TOKEN"),
260 }
261 }
262 }
263
264 #[test]
265 fn from_env_treats_empty_tokens_as_disabled() {
266 let _lock = env_lock().lock().unwrap();
267 let _env = EnvGuard::capture();
268 std::env::set_var("ELASTIK_READ_TOKEN", " ");
269 std::env::set_var("ELASTIK_WRITE_TOKEN", "");
270 std::env::remove_var("ELASTIK_TOKEN");
271 std::env::set_var("ELASTIK_APPROVE_TOKEN", "\u{2003}\n");
272
273 let tokens = Tokens::from_env();
274
275 assert!(tokens.read.is_none());
276 assert!(tokens.write.is_none());
277 assert!(tokens.approve.is_none());
278 assert_eq!(tokens.check(Some("Bearer ")), Tier::Anon);
279 assert_eq!(tokens.check(Some("Basic Og==")), Tier::Anon);
280 assert!(env_set_but_empty("ELASTIK_READ_TOKEN"));
281 assert!(env_set_but_empty("ELASTIK_WRITE_TOKEN"));
282 assert!(env_set_but_empty("ELASTIK_APPROVE_TOKEN"));
283 }
284
285 #[test]
286 fn legacy_elastik_token_is_a_write_token_fallback() {
287 let _lock = env_lock().lock().unwrap();
288 let _env = EnvGuard::capture();
289 std::env::remove_var("ELASTIK_WRITE_TOKEN");
290 std::env::set_var("ELASTIK_TOKEN", "legacy-writer");
291
292 let tokens = Tokens::from_env();
293
294 assert_eq!(tokens.check(Some(&bearer("legacy-writer"))), Tier::Write);
295 }
296
297 #[test]
298 fn invalid_authorization_candidates_never_match() {
299 let tokens = Tokens {
300 read: Some(token(b"reader")),
301 write: Some(token(b"writer")),
302 approve: Some(token(b"approve")),
303 };
304
305 assert_eq!(tokens.check(Some("Bearer ")), Tier::Anon);
306 assert_eq!(tokens.check(Some("Bearer \t\r\n")), Tier::Anon);
307 assert_eq!(tokens.check(Some(&bearer("\u{2003}"))), Tier::Anon);
308 assert_eq!(tokens.check(Some("Basic Og==")), Tier::Anon);
309 }
310
311 #[test]
312 fn wipe_vec_allocation_clears_spare_capacity() {
313 let mut bytes = Vec::with_capacity(8);
314 bytes.extend_from_slice(b"key");
315 let ptr = bytes.as_mut_ptr();
316 let cap = bytes.capacity();
317 unsafe {
318 for index in bytes.len()..cap {
319 ptr.add(index).write(b'x');
320 }
321 }
322
323 wipe_vec_allocation(&mut bytes);
324
325 unsafe {
326 bytes.set_len(cap);
327 }
328 assert!(bytes.iter().all(|byte| *byte == 0));
329 }
330
331 #[test]
332 fn oversized_authorization_header_is_anon() {
333 let tokens = Tokens {
334 read: Some(token(b"reader")),
335 write: Some(token(b"writer")),
336 approve: Some(token(b"approve")),
337 };
338 let header = format!("Bearer {}", "x".repeat(MAX_AUTHORIZATION_BYTES));
339
340 assert_eq!(tokens.check(Some(&header)), Tier::Anon);
341 }
342
343 #[test]
344 fn nonempty_tokens_still_authenticate() {
345 let tokens = Tokens {
346 read: Some(token(b"reader")),
347 write: Some(token(b"writer")),
348 approve: Some(token(b"approve")),
349 };
350 let basic_writer = B64.encode("user:writer");
351
352 assert_eq!(tokens.check(Some(&bearer("reader"))), Tier::Read);
353 assert_eq!(tokens.check(Some("bearer reader")), Tier::Read);
354 assert_eq!(tokens.check(Some(&bearer("writer"))), Tier::Write);
355 assert_eq!(
356 tokens.check(Some(&format!("Basic {basic_writer}"))),
357 Tier::Write
358 );
359 assert_eq!(
360 tokens.check(Some(&format!("basic {basic_writer}"))),
361 Tier::Write
362 );
363 assert_eq!(tokens.check(Some(&bearer("approve"))), Tier::Approve);
364 assert_eq!(tokens.check(Some("Bearer ")), Tier::Anon);
365 }
366}