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