yo_resp/dispatch/acl.rs
1//! Users, the rules that describe what each of them may do, and the gate that
2//! holds every command to them.
3//!
4//! # What an ACL actually is
5//!
6//! A user is a name, a switch saying whether it is usable at all, a list of
7//! password hashes, and one or more selectors. A selector is the interesting
8//! part: a set of commands, a set of key patterns and a set of channel patterns.
9//! A command is allowed if any one selector allows the command and every key and
10//! channel it touches. Selectors are tried in order and the first that says yes
11//! wins, which is how a user can be given two unrelated jobs without either of
12//! them leaking into the other. Without them the only way to say "read anything
13//! under `cache:` and write anything under `job:`" is to allow reads and writes
14//! over both.
15//!
16//! Every user starts with one selector, called the root selector here and in
17//! Redis, which is the one the bare rules land in: `ACL SETUSER u +get ~k:*`
18//! writes into it, and `ACL SETUSER u (+get ~k:*)` adds another one beside it.
19//!
20//! # Why the rules are kept as text as well as bits
21//!
22//! Command permission is a bitmap, one bit a command, because the gate reads it
23//! on every command a guarded server runs and a set of names would mean a lookup
24//! there. But `ACL LIST` and `ACL GETUSER` have to hand back something that can
25//! be fed to `ACL SETUSER` and produce the same user, and a bitmap cannot say
26//! whether `+@read -get` or the eleven names that leaves was what the operator
27//! wrote. Worse, the two are not the same user: the first allows a command added
28//! to the `@read` category later and the second does not.
29//!
30//! So a selector keeps both. The bitmap is what the gate reads and the rule list
31//! is what describes it, and every rule that touches commands appends itself to
32//! the list after removing whatever it overrides. That is Redis's design and it
33//! is the reason `ACL GETUSER` on a user built with categories reads back in
34//! categories. The removal is the fiddly half and it is spelled out at
35//! [`Selector::note`].
36//!
37//! # The one thing here that is not Redis's
38//!
39//! Redis gives every subcommand of a container command its own identity and its
40//! own bit, so `+config|get` sets a bit and `+config|nope` is refused because
41//! there is no such command. There is no subcommand table here yet, which is
42//! D-114, so both go through the mechanism Redis keeps for the other case: a
43//! list of first arguments a command is allowed with. The gate then reaches the
44//! same answer for every rule an operator would actually write, and the
45//! difference is that `+config|nope` is accepted here and refused there. It is
46//! written down as D-137 rather than papered over, and it goes away with the
47//! subcommand table.
48//!
49//! # Where the gate sits
50//!
51//! Right after the password and the transaction refusal and right before the
52//! memory limit, which is where `processCommand` puts it. That ordering is
53//! visible: a command a user is not allowed to run is refused before the server
54//! decides whether it is out of memory, and a command with the wrong number of
55//! arguments is told that rather than told it is not allowed.
56
57use std::collections::VecDeque;
58use std::path::Path;
59use std::sync::Mutex;
60use std::sync::atomic::Ordering::{Acquire, Relaxed, Release};
61use std::sync::atomic::{AtomicBool, AtomicU64};
62
63use yo_common::{Code, Error, Result, glob_matches, sha256};
64
65use super::args::{self, Args, is};
66use super::keyspec::{self, Access};
67use super::table::{self, Spec};
68use super::{Server, Session};
69use crate::reply::Out;
70
71/// The user every connection is on until it says otherwise.
72pub(super) const DEFAULT: &[u8] = b"default";
73
74/// The categories `ACL CAT` lists, in the order it lists them.
75///
76/// The first twenty two are Redis's own list in Redis's own order, which is the
77/// order they are declared in `server.h` and not alphabetical. The nine after
78/// them are the module surfaces this engine has built in. A real server with
79/// those modules loaded reports them here too, so the shape is right even though
80/// a bare server answers twenty two.
81const CATEGORIES: [&str; 31] = [
82 "keyspace",
83 "read",
84 "write",
85 "set",
86 "sortedset",
87 "list",
88 "hash",
89 "string",
90 "array",
91 "bitmap",
92 "hyperloglog",
93 "geo",
94 "stream",
95 "pubsub",
96 "admin",
97 "fast",
98 "slow",
99 "blocking",
100 "dangerous",
101 "connection",
102 "transaction",
103 "scripting",
104 "bloom",
105 "cms",
106 "cuckoo",
107 "graph",
108 "json",
109 "search",
110 "tdigest",
111 "timeseries",
112 "topk",
113];
114
115/// How many words of bitmap it takes to give every command a bit.
116const WORDS: usize = table::count().div_ceil(64);
117
118/// The read bit on a key pattern, which is what `%R~` sets.
119const READ: u8 = 1;
120/// The write bit, which is what `%W~` sets.
121const WRITE: u8 = 2;
122
123/// A key pattern and what it may be used for.
124#[derive(Debug, Clone, PartialEq, Eq)]
125struct Pattern {
126 /// [`READ`], [`WRITE`] or both.
127 flags: u8,
128 /// The glob itself, without the sigil.
129 glob: Vec<u8>,
130}
131
132impl Pattern {
133 /// The pattern written the way `ACL LIST` writes it.
134 ///
135 /// A pattern good for both is written bare, because `%RW~x` and `~x` are the
136 /// same thing and the bare form is the one everybody types. A pattern good
137 /// for one keeps its sigil, because there is no other way to say it.
138 fn describe(&self, into: &mut Vec<u8>) {
139 match self.flags {
140 READ => into.extend_from_slice(b"%R~"),
141 WRITE => into.extend_from_slice(b"%W~"),
142 _ => into.push(b'~'),
143 }
144 into.extend_from_slice(&self.glob);
145 }
146}
147
148/// One set of commands, keys and channels, which is what a permission check runs
149/// against.
150#[derive(Debug, Clone)]
151struct Selector {
152 /// One bit a command, indexed by its row in the table.
153 allowed: [u64; WORDS],
154 /// Whether every command is allowed without looking, which `+@all` sets and
155 /// taking any command away clears.
156 all_commands: bool,
157 /// Whether a command that does not exist yet would be allowed.
158 ///
159 /// Redis keeps this as a reserved bit past the end of the bitmap, set by
160 /// `+@all` and cleared by `-@all` and by nothing else, so that `+@all -get`
161 /// and `-@all +everything-but-get` can be told apart. They describe back
162 /// differently and they behave differently the day a command is added.
163 future: bool,
164 /// Whether every key is allowed.
165 all_keys: bool,
166 /// Whether every channel is allowed.
167 all_channels: bool,
168 /// The command rules in the order they were given, space separated.
169 ///
170 /// Not the bitmap written out. See the module note for why the two are both
171 /// kept.
172 rules: Vec<u8>,
173 /// The first arguments a command is allowed with, when it is not allowed
174 /// outright. Sorted by command index.
175 firstargs: Vec<(u16, Vec<Vec<u8>>)>,
176 /// The first arguments a container command is refused with, when it is
177 /// allowed outright. Sorted by command index.
178 ///
179 /// This is how `+config -config|get` is held. A server with a row a
180 /// subcommand has does it by clearing that row's bit, and the day yo has one
181 /// too this goes away with the rest of D-114.
182 deniedfirst: Vec<(u16, Vec<Vec<u8>>)>,
183 /// The key patterns, in the order they were added.
184 patterns: Vec<Pattern>,
185 /// The channel patterns, in the order they were added.
186 channels: Vec<Vec<u8>>,
187}
188
189impl Selector {
190 /// A selector that allows nothing, which is where every user starts.
191 ///
192 /// Nothing except the channels, which start out either allowed or refused
193 /// depending on `open`, the server's `acl-pubsub-default`. That one setting
194 /// is the whole reason this takes an argument: it is read at the moment a
195 /// selector is made and never again, so a selector made before it changed
196 /// keeps what it was made with.
197 fn new(open: bool) -> Selector {
198 Selector {
199 allowed: [0; WORDS],
200 all_commands: false,
201 future: false,
202 all_keys: false,
203 all_channels: open,
204 rules: Vec::new(),
205 firstargs: Vec::new(),
206 deniedfirst: Vec::new(),
207 patterns: Vec::new(),
208 channels: Vec::new(),
209 }
210 }
211
212 /// Whether the command at `i` is allowed outright.
213 fn bit(&self, i: usize) -> bool {
214 i < table::count() && self.allowed[i / 64] & (1 << (i % 64)) != 0
215 }
216
217 /// Allow or refuse the command at `i`, and forget any first arguments it had.
218 ///
219 /// Taking anything away clears `all_commands`, because that flag is only
220 /// there to let the gate skip the bitmap and it can no longer do that.
221 fn set(&mut self, i: usize, allow: bool) {
222 if i >= table::count() {
223 return;
224 }
225 if allow {
226 self.allowed[i / 64] |= 1 << (i % 64);
227 } else {
228 self.allowed[i / 64] &= !(1 << (i % 64));
229 self.all_commands = false;
230 }
231 self.firstargs.retain(|(at, _)| usize::from(*at) != i);
232 self.deniedfirst.retain(|(at, _)| usize::from(*at) != i);
233 }
234
235 /// Record a command rule, dropping whatever it overrides.
236 ///
237 /// The rule that goes on the end is the one that wins, so anything it makes
238 /// irrelevant has to come off first or the list would grow forever and
239 /// describe the user wrongly. Two things are irrelevant: the same rule
240 /// written before, and, when this rule names a whole command, any earlier
241 /// rule about one of its subcommands. `+get` after `-get|foo` leaves `+get`
242 /// alone, because the subcommand rule cannot survive its parent being
243 /// decided again.
244 ///
245 /// Note that a rule is matched on the name and not on the sign, so `+get`
246 /// removes an earlier `-get` rather than sitting after it.
247 fn note(&mut self, rule: &[u8], allow: bool) {
248 let mut kept: Vec<u8> = Vec::with_capacity(self.rules.len() + rule.len() + 2);
249 for old in self.rules.split(|b| *b == b' ') {
250 if old.is_empty() {
251 continue;
252 }
253 // The sign is not part of the name and is not compared.
254 let name = &old[1..];
255 let same = name == rule;
256 let child =
257 name.len() > rule.len() && name.starts_with(rule) && name[rule.len()] == b'|';
258 if same || child {
259 continue;
260 }
261 if !kept.is_empty() {
262 kept.push(b' ');
263 }
264 kept.extend_from_slice(old);
265 }
266 if !kept.is_empty() {
267 kept.push(b' ');
268 }
269 kept.push(if allow { b'+' } else { b'-' });
270 kept.extend_from_slice(rule);
271 self.rules = kept;
272 }
273
274 /// Allow this command only when its first argument is `first`.
275 fn allow_first(&mut self, i: u16, first: &[u8]) {
276 let lower = first.to_ascii_lowercase();
277 match self.firstargs.binary_search_by_key(&i, |(at, _)| *at) {
278 Ok(at) => {
279 let list = &mut self.firstargs[at].1;
280 if !list.contains(&lower) {
281 list.push(lower);
282 }
283 }
284 Err(at) => self.firstargs.insert(at, (i, vec![lower])),
285 }
286 }
287
288 /// The first arguments the command at `i` is allowed with.
289 fn firsts(&self, i: u16) -> &[Vec<u8>] {
290 match self.firstargs.binary_search_by_key(&i, |(at, _)| *at) {
291 Ok(at) => &self.firstargs[at].1,
292 Err(_) => &[],
293 }
294 }
295
296 /// Refuse this command when its first argument is `first`.
297 ///
298 /// This clears `all_commands` for the same reason [`Selector::set`] does:
299 /// the flag is only there so the gate can skip the bitmap, and once one
300 /// subcommand is spoken for it can no longer skip anything.
301 fn deny_first(&mut self, i: u16, first: &[u8]) {
302 let lower = first.to_ascii_lowercase();
303 self.all_commands = false;
304 match self.deniedfirst.binary_search_by_key(&i, |(at, _)| *at) {
305 Ok(at) => {
306 let list = &mut self.deniedfirst[at].1;
307 if !list.contains(&lower) {
308 list.push(lower);
309 }
310 }
311 Err(at) => self.deniedfirst.insert(at, (i, vec![lower])),
312 }
313 }
314
315 /// The first arguments the command at `i` is refused with.
316 fn denied(&self, i: u16) -> &[Vec<u8>] {
317 match self.deniedfirst.binary_search_by_key(&i, |(at, _)| *at) {
318 Ok(at) => &self.deniedfirst[at].1,
319 Err(_) => &[],
320 }
321 }
322
323 /// Everything a rule about commands could have set, back to nothing.
324 fn reset_commands(&mut self, all: bool) {
325 self.allowed = [if all { u64::MAX } else { 0 }; WORDS];
326 self.all_commands = all;
327 self.future = all;
328 self.rules.clear();
329 self.firstargs.clear();
330 self.deniedfirst.clear();
331 }
332
333 /// The command rules written the way `ACL SETUSER` would take them back.
334 ///
335 /// Always starts with `+@all` or `-@all` and then repeats the rule list,
336 /// which is exactly what was fed in after the last time it was cleared. That
337 /// is why this needs no cleverness: the list is already the answer.
338 fn describe_commands(&self) -> Vec<u8> {
339 let mut out = Vec::with_capacity(self.rules.len() + 8);
340 out.extend_from_slice(if self.future { b"+@all" } else { b"-@all" });
341 if !self.rules.is_empty() {
342 out.push(b' ');
343 out.extend_from_slice(&self.rules);
344 }
345 out
346 }
347
348 /// The key patterns, space separated, or `~*`.
349 fn describe_keys(&self) -> Vec<u8> {
350 let mut out = Vec::new();
351 if self.all_keys {
352 out.extend_from_slice(b"~*");
353 return out;
354 }
355 for pattern in &self.patterns {
356 if !out.is_empty() {
357 out.push(b' ');
358 }
359 pattern.describe(&mut out);
360 }
361 out
362 }
363
364 /// The channel patterns, space separated, or `&*`.
365 fn describe_channels(&self) -> Vec<u8> {
366 let mut out = Vec::new();
367 if self.all_channels {
368 out.extend_from_slice(b"&*");
369 return out;
370 }
371 for channel in &self.channels {
372 if !out.is_empty() {
373 out.push(b' ');
374 }
375 out.push(b'&');
376 out.extend_from_slice(channel);
377 }
378 out
379 }
380
381 /// The whole selector as `ACL LIST` writes it inside a user's line.
382 ///
383 /// Keys first, then channels, then commands. The `resetchannels` in front of
384 /// the channel list is not decoration: without it a line read back on a
385 /// server whose `acl-pubsub-default` is `allchannels` would start from every
386 /// channel allowed and the `&x` rules would add nothing.
387 fn describe(&self) -> Vec<u8> {
388 let mut out = self.describe_keys();
389 if !out.is_empty() {
390 out.push(b' ');
391 }
392 if self.all_channels {
393 out.extend_from_slice(b"&* ");
394 } else {
395 out.extend_from_slice(b"resetchannels ");
396 for channel in &self.channels {
397 out.push(b'&');
398 out.extend_from_slice(channel);
399 out.push(b' ');
400 }
401 }
402 out.extend_from_slice(&self.describe_commands());
403 out
404 }
405}
406
407/// What one of these is allowed to say went wrong with a rule.
408///
409/// The sentences are Redis's, word for word, because an operator reading one has
410/// almost certainly found it in Redis's documentation first.
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412enum Bad {
413 /// The rule is not one, or is malformed.
414 Syntax,
415 /// A command or category nobody has heard of.
416 Unknown,
417 /// A key pattern after `~*`.
418 AfterAllKeys,
419 /// A channel pattern after `&*`.
420 AfterAllChannels,
421 /// `<password` for a password the user has not got.
422 NoSuchPassword,
423 /// A `#` hash that is not sixty four hexadecimal characters.
424 Hash,
425 /// `+get|set|other`, which Redis has never supported.
426 NestedFirstArg,
427 /// A `(` with no `)`, which is reported differently from all the rest.
428 Unmatched,
429}
430
431impl Bad {
432 /// The sentence after `Error in ACL SETUSER modifier '<rule>': `.
433 fn text(self) -> &'static str {
434 match self {
435 Bad::Syntax | Bad::Unmatched => "Syntax error",
436 Bad::Unknown => "Unknown command or category name in ACL",
437 Bad::AfterAllKeys => {
438 "Adding a pattern after the * pattern (or the 'allkeys' flag) is not valid and does not have any effect. Try 'resetkeys' to start with an empty list of patterns"
439 }
440 Bad::AfterAllChannels => {
441 "Adding a pattern after the * pattern (or the 'allchannels' flag) is not valid and does not have any effect. Try 'resetchannels' to start with an empty list of channels"
442 }
443 Bad::NoSuchPassword => {
444 "The password you are trying to remove from the user does not exist"
445 }
446 Bad::Hash => {
447 "The password hash must be exactly 64 characters and contain only lowercase hexadecimal characters"
448 }
449 Bad::NestedFirstArg => "Allowing first-arg of a subcommand is not supported",
450 }
451 }
452}
453
454/// One account, and everything it is allowed to do.
455#[derive(Debug, Clone)]
456pub(crate) struct User {
457 /// The name, which is also the key it is filed under.
458 name: Vec<u8>,
459 /// Whether it may authenticate at all.
460 enabled: bool,
461 /// Whether any password gets in, which is what `nopass` means.
462 nopass: bool,
463 /// Whether `DEBUG` payload sanitising is skipped for this user.
464 ///
465 /// Nothing reads it here yet, and it is kept and reported because it is on
466 /// every `ACL LIST` line a real server writes and a config file round trip
467 /// that dropped it would quietly change the user.
468 skip_sanitize: bool,
469 /// The SHA-256 hashes of the passwords that get in, lower case hex.
470 passwords: Vec<[u8; 64]>,
471 /// The root selector first and any others after it.
472 selectors: Vec<Selector>,
473}
474
475impl User {
476 /// A new user, off, with no password and allowed nothing.
477 ///
478 /// `open` is the server's `acl-pubsub-default` and goes to the root
479 /// selector, which is the only thing about a new user that a server setting
480 /// has any say in.
481 fn new(name: &[u8], open: bool) -> User {
482 User {
483 name: name.to_vec(),
484 enabled: false,
485 nopass: false,
486 skip_sanitize: false,
487 passwords: Vec::new(),
488 selectors: vec![Selector::new(open)],
489 }
490 }
491
492 /// The user a server with no `aclfile` starts with, which can do anything.
493 fn default_user() -> User {
494 let mut user = User::new(DEFAULT, false);
495 user.enabled = true;
496 user.nopass = true;
497 let root = &mut user.selectors[0];
498 root.reset_commands(true);
499 root.all_keys = true;
500 root.all_channels = true;
501 user
502 }
503
504 /// The flag words `ACL LIST` and `ACL GETUSER` print, in Redis's order.
505 fn flags(&self) -> Vec<&'static str> {
506 let mut out = Vec::with_capacity(3);
507 out.push(if self.enabled { "on" } else { "off" });
508 if self.nopass {
509 out.push("nopass");
510 }
511 // Exactly one of the two is always set, because a user is created with
512 // the sanitising one on and the rules only ever swap them.
513 out.push(if self.skip_sanitize {
514 "skip-sanitize-payload"
515 } else {
516 "sanitize-payload"
517 });
518 out
519 }
520
521 /// The whole user as one `ACL LIST` line, without the leading `user `.
522 fn describe(&self) -> Vec<u8> {
523 let mut out = Vec::with_capacity(64);
524 out.extend_from_slice(&self.name);
525 for flag in self.flags() {
526 out.push(b' ');
527 out.extend_from_slice(flag.as_bytes());
528 }
529 for hash in &self.passwords {
530 out.extend_from_slice(b" #");
531 out.extend_from_slice(hash);
532 }
533 for (at, selector) in self.selectors.iter().enumerate() {
534 out.push(b' ');
535 if at == 0 {
536 out.extend_from_slice(&selector.describe());
537 } else {
538 out.push(b'(');
539 out.extend_from_slice(&selector.describe());
540 out.push(b')');
541 }
542 }
543 out
544 }
545
546 /// Whether nothing this user could be asked to do would be refused.
547 ///
548 /// One selector that allows every command, every key and every channel, and
549 /// no other selectors, because a second one could only ever allow more and
550 /// the first already allows everything. That is the shape the default user
551 /// has on a server nobody has written an ACL for.
552 fn unrestricted(&self) -> bool {
553 self.selectors.len() == 1
554 && self.selectors[0].all_commands
555 && self.selectors[0].all_keys
556 && self.selectors[0].all_channels
557 }
558
559 /// Whether this password gets in.
560 ///
561 /// Every hash is compared whatever the first one said, so the number of
562 /// passwords a user has is not readable from how long a wrong guess took.
563 /// The hashing has already thrown away everything about the guess that a
564 /// timing difference could leak, so this is belt and braces, and it costs
565 /// one compare of thirty two bytes a password.
566 fn admits(&self, password: &[u8]) -> bool {
567 if !self.enabled {
568 return false;
569 }
570 if self.nopass {
571 return true;
572 }
573 let guess = sha256::hex(password);
574 let mut hit = false;
575 for hash in &self.passwords {
576 hit |= same(hash, &guess);
577 }
578 hit
579 }
580}
581
582/// Whether two byte strings are equal, in time that does not depend on where
583/// they stop being equal.
584pub(super) fn same(a: &[u8], b: &[u8]) -> bool {
585 let mut diff = u8::from(a.len() != b.len());
586 for i in 0..a.len().max(b.len()) {
587 diff |= a.get(i).copied().unwrap_or(0) ^ b.get(i).copied().unwrap_or(0xff);
588 }
589 diff == 0
590}
591
592/// Every user on the server, and a counter saying when one last changed.
593///
594/// One lock over the lot, because every path that writes here is an operator
595/// typing a command and every path that reads is either the same or a
596/// connection authenticating. The command gate does not come through here at
597/// all: it holds a clone of the user it is running as, refreshed when the
598/// counter moves, which is what keeps a guarded server's hot path free of this
599/// lock. See [`Session::acl_user`].
600#[derive(Debug)]
601pub(crate) struct Users {
602 /// The users, sorted by name, which is the order `ACL LIST` reports.
603 ///
604 /// Redis keeps them in a radix tree and walks it in order, so the sorting is
605 /// not a nicety, it is the observable order of two commands.
606 table: Mutex<Vec<User>>,
607 /// Bumped whenever anything in the table changes.
608 generation: AtomicU64,
609 /// Whether a connection has to authenticate before it can do anything.
610 guarded: AtomicBool,
611 /// Whether any user here could be refused any command.
612 ///
613 /// The gate reads this and nothing else on a server nobody has written an
614 /// ACL for, which is every server that only ever set `requirepass` and every
615 /// server that did not even do that. Setting a password does not make it
616 /// true: the default user still has every permission, so no command can be
617 /// refused once a connection is past the password.
618 restricted: AtomicBool,
619 /// Whether a selector starts out allowed every channel.
620 ///
621 /// This is `acl-pubsub-default`, and it is here rather than with the other
622 /// settings because the only thing that reads it is the making of a
623 /// selector. It is false, meaning `resetchannels`, on a server nobody has
624 /// told otherwise, which has been the default since Redis 7.
625 open_channels: AtomicBool,
626}
627
628impl Default for Users {
629 fn default() -> Users {
630 Users {
631 table: Mutex::new(vec![User::default_user()]),
632 generation: AtomicU64::new(1),
633 guarded: AtomicBool::new(false),
634 restricted: AtomicBool::new(false),
635 open_channels: AtomicBool::new(false),
636 }
637 }
638}
639
640impl Users {
641 /// Run `f` over the table, and say that it changed if `f` says so.
642 ///
643 /// The two summaries are recomputed here rather than at every call site,
644 /// which is the point of routing every write through one function: there is
645 /// exactly one place that can leave them disagreeing with the table.
646 fn with<T>(&self, f: impl FnOnce(&mut Vec<User>) -> (bool, T)) -> T {
647 let mut held = self.table.lock().unwrap_or_else(|e| e.into_inner());
648 let (changed, out) = f(&mut held);
649 if changed {
650 let guarded = held
651 .binary_search_by(|u| u.name.as_slice().cmp(DEFAULT))
652 .is_ok_and(|at| !held[at].nopass || !held[at].enabled);
653 self.guarded.store(guarded, Relaxed);
654 self.restricted
655 .store(held.iter().any(|u| !u.unrestricted()), Relaxed);
656 // Released after the change and acquired before a reader looks at
657 // its copy, so a session that sees a new number sees the table that
658 // goes with it.
659 self.generation.fetch_add(1, Release);
660 }
661 out
662 }
663
664 /// Whether a selector made from here on starts out allowed every channel.
665 pub(crate) fn open_channels(&self) -> bool {
666 self.open_channels.load(Relaxed)
667 }
668
669 /// Set `acl-pubsub-default`, which changes nothing that already exists.
670 pub(crate) fn set_open_channels(&self, open: bool) {
671 self.open_channels.store(open, Relaxed);
672 }
673
674 /// A copy of the user called `name`, if there is one.
675 fn get(&self, name: &[u8]) -> Option<User> {
676 self.with(|table| {
677 let found = table
678 .binary_search_by(|u| u.name.as_slice().cmp(name))
679 .ok()
680 .map(|at| table[at].clone());
681 (false, found)
682 })
683 }
684
685 /// The number the session's copy of a user is stamped with.
686 fn generation(&self) -> u64 {
687 self.generation.load(Acquire)
688 }
689}
690
691impl Server {
692 /// Whether this server asks connections for a password.
693 ///
694 /// True when the default user has one, which is exactly what `requirepass`
695 /// means: a server whose default user is `nopass` lets an unauthenticated
696 /// connection straight through as that user, and a server whose default user
697 /// has a password does not.
698 #[must_use]
699 pub(crate) fn guarded(&self) -> bool {
700 self.acl.guarded.load(Relaxed)
701 }
702
703 /// Whether the gate has to ask the ACL anything at all.
704 #[must_use]
705 pub(crate) fn restricted(&self) -> bool {
706 self.acl.restricted.load(Relaxed)
707 }
708
709 /// Set or clear the default user's password, where an empty one clears it.
710 ///
711 /// This is the whole of `CONFIG SET requirepass`, and on a real server it is
712 /// the whole of it too: the config option is a way of writing one rule on
713 /// one user. Setting it drops any password the default user already had,
714 /// which is what the reference does and is worth knowing, because it means
715 /// `requirepass` and `ACL SETUSER default >pw` fight rather than add up.
716 pub fn set_password(&self, password: &[u8]) {
717 self.acl.with(|table| {
718 let Ok(at) = table.binary_search_by(|u| u.name.as_slice().cmp(DEFAULT)) else {
719 return (false, ());
720 };
721 let user = &mut table[at];
722 user.passwords.clear();
723 if password.is_empty() {
724 user.nopass = true;
725 } else {
726 user.nopass = false;
727 user.passwords.push(sha256::hex(password));
728 }
729 (true, ())
730 });
731 self.plain.set(password);
732 }
733
734 /// Hand the plain `requirepass` to `f`, which is how `CONFIG GET` writes it.
735 pub(crate) fn with_password<T>(&self, f: impl FnOnce(&[u8]) -> T) -> T {
736 self.plain.with(f)
737 }
738
739 /// The users, for the `ACL` command and for authentication.
740 pub(crate) fn users(&self) -> &Users {
741 &self.acl
742 }
743}
744
745/// The plain `requirepass`, kept beside the hash because `CONFIG GET` reports it.
746///
747/// A real server does the same and it is worth being explicit about why, because
748/// it looks like the hashing was pointless. It is not: the hash is what an ACL
749/// user's password is, and the config file and `ACL LIST` and `ACL GETUSER` all
750/// name it rather than this. This copy exists for one command, `CONFIG GET
751/// requirepass`, which a real server answers with the password itself, and a
752/// server that answered a hash there would break every tool that reads its own
753/// config back.
754#[derive(Debug, Default)]
755pub(crate) struct Plain {
756 /// Empty when there is no `requirepass`.
757 secret: Mutex<Vec<u8>>,
758}
759
760impl Plain {
761 /// Remember the password `CONFIG SET requirepass` was given.
762 fn set(&self, password: &[u8]) {
763 let mut held = self.secret.lock().unwrap_or_else(|e| e.into_inner());
764 held.clear();
765 held.extend_from_slice(password);
766 }
767
768 /// Hand it to `f`, borrowed rather than copied so it lives in one place.
769 fn with<T>(&self, f: impl FnOnce(&[u8]) -> T) -> T {
770 let held = self.secret.lock().unwrap_or_else(|e| e.into_inner());
771 f(&held)
772 }
773}
774
775/// Let this connection in as `user` if the password is right, and say whether
776/// it did.
777///
778/// Answers the same thing for a user that does not exist and a user whose
779/// password was wrong, on purpose: the difference between the two is a list of
780/// user names. Both write the same line to the ACL log, naming the user that
781/// was asked for, which is where an operator goes to find out that somebody has
782/// been guessing.
783///
784/// `args` is the whole command, and the only thing read out of it is the name at
785/// the front, because that is what the log calls the object: a failed `AUTH` and
786/// a failed `HELLO ... AUTH` are otherwise the same row.
787///
788/// The generation is read before the copy is taken rather than after. A write
789/// that lands in between then leaves the session stamped with a number that is
790/// too small, so its next command takes a fresh copy for nothing, and the other
791/// order would leave it stamped with a number that is too large and holding a
792/// user whose rules had already changed.
793pub(super) fn authenticate(
794 server: &Server,
795 session: &mut Session,
796 user: &[u8],
797 password: &[u8],
798 args: Args<'_>,
799 out: &Out,
800) -> bool {
801 let stamp = server.acl.generation();
802 let Some(found) = server.acl.get(user) else {
803 note_auth(server, session, out, args.get(0), user);
804 return false;
805 };
806 if !found.admits(password) {
807 note_auth(server, session, out, args.get(0), user);
808 return false;
809 }
810 session.become_user(stamp, found);
811 session.admit(true);
812 true
813}
814
815// ------------------------------------------------------------------ the rules
816
817/// Apply one rule to `user`, which is `ACLSetUser` written out.
818///
819/// The user level rules are here and everything else falls through to the
820/// selector. That split is Redis's and it is why `ACL SETUSER u (on)` is a
821/// syntax error: `on` is a fact about the account and a selector is a set of
822/// permissions, so there is nowhere in a selector to put it.
823fn set_user(user: &mut User, rule: &[u8], open: bool) -> std::result::Result<(), Bad> {
824 if rule.is_empty() {
825 return Ok(());
826 }
827 if word(rule, b"on") {
828 user.enabled = true;
829 } else if word(rule, b"off") {
830 user.enabled = false;
831 } else if word(rule, b"skip-sanitize-payload") {
832 user.skip_sanitize = true;
833 } else if word(rule, b"sanitize-payload") {
834 user.skip_sanitize = false;
835 } else if word(rule, b"nopass") {
836 user.nopass = true;
837 user.passwords.clear();
838 } else if word(rule, b"resetpass") {
839 user.nopass = false;
840 user.passwords.clear();
841 } else if rule[0] == b'>' || rule[0] == b'#' {
842 let hash = hash_of(rule)?;
843 if !user.passwords.contains(&hash) {
844 user.passwords.push(hash);
845 }
846 // A user with a password is not a `nopass` user, whatever it was.
847 user.nopass = false;
848 } else if rule[0] == b'<' || rule[0] == b'!' {
849 let hash = hash_of(rule)?;
850 let before = user.passwords.len();
851 user.passwords.retain(|held| *held != hash);
852 if user.passwords.len() == before {
853 return Err(Bad::NoSuchPassword);
854 }
855 } else if rule[0] == b'(' && rule[rule.len() - 1] == b')' {
856 let mut selector = Selector::new(open);
857 for word in split(&rule[1..rule.len() - 1]) {
858 set_selector(&mut selector, &word)?;
859 }
860 user.selectors.push(selector);
861 } else if rule[0] == b'(' {
862 return Err(Bad::Unmatched);
863 } else if word(rule, b"clearselectors") {
864 user.selectors.truncate(1);
865 } else if word(rule, b"reset") {
866 let name = std::mem::take(&mut user.name);
867 *user = User::new(&name, open);
868 } else {
869 return set_selector(&mut user.selectors[0], rule);
870 }
871 Ok(())
872}
873
874/// The hash a `>`, `#`, `<` or `!` rule names.
875///
876/// The first form of each pair is a password to hash and the second is a hash
877/// already, and the only difference between them is whether the sixty four
878/// characters are checked or produced.
879fn hash_of(rule: &[u8]) -> std::result::Result<[u8; 64], Bad> {
880 let rest = &rule[1..];
881 if rule[0] == b'>' || rule[0] == b'<' {
882 return Ok(sha256::hex(rest));
883 }
884 let ok = rest.len() == 64
885 && rest
886 .iter()
887 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(b));
888 if !ok {
889 return Err(Bad::Hash);
890 }
891 let mut hash = [0u8; 64];
892 hash.copy_from_slice(rest);
893 Ok(hash)
894}
895
896/// Apply one rule to a selector, which is `ACLSetSelector` written out.
897fn set_selector(selector: &mut Selector, rule: &[u8]) -> std::result::Result<(), Bad> {
898 if word(rule, b"allkeys") || rule == b"~*" {
899 selector.all_keys = true;
900 selector.patterns.clear();
901 } else if word(rule, b"resetkeys") {
902 selector.all_keys = false;
903 selector.patterns.clear();
904 } else if word(rule, b"allchannels") || rule == b"&*" {
905 selector.all_channels = true;
906 selector.channels.clear();
907 } else if word(rule, b"resetchannels") {
908 selector.all_channels = false;
909 selector.channels.clear();
910 } else if word(rule, b"allcommands") || rule == b"+@all" {
911 selector.reset_commands(true);
912 } else if word(rule, b"nocommands") || rule == b"-@all" {
913 selector.reset_commands(false);
914 } else if rule[0] == b'~' || rule[0] == b'%' {
915 add_pattern(selector, rule)?;
916 } else if rule[0] == b'&' {
917 if selector.all_channels {
918 return Err(Bad::AfterAllChannels);
919 }
920 let glob = &rule[1..];
921 if glob.contains(&b' ') {
922 return Err(Bad::Syntax);
923 }
924 if !selector.channels.iter().any(|held| held == glob) {
925 selector.channels.push(glob.to_vec());
926 }
927 } else if rule[0] == b'+' && rule.get(1) != Some(&b'@') {
928 add_command(selector, &rule[1..], true)?;
929 } else if rule[0] == b'-' && rule.get(1) != Some(&b'@') {
930 add_command(selector, &rule[1..], false)?;
931 } else if rule[0] == b'+' || rule[0] == b'-' {
932 let allow = rule[0] == b'+';
933 add_category(selector, &rule[2..], allow)?;
934 // Recorded with the sigil, because `@read` is what has to be written
935 // back and a bare `read` would read as a command name.
936 let mut name = Vec::with_capacity(rule.len() - 1);
937 name.push(b'@');
938 name.extend_from_slice(&rule[2..].to_ascii_lowercase());
939 selector.note(&name, allow);
940 } else {
941 return Err(Bad::Syntax);
942 }
943 Ok(())
944}
945
946/// A `~pattern` or `%RW~pattern` rule.
947fn add_pattern(selector: &mut Selector, rule: &[u8]) -> std::result::Result<(), Bad> {
948 if selector.all_keys {
949 return Err(Bad::AfterAllKeys);
950 }
951 let mut flags = 0u8;
952 let mut at = 1;
953 if rule[0] == b'%' {
954 // The letters run until the `~`, each may appear once, and there has to
955 // be at least one of them. `%~x` and `%RR~x` are both syntax errors. A
956 // rule that ends before the `~` is not: a bare `%R` is a pattern with an
957 // empty glob, which a real server takes and describes back as `%R~`.
958 let mut ok = true;
959 while at < rule.len() {
960 let letter = rule[at].to_ascii_uppercase();
961 if letter == b'R' && flags & READ == 0 {
962 flags |= READ;
963 } else if letter == b'W' && flags & WRITE == 0 {
964 flags |= WRITE;
965 } else if rule[at] == b'~' {
966 at += 1;
967 break;
968 } else {
969 ok = false;
970 break;
971 }
972 at += 1;
973 }
974 if flags == 0 || !ok {
975 return Err(Bad::Syntax);
976 }
977 } else {
978 flags = READ | WRITE;
979 }
980 let glob = &rule[at..];
981 if glob.contains(&b' ') {
982 return Err(Bad::Syntax);
983 }
984 // The same pattern given twice with different letters is one pattern good
985 // for both, which is why `%R~x %W~x` reads back as `~x`.
986 if let Some(held) = selector.patterns.iter_mut().find(|p| p.glob == glob) {
987 held.flags |= flags;
988 } else {
989 selector.patterns.push(Pattern {
990 flags,
991 glob: glob.to_vec(),
992 });
993 }
994 Ok(())
995}
996
997/// A `+command` or `+command|first` rule.
998fn add_command(selector: &mut Selector, name: &[u8], allow: bool) -> std::result::Result<(), Bad> {
999 let Some(bar) = name.iter().rposition(|b| *b == b'|') else {
1000 let Some(spec) = table::lookup(name) else {
1001 return Err(Bad::Unknown);
1002 };
1003 selector.set(table::index_of(spec), allow);
1004 selector.note(&name.to_ascii_lowercase(), allow);
1005 return Ok(());
1006 };
1007 let (head, first) = (&name[..bar], &name[bar + 1..]);
1008 // The command has to exist even though the first argument cannot be checked,
1009 // so `+nosuch|get` is refused and `+get|nosuch` is not.
1010 let Some(spec) = table::lookup(head) else {
1011 return Err(Bad::Unknown);
1012 };
1013 if head.contains(&b'|') {
1014 return Err(Bad::NestedFirstArg);
1015 }
1016 if first.is_empty() {
1017 return Err(Bad::Syntax);
1018 }
1019 let at = u16::try_from(table::index_of(spec)).unwrap_or(u16::MAX);
1020 if allow {
1021 // Nothing to do when the command is already allowed outright, which is
1022 // what makes `+get +get|set` the same user as `+get`.
1023 if !selector.bit(usize::from(at)) {
1024 selector.allow_first(at, first);
1025 }
1026 } else {
1027 // Taking one subcommand away only means anything for a command that has
1028 // subcommands. A real server refuses `-get|nope` because it looks the
1029 // whole name up in the table and GET has no such row, so this refuses it
1030 // too, and for the same answer.
1031 if !super::CONTAINERS.contains(&spec.name) {
1032 return Err(Bad::Unknown);
1033 }
1034 selector.deny_first(at, first);
1035 }
1036 selector.note(&name.to_ascii_lowercase(), allow);
1037 Ok(())
1038}
1039
1040/// A `+@category` or `-@category` rule.
1041fn add_category(selector: &mut Selector, name: &[u8], allow: bool) -> std::result::Result<(), Bad> {
1042 let Some(wanted) = category(name) else {
1043 return Err(Bad::Unknown);
1044 };
1045 for (at, spec) in table::COMMANDS.iter().enumerate() {
1046 if spec.acl.iter().any(|held| &held[1..] == wanted) {
1047 selector.set(at, allow);
1048 }
1049 }
1050 // And then the subcommands that are in the category without their container
1051 // being in it, one first argument at a time. `-@admin` has to reach CONFIG
1052 // GET and leave CONFIG HELP alone, and this is the only thing that knows the
1053 // two are different. See the note on `table::SUBCATS`.
1054 for (container, sub, cats) in table::SUBCATS {
1055 if !cats.iter().any(|held| &held[1..] == wanted) {
1056 continue;
1057 }
1058 let Some(spec) = table::lookup(container.as_bytes()) else {
1059 continue;
1060 };
1061 let at = u16::try_from(table::index_of(spec)).unwrap_or(u16::MAX);
1062 if !allow {
1063 selector.deny_first(at, sub.as_bytes());
1064 } else if !selector.bit(usize::from(at)) {
1065 selector.allow_first(at, sub.as_bytes());
1066 }
1067 }
1068 Ok(())
1069}
1070
1071/// The category called `name`, whatever case it was written in.
1072fn category(name: &[u8]) -> Option<&'static str> {
1073 CATEGORIES
1074 .iter()
1075 .find(|held| name.eq_ignore_ascii_case(held.as_bytes()))
1076 .copied()
1077}
1078
1079/// Whether `rule` is the keyword `word`, case insensitively.
1080fn word(rule: &[u8], keyword: &[u8]) -> bool {
1081 rule.eq_ignore_ascii_case(keyword)
1082}
1083
1084/// Split a selector's inside into rules on runs of spaces.
1085///
1086/// A selector arrives as one argument, `(+get ~k:*)`, so the words inside it
1087/// have to be taken apart here. Redis uses its config file splitter, which
1088/// understands quotes; this does not, because a key pattern with a space in it
1089/// is refused by the rule above anyway and a quoted rule has nowhere to be
1090/// useful.
1091fn split(inside: &[u8]) -> Vec<Vec<u8>> {
1092 inside
1093 .split(|b| *b == b' ')
1094 .filter(|part| !part.is_empty())
1095 .map(<[u8]>::to_vec)
1096 .collect()
1097}
1098
1099// ------------------------------------------------------------- the permissions
1100
1101/// Why a command was refused, ranked the way Redis ranks them.
1102///
1103/// The rank decides which of several selectors' complaints is reported: a user
1104/// with two selectors that both say no is told about the most specific refusal,
1105/// on the grounds that the selector which got as far as looking at a key was the
1106/// one the operator meant to use.
1107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1108pub(crate) enum Denied {
1109 /// Not allowed to run the command at all.
1110 Command,
1111 /// Allowed the command, not the key at this argument.
1112 Key(usize),
1113 /// Allowed the command, not the channel at this argument.
1114 Channel(usize),
1115}
1116
1117impl Denied {
1118 /// Redis's numeric ranking, which is what two refusals are compared on.
1119 fn rank(self) -> u8 {
1120 match self {
1121 Denied::Command => 1,
1122 Denied::Key(_) => 2,
1123 Denied::Channel(_) => 4,
1124 }
1125 }
1126
1127 /// The argument the refusal is about, or nought for the command itself.
1128 fn at(self) -> usize {
1129 match self {
1130 Denied::Command => 0,
1131 Denied::Key(at) | Denied::Channel(at) => at,
1132 }
1133 }
1134}
1135
1136/// Where a command's channels are, since they are not in the key specs.
1137///
1138/// Eight commands and no more, which is Redis's own table. The flags in Redis
1139/// are four and only two of them are checked, so what is kept here is the one
1140/// bit that matters, whether the argument is a pattern being subscribed to, plus
1141/// whether the command is checked at all: unsubscribing is always allowed,
1142/// because a client that has lost permission to a channel still has to be able
1143/// to stop listening to it.
1144struct Channels {
1145 /// The first argument that is a channel.
1146 first: usize,
1147 /// How many there are, or `None` for all the rest.
1148 count: Option<usize>,
1149 /// Whether these are patterns, which are matched literally rather than as
1150 /// globs. `PSUBSCRIBE news.*` needs the ACL to hold `&news.*` exactly, not
1151 /// something that matches it, because otherwise `&news.sport` would let a
1152 /// client subscribe to `news.*` and hear everything.
1153 pattern: bool,
1154}
1155
1156/// The channel arguments of `name`, if it has any that are checked.
1157fn channels_of(name: &str) -> Option<Channels> {
1158 let spec = match name {
1159 "subscribe" | "ssubscribe" => Channels {
1160 first: 1,
1161 count: None,
1162 pattern: false,
1163 },
1164 "psubscribe" => Channels {
1165 first: 1,
1166 count: None,
1167 pattern: true,
1168 },
1169 "publish" | "spublish" => Channels {
1170 first: 1,
1171 count: Some(1),
1172 pattern: false,
1173 },
1174 _ => return None,
1175 };
1176 Some(spec)
1177}
1178
1179/// Whether `selector` may reach `key` for `need`.
1180fn key_ok(selector: &Selector, key: &[u8], need: Access) -> bool {
1181 if selector.all_keys {
1182 return true;
1183 }
1184 selector
1185 .patterns
1186 .iter()
1187 .any(|p| Access::from_bits(p.flags).covers(need) && glob_matches(&p.glob, key))
1188}
1189
1190/// Whether `selector` may reach `channel`.
1191fn channel_ok(selector: &Selector, channel: &[u8], pattern: bool) -> bool {
1192 if selector.all_channels {
1193 return true;
1194 }
1195 selector.channels.iter().any(|held| {
1196 if pattern {
1197 held == channel
1198 } else {
1199 glob_matches(held, channel)
1200 }
1201 })
1202}
1203
1204/// Whether one selector allows this whole command, and what it objected to.
1205fn selector_ok(
1206 selector: &Selector,
1207 spec: &'static Spec,
1208 args: Args<'_>,
1209 base: usize,
1210) -> std::result::Result<(), Denied> {
1211 if !selector.all_commands && !spec.flags.contains(&"no_auth") {
1212 let at = table::index_of(spec);
1213 let index = u16::try_from(at).unwrap_or(u16::MAX);
1214 let sub = (args.len() > base + 1).then(|| args.get(base + 1));
1215 let named = |list: &[Vec<u8>]| {
1216 sub.is_some_and(|word| list.iter().any(|held| word.eq_ignore_ascii_case(held)))
1217 };
1218 if selector.bit(at) {
1219 // Allowed outright, unless this is the one subcommand that was taken
1220 // back off it.
1221 if named(selector.denied(index)) {
1222 return Err(Denied::Command);
1223 }
1224 } else if !named(selector.firsts(index)) {
1225 return Err(Denied::Command);
1226 }
1227 }
1228
1229 if !selector.all_keys && keyspec::takes_keys(spec, args, base) {
1230 let mut refused = None;
1231 keyspec::find(spec, args, base, &mut |run| {
1232 if refused.is_some() {
1233 return;
1234 }
1235 let need = run.need();
1236 for i in 0..run.count {
1237 let at = run.first + i * run.step;
1238 if at < args.len() && !key_ok(selector, args.get(at), need) {
1239 refused = Some(Denied::Key(at));
1240 return;
1241 }
1242 }
1243 });
1244 if let Some(why) = refused {
1245 return Err(why);
1246 }
1247 }
1248
1249 if !selector.all_channels
1250 && let Some(where_) = channels_of(spec.name)
1251 {
1252 let first = base + where_.first;
1253 let stop = where_
1254 .count
1255 .map_or(args.len(), |n| (first + n).min(args.len()));
1256 for at in first..stop {
1257 if !channel_ok(selector, args.get(at), where_.pattern) {
1258 return Err(Denied::Channel(at));
1259 }
1260 }
1261 }
1262 Ok(())
1263}
1264
1265/// Whether `user` may run this command, and what it objected to.
1266///
1267/// Every selector is tried and the first that says yes wins. When none does, the
1268/// refusal reported is the highest ranked one, and on a tie the one about the
1269/// argument furthest along, which is Redis's rule and is what makes a two
1270/// selector user complain about the key rather than the command.
1271pub(crate) fn permits(
1272 user: &User,
1273 spec: &'static Spec,
1274 args: Args<'_>,
1275 base: usize,
1276) -> std::result::Result<(), Denied> {
1277 // The whole of the cost on a server whose users can do anything, which is
1278 // every server nobody has written an ACL for.
1279 if let Some(root) = user.selectors.first()
1280 && root.all_commands
1281 && root.all_keys
1282 && root.all_channels
1283 {
1284 return Ok(());
1285 }
1286 let mut worst = Denied::Command;
1287 for selector in &user.selectors {
1288 match selector_ok(selector, spec, args, base) {
1289 Ok(()) => return Ok(()),
1290 Err(why) => {
1291 if why.rank() > worst.rank()
1292 || (why.rank() == worst.rank() && why.at() > worst.at())
1293 {
1294 worst = why;
1295 }
1296 }
1297 }
1298 }
1299 Err(worst)
1300}
1301
1302/// The sentence a refusal is reported with.
1303///
1304/// `verbose` is the difference between the gate and `ACL DRYRUN`. The gate is
1305/// answering a client that has just been told no and naming the key it asked
1306/// about would tell it which keys exist, so the terse form says only that a key
1307/// was the problem. `ACL DRYRUN` is answering an operator who asked the
1308/// question on purpose and wants to know which key, so it names it.
1309pub(crate) fn refusal(
1310 why: Denied,
1311 user: &[u8],
1312 spec: &'static Spec,
1313 args: Args<'_>,
1314 base: usize,
1315 verbose: bool,
1316) -> String {
1317 let name = String::from_utf8_lossy(user);
1318 match why {
1319 Denied::Command => {
1320 let sub = container_name(spec, args, base);
1321 format!("User {name} has no permissions to run the '{sub}' command")
1322 }
1323 Denied::Key(at) if verbose => {
1324 let key = String::from_utf8_lossy(args.get(at));
1325 format!("User {name} has no permissions to access the '{key}' key")
1326 }
1327 Denied::Key(_) => "No permissions to access a key".to_string(),
1328 Denied::Channel(at) if verbose => {
1329 let channel = String::from_utf8_lossy(args.get(at));
1330 format!("User {name} has no permissions to access the '{channel}' channel")
1331 }
1332 Denied::Channel(_) => "No permissions to access a channel".to_string(),
1333 }
1334}
1335
1336/// The command's name as a refusal spells it, which is `acl|list` and not `acl`.
1337fn container_name(spec: &'static Spec, args: Args<'_>, base: usize) -> String {
1338 if super::CONTAINERS.contains(&spec.name) && args.len() > base + 1 {
1339 let sub = String::from_utf8_lossy(args.get(base + 1)).to_lowercase();
1340 return format!("{}|{sub}", spec.name);
1341 }
1342 spec.name.to_string()
1343}
1344
1345/// Who a connection is, and the copy of that user its commands are checked
1346/// against.
1347///
1348/// Boxed on the session, because it is three allocations and a connection on a
1349/// server with no ACL never reads any of them past the first command.
1350#[derive(Debug)]
1351pub(crate) struct Identity {
1352 /// The name, which is what `ACL WHOAMI` and `CLIENT INFO` report.
1353 name: Vec<u8>,
1354 /// The generation the copy below was taken at.
1355 stamp: u64,
1356 /// The copy itself.
1357 user: User,
1358}
1359
1360impl Default for Identity {
1361 /// A connection starts as the default user, with a copy stamped nought so
1362 /// that the first command it sends fetches the real one.
1363 fn default() -> Identity {
1364 Identity {
1365 name: DEFAULT.to_vec(),
1366 stamp: 0,
1367 user: User::default_user(),
1368 }
1369 }
1370}
1371
1372impl Session {
1373 /// The name this connection is authenticated as.
1374 pub(crate) fn acl_name(&self) -> &[u8] {
1375 &self.acl.name
1376 }
1377
1378 /// Say that this connection has authenticated as `user`.
1379 ///
1380 /// Written to the row as well as here, because `CLIENT LIST` reports the
1381 /// user of every connection and runs on whichever thread the client asking
1382 /// is on, which is very often not this one.
1383 pub(super) fn become_user(&mut self, stamp: u64, user: User) {
1384 yo_alloc::allow(|| {
1385 self.acl.name.clear();
1386 self.acl.name.extend_from_slice(&user.name);
1387 });
1388 self.acl.stamp = stamp;
1389 self.acl.user = user;
1390 self.sock.set_text(|text| &mut text.user, &self.acl.name);
1391 }
1392
1393 /// Put the connection back on the default user, which is what `RESET` does.
1394 pub(super) fn forget_user(&mut self) {
1395 *self.acl = Identity::default();
1396 // Empty rather than the name, which is how the row spells the default
1397 // user, so a connection that never authenticated costs nothing.
1398 self.sock.set_text(|text| &mut text.user, b"");
1399 }
1400
1401 /// The generation the copy was taken at.
1402 fn acl_stamp(&self) -> u64 {
1403 self.acl.stamp
1404 }
1405
1406 /// Take a fresh copy, or keep the one there is if the user has gone.
1407 fn acl_refresh(&mut self, now: u64, fresh: Option<User>) {
1408 self.acl.stamp = now;
1409 if let Some(user) = fresh {
1410 self.acl.user = user;
1411 }
1412 }
1413
1414 /// The copy itself.
1415 fn acl_cached(&self) -> &User {
1416 &self.acl.user
1417 }
1418}
1419
1420/// What the connection is running as, refreshed if the table has moved on.
1421///
1422/// A session keeps a copy of its user rather than a handle into the table,
1423/// because the alternative is taking the table's lock on every command on every
1424/// connection. The copy is stamped with the generation it was taken at and
1425/// replaced when that number moves, so a `SETUSER` that tightens a user's
1426/// permissions reaches every connection already authenticated as it, on that
1427/// connection's next command. That is what a real server does, and it is the
1428/// half of `SETUSER` that would be easy to get wrong: the point of tightening a
1429/// user is usually the connection that is already open.
1430///
1431/// A user deleted out from under a connection leaves the copy in place, which is
1432/// also Redis's behaviour: `ACL DELUSER` closes those connections rather than
1433/// leaving them running as a user that no longer exists.
1434fn current<'a>(server: &Server, session: &'a mut Session) -> &'a User {
1435 let now = server.acl.generation();
1436 if session.acl_stamp() != now {
1437 let fresh = server.acl.get(session.acl_name());
1438 session.acl_refresh(now, fresh);
1439 }
1440 session.acl_cached()
1441}
1442
1443/// The gate, which is every command on a server that has an ACL worth checking.
1444///
1445/// `None` means the command may run. The refusal is written by the caller
1446/// rather than here, because it is one of the errors that kills an open
1447/// transaction and the caller is what knows about transactions.
1448pub(super) fn gate(
1449 server: &Server,
1450 session: &mut Session,
1451 spec: &'static Spec,
1452 args: Args<'_>,
1453 out: &Out,
1454) -> Option<String> {
1455 // The user is borrowed out of the session and the log wants the session
1456 // back, so everything read off the user is copied out here and the borrow
1457 // ends with the block. It costs one name and one sentence on a command that
1458 // has just been refused, which is not a path anything is timed on.
1459 let (why, said) = {
1460 let user = current(server, session);
1461 let why = permits(user, spec, args, 0).err()?;
1462 // The code is in the line rather than in front of it, because the line
1463 // goes two places: straight into the reply, and spliced into the
1464 // `EXECABORT` an `EXEC` gets. The same reason `NOAUTH` is a whole line.
1465 (why, refusal(why, &user.name, spec, args, 0, false))
1466 };
1467 yo_alloc::allow(|| {
1468 let object = match why {
1469 // The name a refusal spells, which is `acl|list` and not `acl`, and
1470 // is the same string the sentence above names.
1471 Denied::Command => container_name(spec, args, 0).into_bytes(),
1472 Denied::Key(at) | Denied::Channel(at) => args.get(at).to_vec(),
1473 };
1474 let name = session.acl_name().to_vec();
1475 server
1476 .acl_log()
1477 .note(server, session, out, why.reason(), object, name);
1478 Some(format!("NOPERM {said}"))
1479 })
1480}
1481
1482// ----------------------------------------------------------------- the log
1483
1484/// How long two refusals can be apart and still be counted as the same one.
1485///
1486/// A minute, which is Redis's `ACL_LOG_GROUPING_MAX_TIME_DELTA`. The point of it
1487/// is that a client stuck in a retry loop against a command it may not run fills
1488/// the log with one entry and a count rather than with a hundred and twenty eight
1489/// copies of itself, and the operator who comes to look still sees what else
1490/// happened.
1491const GROUPING_MS: u64 = 60_000;
1492
1493/// How far back a new refusal looks for one it matches.
1494///
1495/// Ten, which is Redis's `toscan`. It is a bound on the work rather than on the
1496/// grouping: a refusal that would have matched the eleventh entry gets its own
1497/// row instead, and the operator sees two rows where a longer scan would have
1498/// shown one.
1499const SCAN: usize = 10;
1500
1501/// Why something was refused, which is what `ACL LOG` reports as `reason`.
1502///
1503/// Redis has a fifth, `tls-cert`, for a client certificate that named a user the
1504/// server has not got. There is no TLS here, so there is no way to reach it and
1505/// no value for it, and the counter it feeds in `INFO stats` is reported as the
1506/// nought it would always be.
1507#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1508pub(crate) enum Reason {
1509 /// A command the user may not run.
1510 Command,
1511 /// A key the user may not reach.
1512 Key,
1513 /// A channel the user may not reach.
1514 Channel,
1515 /// A password that did not get in.
1516 Auth,
1517}
1518
1519impl Reason {
1520 /// The word `ACL LOG` prints.
1521 fn name(self) -> &'static str {
1522 match self {
1523 Reason::Command => "command",
1524 Reason::Key => "key",
1525 Reason::Channel => "channel",
1526 Reason::Auth => "auth",
1527 }
1528 }
1529}
1530
1531impl Denied {
1532 /// The reason a refused command is logged under.
1533 fn reason(self) -> Reason {
1534 match self {
1535 Denied::Command => Reason::Command,
1536 Denied::Key(_) => Reason::Key,
1537 Denied::Channel(_) => Reason::Channel,
1538 }
1539 }
1540}
1541
1542/// Where the refused command came from, which is what `ACL LOG` reports as
1543/// `context`.
1544///
1545/// Redis has a fourth, `module`, for a command a module ran on a user's behalf.
1546/// Nothing here runs a command from a module, so there is no way to reach it.
1547#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1548pub(crate) enum Context {
1549 /// The client sent it.
1550 Toplevel,
1551 /// The client was queueing it into a transaction.
1552 Multi,
1553 /// A script called it.
1554 Lua,
1555}
1556
1557impl Context {
1558 /// The word `ACL LOG` prints.
1559 fn name(self) -> &'static str {
1560 match self {
1561 Context::Toplevel => "toplevel",
1562 Context::Multi => "multi",
1563 Context::Lua => "lua",
1564 }
1565 }
1566
1567 /// Where this connection's next refusal is coming from.
1568 ///
1569 /// A command being queued is `multi` and a command `EXEC` is replaying is
1570 /// not, which is Redis's answer and falls out of where the gate sits: the
1571 /// check happens as the command is queued, and the replay does not go
1572 /// through it again.
1573 fn of(session: &Session) -> Context {
1574 if session.scripted {
1575 Context::Lua
1576 } else if session.in_multi() {
1577 Context::Multi
1578 } else {
1579 Context::Toplevel
1580 }
1581 }
1582}
1583
1584/// One refusal, which is one row of `ACL LOG`.
1585#[derive(Debug)]
1586struct Entry {
1587 /// How many refusals have been folded into this row.
1588 count: u64,
1589 reason: Reason,
1590 context: Context,
1591 /// What was refused: the command's name, or the key or channel it named, or
1592 /// the command the password was sent with.
1593 object: Vec<u8>,
1594 /// Who was refused, which for a failed `AUTH` is who they said they were.
1595 username: Vec<u8>,
1596 /// When the last refusal folded into this row happened.
1597 ctime: u64,
1598 /// The `CLIENT INFO` line of the connection that was refused.
1599 cinfo: String,
1600 /// Which refusal this was, counted over the life of the server.
1601 entry_id: u64,
1602 /// When the first refusal folded into this row happened.
1603 created: u64,
1604}
1605
1606/// The refusals, newest first, and the counters that go with them.
1607///
1608/// Its own lock rather than a corner of [`Users`], because the two are touched
1609/// at opposite moments: the table is read on the command path and written by an
1610/// operator, and this is written only when something has already gone wrong. A
1611/// server whose users can do what they are asking for never takes this lock at
1612/// all.
1613#[derive(Debug)]
1614pub(crate) struct Log {
1615 /// Newest at the front, which is the order `ACL LOG` reports.
1616 entries: Mutex<VecDeque<Entry>>,
1617 /// How many rows have ever been opened, which is the next `entry-id`.
1618 next_id: AtomicU64,
1619 /// `acllog-max-len`, and nought means keep nothing.
1620 max_len: AtomicU64,
1621 /// The four counters `INFO stats` reports, in the order [`Reason`] declares
1622 /// them.
1623 denied: [AtomicU64; 4],
1624}
1625
1626impl Default for Log {
1627 fn default() -> Log {
1628 Log {
1629 entries: Mutex::new(VecDeque::new()),
1630 next_id: AtomicU64::new(0),
1631 // A hundred and twenty eight, which is the reference's default.
1632 max_len: AtomicU64::new(128),
1633 denied: Default::default(),
1634 }
1635 }
1636}
1637
1638impl Log {
1639 /// Write down a refusal.
1640 ///
1641 /// The counter moves whatever the length allows, which is Redis's order and
1642 /// matters: an operator who turned the log off with `acllog-max-len 0` still
1643 /// gets the numbers in `INFO stats` and only gives up the detail.
1644 fn note(
1645 &self,
1646 server: &Server,
1647 session: &Session,
1648 out: &Out,
1649 reason: Reason,
1650 object: Vec<u8>,
1651 username: Vec<u8>,
1652 ) {
1653 self.denied[reason as usize].fetch_add(1, Relaxed);
1654 let max = self.max_len.load(Relaxed);
1655 let mut held = self.entries.lock().unwrap_or_else(|e| e.into_inner());
1656 if max == 0 {
1657 held.clear();
1658 return;
1659 }
1660 let context = Context::of(session);
1661 let now = server.now_ms();
1662 // The whole `CLIENT INFO` line of the connection, which is what makes
1663 // the log worth reading: the row says a user was refused and this says
1664 // which connection, from which address, running which library. Without
1665 // the newline on the end, which `CLIENT INFO` puts there itself and this
1666 // does not, so the two fields differ by that one byte on a real server
1667 // too.
1668 let mut cinfo = super::client::report(server, session, out.proto(), out.len());
1669 if cinfo.ends_with('\n') {
1670 cinfo.pop();
1671 }
1672 let matched = held.iter().take(SCAN).position(|e| {
1673 e.reason == reason
1674 && e.context == context
1675 && e.object == object
1676 && e.username == username
1677 && now.abs_diff(e.ctime) <= GROUPING_MS
1678 });
1679 if let Some(at) = matched {
1680 // Moved to the front as well as bumped, so the log is in order of
1681 // when something last happened rather than of when it first did.
1682 let mut entry = held.remove(at).expect("the position came from the deque");
1683 entry.cinfo = cinfo;
1684 entry.ctime = now;
1685 entry.count += 1;
1686 held.push_front(entry);
1687 return;
1688 }
1689 held.push_front(Entry {
1690 count: 1,
1691 reason,
1692 context,
1693 object,
1694 username,
1695 ctime: now,
1696 cinfo,
1697 entry_id: self.next_id.fetch_add(1, Relaxed),
1698 created: now,
1699 });
1700 while held.len() as u64 > max {
1701 held.pop_back();
1702 }
1703 }
1704
1705 /// `ACL LOG RESET`.
1706 ///
1707 /// The ids do not go back to nought, which is Redis's behaviour and is the
1708 /// useful one: a tool that remembers the last id it read must not be shown
1709 /// that id again attached to a different refusal.
1710 fn clear(&self) {
1711 self.entries
1712 .lock()
1713 .unwrap_or_else(|e| e.into_inner())
1714 .clear();
1715 }
1716
1717 /// The five numbers `INFO stats` reports, in the order it reports them.
1718 pub(crate) fn counters(&self) -> [u64; 5] {
1719 [
1720 self.denied[Reason::Auth as usize].load(Relaxed),
1721 self.denied[Reason::Command as usize].load(Relaxed),
1722 self.denied[Reason::Key as usize].load(Relaxed),
1723 self.denied[Reason::Channel as usize].load(Relaxed),
1724 // `acl_access_denied_tls_cert`, which cannot move without TLS.
1725 0,
1726 ]
1727 }
1728
1729 /// `acllog-max-len`.
1730 pub(crate) fn max_len(&self) -> u64 {
1731 self.max_len.load(Relaxed)
1732 }
1733
1734 /// `CONFIG SET acllog-max-len`.
1735 ///
1736 /// Nothing is trimmed here, which is the reference's behaviour: lowering the
1737 /// number decides how long the log is allowed to be after the next refusal,
1738 /// and until then `ACL LOG` still reports what is already in it.
1739 pub(crate) fn set_max_len(&self, n: u64) {
1740 self.max_len.store(n, Relaxed);
1741 }
1742}
1743
1744impl Server {
1745 /// The refusals, for the gate and for `ACL LOG`.
1746 pub(crate) fn acl_log(&self) -> &Log {
1747 &self.acllog
1748 }
1749}
1750
1751/// Write down a password that did not get in.
1752///
1753/// Its own function rather than a branch in [`authenticate`], because the two
1754/// things it needs that the rest of that path does not are the command the
1755/// password arrived with and the buffer the reply is going into.
1756fn note_auth(server: &Server, session: &Session, out: &Out, said: &[u8], user: &[u8]) {
1757 yo_alloc::allow(|| {
1758 server.acl_log().note(
1759 server,
1760 session,
1761 out,
1762 Reason::Auth,
1763 // The command as the client spelled it, which is `AUTH` or `HELLO`
1764 // in whatever case it was typed. That is what Redis logs, and it is
1765 // the only way to tell the two apart in the log afterwards.
1766 said.to_vec(),
1767 user.to_vec(),
1768 );
1769 });
1770}
1771
1772// ------------------------------------------------------------- the command
1773
1774/// The subcommands and how many arguments each takes, counting `ACL` itself.
1775///
1776/// A real server gives every one of these a row in the command table with its
1777/// own arity, and enforces it before the body is reached, which is why `ACL
1778/// WHOAMI x` is a wrong number of arguments and `ACL LOG 1 2` is an unknown
1779/// subcommand: the second one passes its own arity check and then falls off the
1780/// end of the parse. There is no subcommand table here yet, so the arities live
1781/// in this list and the same two sentences come out. It folds into D-114.
1782const ARITIES: [(&[u8], i32); 12] = [
1783 (b"cat", -2),
1784 (b"deluser", -3),
1785 (b"dryrun", -4),
1786 (b"genpass", -2),
1787 (b"getuser", 3),
1788 (b"help", 2),
1789 (b"list", 2),
1790 (b"load", 2),
1791 (b"log", -2),
1792 (b"save", 2),
1793 (b"setuser", -3),
1794 (b"users", 2),
1795 // `whoami` is not here because its arity is exactly two, which is the same
1796 // check the fallthrough below makes, and a row for it would be dead weight.
1797];
1798
1799/// `ACL <subcommand> ...`.
1800pub(super) fn execute(
1801 server: &Server,
1802 session: &mut Session,
1803 args: Args<'_>,
1804 out: &mut Out,
1805) -> Result<()> {
1806 let sub = args.get(1);
1807 // The arity of the subcommand before anything else, which is where a real
1808 // server makes this decision: the row is found and checked in
1809 // `processCommand`, so a wrong count is refused before the body runs.
1810 if let Some((name, arity)) = ARITIES
1811 .iter()
1812 .find(|(name, _)| sub.eq_ignore_ascii_case(name))
1813 {
1814 let n = args.len() as i32;
1815 if (*arity > 0 && n != *arity) || (*arity < 0 && n < -*arity) {
1816 let name = std::str::from_utf8(name).unwrap_or("acl");
1817 return Err(args::wrong_arity_sub("acl", name));
1818 }
1819 }
1820
1821 if is(sub, b"whoami") {
1822 if args.len() != 2 {
1823 return Err(args::wrong_arity_sub("acl", "whoami"));
1824 }
1825 out.bulk(session.acl_name());
1826 } else if (is(sub, b"load") || is(sub, b"save")) && server.aclfile().is_none() {
1827 // In front of both bodies and in front of everything either of them
1828 // would check, which is where the reference puts it: a server with no
1829 // ACL file says so and says nothing about what was asked of it.
1830 return Err(Error::new(
1831 Code::Invalid,
1832 "This Redis instance is not configured to use an ACL file. You may want to specify users via the ACL SETUSER command and then issue a CONFIG REWRITE (assuming you have a Redis configuration file set) in order to store users in the Redis configuration.",
1833 ));
1834 } else if is(sub, b"load") {
1835 return yo_alloc::allow(|| load(server, out));
1836 } else if is(sub, b"save") {
1837 return yo_alloc::allow(|| save(server, out));
1838 } else if is(sub, b"log") {
1839 return yo_alloc::allow(|| log(server, args, out));
1840 } else if is(sub, b"cat") {
1841 cat(args, out)?;
1842 } else if is(sub, b"list") {
1843 yo_alloc::allow(|| {
1844 server.users().with(|table| {
1845 out.array(table.len());
1846 for user in table.iter() {
1847 let mut line = b"user ".to_vec();
1848 line.extend_from_slice(&user.describe());
1849 out.bulk(&line);
1850 }
1851 (false, ())
1852 });
1853 });
1854 } else if is(sub, b"users") {
1855 server.users().with(|table| {
1856 out.array(table.len());
1857 for user in table.iter() {
1858 out.bulk(&user.name);
1859 }
1860 (false, ())
1861 });
1862 } else if is(sub, b"getuser") {
1863 yo_alloc::allow(|| getuser(server, args, out));
1864 } else if is(sub, b"setuser") {
1865 return yo_alloc::allow(|| setuser(server, args, out));
1866 } else if is(sub, b"deluser") {
1867 return deluser(server, args, out);
1868 } else if is(sub, b"genpass") {
1869 genpass(args, out)?;
1870 } else if is(sub, b"dryrun") {
1871 return yo_alloc::allow(|| dryrun(server, args, out));
1872 } else if is(sub, b"help") {
1873 super::server::help(out, HELP);
1874 } else {
1875 return Err(args::unknown_subcommand(sub, "ACL"));
1876 }
1877 Ok(())
1878}
1879
1880/// `ACL CAT` and `ACL CAT <category>`.
1881///
1882/// With no argument this is the list of categories, and with one it is the
1883/// commands in it, in table order. Redis walks a hash table there and so reports
1884/// an order that is neither sorted nor stable across versions, so matching it
1885/// exactly is not a thing to aim at; what a client can rely on is the set, and
1886/// this reports the same set for every category the two servers share.
1887fn cat(args: Args<'_>, out: &mut Out) -> Result<()> {
1888 if args.len() == 2 {
1889 out.array(CATEGORIES.len());
1890 for name in CATEGORIES {
1891 out.bulk(name.as_bytes());
1892 }
1893 return Ok(());
1894 }
1895 if args.len() > 3 {
1896 return Err(args::subcommand_syntax(args.get(1), "ACL"));
1897 }
1898 let Some(wanted) = category(args.get(2)) else {
1899 return Err(yo_alloc::allow(|| {
1900 Error::fmt(
1901 Code::Invalid,
1902 format_args!(
1903 "Unknown category '{}'",
1904 String::from_utf8_lossy(args.get(2))
1905 ),
1906 )
1907 }));
1908 };
1909 // The header goes on afterwards, because how many commands are in a
1910 // category is not a thing the table can be asked without walking it.
1911 let start = out.len();
1912 let mut n = 0;
1913 for spec in table::COMMANDS {
1914 if spec.acl.iter().any(|held| &held[1..] == wanted) {
1915 out.bulk(spec.name.as_bytes());
1916 n += 1;
1917 }
1918 }
1919 out.close_array(start, n);
1920 Ok(())
1921}
1922
1923/// `ACL GETUSER <username>`.
1924fn getuser(server: &Server, args: Args<'_>, out: &mut Out) {
1925 let Some(user) = server.users().get(args.get(2)) else {
1926 out.nil();
1927 return;
1928 };
1929 // Six fields: the two about the account, the root selector's three repeated
1930 // at the top level for the clients that were written before selectors
1931 // existed, and the selectors themselves.
1932 out.map(6);
1933 out.bulk(b"flags");
1934 // Only the flags that belong to the account. The selector flags are named in
1935 // the same table and an older server did list them here, but 8.10.1 does not,
1936 // and a client that wants to know whether a user can reach every key reads
1937 // the keys field rather than counting words in this set.
1938 let flags = user.flags();
1939 let root = &user.selectors[0];
1940 out.set(flags.len());
1941 for flag in &flags {
1942 out.bulk(flag.as_bytes());
1943 }
1944 out.bulk(b"passwords");
1945 out.array(user.passwords.len());
1946 for hash in &user.passwords {
1947 out.bulk(hash);
1948 }
1949 describe_selector(root, out);
1950 out.bulk(b"selectors");
1951 out.array(user.selectors.len() - 1);
1952 for selector in &user.selectors[1..] {
1953 out.map(3);
1954 describe_selector(selector, out);
1955 }
1956}
1957
1958/// The three fields a selector contributes to `ACL GETUSER`.
1959fn describe_selector(selector: &Selector, out: &mut Out) {
1960 out.bulk(b"commands");
1961 out.bulk(&selector.describe_commands());
1962 out.bulk(b"keys");
1963 out.bulk(&selector.describe_keys());
1964 out.bulk(b"channels");
1965 out.bulk(&selector.describe_channels());
1966}
1967
1968/// `ACL SETUSER <username> [rule ...]`.
1969///
1970/// Every rule is applied to a copy and the copy replaces the user only if all of
1971/// them worked, so a `SETUSER` that fails halfway leaves nothing behind. That is
1972/// worth more than it sounds: the failure case is an operator tightening a
1973/// user's permissions and mistyping one rule, and a server that applied the
1974/// first half would have left the user with the new restrictions and none of the
1975/// new grants.
1976fn setuser(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
1977 let name = args.get(2);
1978 if name.contains(&b' ') || name.contains(&0) {
1979 return Err(Error::new(
1980 Code::Invalid,
1981 "Usernames can't contain spaces or null characters",
1982 ));
1983 }
1984 let rules = merge(args, 3)?;
1985 let open = server.users().open_channels();
1986 let outcome = server.users().with(|table| {
1987 let at = table.binary_search_by(|u| u.name.as_slice().cmp(name));
1988 let mut staged = match at {
1989 Ok(at) => table[at].clone(),
1990 Err(_) => User::new(name, open),
1991 };
1992 for rule in &rules {
1993 if let Err(why) = set_user(&mut staged, rule, open) {
1994 return (false, Err((rule.clone(), why)));
1995 }
1996 }
1997 match at {
1998 Ok(at) => table[at] = staged,
1999 Err(at) => table.insert(at, staged),
2000 }
2001 (true, Ok(()))
2002 });
2003 match outcome {
2004 Ok(()) => {
2005 out.ok();
2006 Ok(())
2007 }
2008 Err((rule, why)) => Err(Error::fmt(
2009 Code::Invalid,
2010 format_args!(
2011 "Error in ACL SETUSER modifier '{}': {}",
2012 String::from_utf8_lossy(&rule),
2013 why.text()
2014 ),
2015 )),
2016 }
2017}
2018
2019/// Join the arguments from `from` on, gluing a selector back together.
2020///
2021/// A selector is one rule and a client sends it as several arguments, because
2022/// `(+get ~k:*)` has a space in it and the wire has no way to say that was meant
2023/// as one word. So a `(` that does not end in `)` swallows the arguments after
2024/// it until one does. An opening bracket that is never closed is the one rule
2025/// error reported in its own sentence rather than as a modifier error, because
2026/// there is no single modifier to blame.
2027fn merge(args: Args<'_>, from: usize) -> Result<Vec<Vec<u8>>> {
2028 let words = (from..args.len()).map(|i| args.get(i));
2029 glue(words).map_err(|at| {
2030 Error::fmt(
2031 Code::Invalid,
2032 format_args!(
2033 "Unmatched parenthesis in acl selector starting at '{}'.",
2034 String::from_utf8_lossy(args.get(from + at))
2035 ),
2036 )
2037 })
2038}
2039
2040/// The half of [`merge`] the ACL file wants too, which is everything but the
2041/// sentence.
2042///
2043/// The error is which word opened the bracket that was never closed, because the
2044/// two callers name it differently: the command quotes the word and the file
2045/// gives the line it was on.
2046fn glue<'a>(words: impl Iterator<Item = &'a [u8]>) -> std::result::Result<Vec<Vec<u8>>, usize> {
2047 let mut out: Vec<Vec<u8>> = Vec::new();
2048 let mut open: Option<usize> = None;
2049 for (i, word) in words.enumerate() {
2050 if open.is_none() && word.first() == Some(&b'(') && word.last() != Some(&b')') {
2051 open = Some(i);
2052 out.push(word.to_vec());
2053 continue;
2054 }
2055 if open.is_some() {
2056 let held = out.last_mut().expect("an open bracket left a rule behind");
2057 held.push(b' ');
2058 held.extend_from_slice(word);
2059 if word.last() == Some(&b')') {
2060 open = None;
2061 }
2062 continue;
2063 }
2064 out.push(word.to_vec());
2065 }
2066 match open {
2067 Some(at) => Err(at),
2068 None => Ok(out),
2069 }
2070}
2071
2072/// `ACL DELUSER <username> [<username> ...]`.
2073///
2074/// The default user is checked for over the whole list before anything is
2075/// deleted, so `ACL DELUSER alice default` deletes neither. Redis does the same
2076/// and the reason is the same as `SETUSER`'s: a half done change to who may
2077/// reach a server is worse than no change.
2078fn deluser(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
2079 for i in 2..args.len() {
2080 if args.get(i) == DEFAULT {
2081 return Err(Error::new(
2082 Code::Invalid,
2083 "The 'default' user cannot be removed",
2084 ));
2085 }
2086 }
2087 let gone = server.users().with(|table| {
2088 let mut gone = 0;
2089 for i in 2..args.len() {
2090 if let Ok(at) = table.binary_search_by(|u| u.name.as_slice().cmp(args.get(i))) {
2091 table.remove(at);
2092 gone += 1;
2093 }
2094 }
2095 (gone > 0, gone)
2096 });
2097 out.int(gone);
2098 Ok(())
2099}
2100
2101/// `ACL GENPASS [<bits>]`.
2102///
2103/// The bytes come from the operating system rather than from the engine's own
2104/// generator, which is seeded and reproducible on purpose. See
2105/// [`yo_common::entropy`].
2106fn genpass(args: Args<'_>, out: &mut Out) -> Result<()> {
2107 if args.len() > 3 {
2108 return Err(args::subcommand_syntax(args.get(1), "ACL"));
2109 }
2110 let bits = if args.len() == 3 { args.int(2)? } else { 256 };
2111 if bits <= 0 || bits > 4096 {
2112 return Err(Error::new(
2113 Code::Invalid,
2114 "ACL GENPASS argument must be the number of bits for the output password, a positive number up to 4096",
2115 ));
2116 }
2117 // One hex character is four bits, rounded up, so `GENPASS 10` is three
2118 // characters and holds twelve bits rather than ten. That is the reference's
2119 // arithmetic and it errs towards more entropy than was asked for.
2120 let chars = ((bits + 3) / 4) as usize;
2121 let mut raw = [0u8; 4096 / 8 + 1];
2122 let bytes = chars.div_ceil(2);
2123 yo_common::entropy::fill(&mut raw[..bytes]);
2124 const DIGITS: &[u8; 16] = b"0123456789abcdef";
2125 let mut hex = [0u8; 1024];
2126 for (i, slot) in hex[..chars].iter_mut().enumerate() {
2127 let byte = raw[i / 2];
2128 *slot = DIGITS[usize::from(if i % 2 == 0 { byte >> 4 } else { byte & 0xf })];
2129 }
2130 out.bulk(&hex[..chars]);
2131 Ok(())
2132}
2133
2134/// `ACL DRYRUN <username> <command> [<arg> ...]`.
2135///
2136/// The same question the gate asks, asked out loud. The answer is a bulk string
2137/// rather than an error even when it is a refusal, because the command
2138/// succeeded: it was asked whether something would be allowed and it found out.
2139fn dryrun(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
2140 let Some(user) = server.users().get(args.get(2)) else {
2141 return Err(Error::fmt(
2142 Code::Invalid,
2143 format_args!("User '{}' not found", String::from_utf8_lossy(args.get(2))),
2144 ));
2145 };
2146 let Some(spec) = table::lookup(args.get(3)) else {
2147 return Err(Error::fmt(
2148 Code::Invalid,
2149 format_args!(
2150 "Command '{}' not found",
2151 String::from_utf8_lossy(args.get(3))
2152 ),
2153 ));
2154 };
2155 if !table::arity_ok(spec, args.len() - 3) {
2156 return Err(args::wrong_arity(spec.name));
2157 }
2158 match permits(&user, spec, args, 3) {
2159 Ok(()) => out.ok(),
2160 Err(why) => {
2161 let text = refusal(why, &user.name, spec, args, 3, true);
2162 out.bulk(text.as_bytes());
2163 }
2164 }
2165 Ok(())
2166}
2167
2168/// `ACL LOG [<count> | RESET]`.
2169fn log(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
2170 // Two arguments or three and nothing else, and a fourth is an unknown
2171 // subcommand rather than a wrong count. That reads like a mistake and is
2172 // not: a real server checks `acl|log`'s own arity first, which is a minimum
2173 // of two and lets `ACL LOG 1 2` through, and then falls off the end of a
2174 // parse that only knows two shapes.
2175 if args.len() > 3 {
2176 return Err(args::subcommand_syntax(args.get(1), "ACL"));
2177 }
2178 if args.len() == 3 && is(args.get(2), b"reset") {
2179 server.acl_log().clear();
2180 out.ok();
2181 return Ok(());
2182 }
2183 // Ten by default, and a negative count is nought rather than an error, which
2184 // is what makes `ACL LOG -1` an empty array.
2185 let wanted = if args.len() == 3 {
2186 args.int(2)?.max(0)
2187 } else {
2188 10
2189 };
2190 let now = server.now_ms();
2191 let held = server
2192 .acl_log()
2193 .entries
2194 .lock()
2195 .unwrap_or_else(|e| e.into_inner());
2196 let count = (wanted as u64).min(held.len() as u64) as usize;
2197 out.array(count);
2198 for entry in held.iter().take(count) {
2199 out.map(10);
2200 out.bulk(b"count");
2201 out.int(entry.count as i64);
2202 out.bulk(b"reason");
2203 out.bulk(entry.reason.name().as_bytes());
2204 out.bulk(b"context");
2205 out.bulk(entry.context.name().as_bytes());
2206 out.bulk(b"object");
2207 out.bulk(&entry.object);
2208 out.bulk(b"username");
2209 out.bulk(&entry.username);
2210 out.bulk(b"age-seconds");
2211 // Seconds with the milliseconds after the point, which is the one field
2212 // here that is a double, and it is the age now rather than the age when
2213 // the refusal happened.
2214 out.double(now.saturating_sub(entry.ctime) as f64 / 1000.0);
2215 out.bulk(b"client-info");
2216 out.bulk(entry.cinfo.as_bytes());
2217 out.bulk(b"entry-id");
2218 out.int(entry.entry_id as i64);
2219 out.bulk(b"timestamp-created");
2220 out.int(entry.created as i64);
2221 out.bulk(b"timestamp-last-updated");
2222 out.int(entry.ctime as i64);
2223 }
2224 Ok(())
2225}
2226
2227/// `ACL SAVE`, which writes the users out in the format the file is read in.
2228///
2229/// A temporary file beside the real one, then fsync, then rename, then fsync of
2230/// the directory. That is Redis's sequence and it is the only sequence that
2231/// leaves a reader with either the whole old file or the whole new one: a write
2232/// straight over the real file would leave a half written ACL on a server that
2233/// lost power, and a server that came back up refusing to let anybody in is a
2234/// worse outcome than one that came back up with yesterday's users.
2235fn save(server: &Server, out: &mut Out) -> Result<()> {
2236 let Some(path) = server.aclfile() else {
2237 return Ok(());
2238 };
2239 let mut text = Vec::with_capacity(256);
2240 server.users().with(|table| {
2241 for user in table.iter() {
2242 text.extend_from_slice(b"user ");
2243 text.extend_from_slice(&user.describe());
2244 text.push(b'\n');
2245 }
2246 (false, ())
2247 });
2248 if write_file(path, &text).is_err() {
2249 // The reason is in the server log on a real server and there is nowhere
2250 // to put it here, so the client gets the sentence and nothing else,
2251 // which is also what a real server gives it.
2252 return Err(Error::new(
2253 Code::Invalid,
2254 "There was an error trying to save the ACLs. Please check the server logs for more information",
2255 ));
2256 }
2257 out.ok();
2258 Ok(())
2259}
2260
2261/// The write, the rename and the two syncs.
2262fn write_file(path: &Path, text: &[u8]) -> std::io::Result<()> {
2263 use std::io::Write as _;
2264
2265 let temp = path.with_file_name(format!(
2266 "{}.tmp-{}-{}",
2267 path.file_name().unwrap_or_default().to_string_lossy(),
2268 std::process::id(),
2269 std::time::SystemTime::now()
2270 .duration_since(std::time::UNIX_EPOCH)
2271 .map_or(0, |d| d.as_millis())
2272 ));
2273 let outcome = (|| {
2274 let mut file = std::fs::File::create(&temp)?;
2275 file.write_all(text)?;
2276 file.sync_all()?;
2277 drop(file);
2278 std::fs::rename(&temp, path)
2279 })();
2280 if outcome.is_err() {
2281 let _ = std::fs::remove_file(&temp);
2282 return outcome;
2283 }
2284 // The directory, so that the rename itself is on disk and not only the
2285 // bytes the rename pointed at. Best effort, because opening a directory as a
2286 // file is a Unix thing and Windows answers an error rather than a handle,
2287 // and a failure here means the file is written and the entry naming it may
2288 // not have reached the platter yet.
2289 if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty())
2290 && let Ok(handle) = std::fs::File::open(dir)
2291 {
2292 let _ = handle.sync_all();
2293 }
2294 Ok(())
2295}
2296
2297/// `ACL LOAD`, which replaces every user with what the file says.
2298fn load(server: &Server, out: &mut Out) -> Result<()> {
2299 let Some(path) = server.aclfile() else {
2300 return Ok(());
2301 };
2302 match load_file(server, path) {
2303 Ok(()) => {
2304 out.ok();
2305 Ok(())
2306 }
2307 Err(errors) => Err(Error::fmt(Code::Invalid, format_args!("{errors}"))),
2308 }
2309}
2310
2311/// Read `path` and, if every line of it is good, make it the server's users.
2312///
2313/// The whole file or none of it. The users are built in a table of their own and
2314/// only swapped in once the last line has parsed, which is the reference's
2315/// design and is the only sane one: a file with a typo halfway down would
2316/// otherwise leave a server holding half of the new ACL and half of the old one,
2317/// and nobody could say which half.
2318///
2319/// # Errors
2320///
2321/// Every complaint the file raised, joined into one sentence, which is what the
2322/// client gets and what a server that cannot start prints.
2323pub(crate) fn load_file(server: &Server, path: &Path) -> std::result::Result<(), String> {
2324 let name = path.display().to_string();
2325 let text = match std::fs::read(path) {
2326 Ok(text) => text,
2327 Err(e) => {
2328 return Err(format!(
2329 "Error loading ACLs, opening file '{name}': {}",
2330 because(&e)
2331 ));
2332 }
2333 };
2334 let mut errors = String::new();
2335 let mut staged: Vec<User> = Vec::new();
2336 let open = server.users().open_channels();
2337 for (at, raw) in text.split(|b| *b == b'\n').enumerate() {
2338 let line = trim(raw);
2339 // Blank lines and comments, which is what lets a file be commented.
2340 if line.is_empty() || line[0] == b'#' {
2341 continue;
2342 }
2343 let linenum = at + 1;
2344 let words: Vec<&[u8]> = line.split(|b| *b == b' ').collect();
2345 if words[0] != b"user" || words.len() < 2 {
2346 errors.push_str(&format!(
2347 "{name}:{linenum} should start with user keyword followed by the username. "
2348 ));
2349 continue;
2350 }
2351 // A username with a space in it could not be read back, since this is
2352 // where the reading happens and it splits on spaces. A tab or a null is
2353 // refused for the same reason a space is: the file is the only place a
2354 // user is written down and a name that cannot be written down is a user
2355 // that cannot be got at again.
2356 let who = words[1];
2357 if who.iter().any(|b| b.is_ascii_whitespace() || *b == 0) {
2358 errors.push_str(&format!(
2359 "'{name}:{linenum}: username '{}' contains invalid characters. ",
2360 String::from_utf8_lossy(who)
2361 ));
2362 continue;
2363 }
2364 if staged.iter().any(|u| u.name == who) {
2365 errors.push_str(&format!(
2366 "WARNING: Duplicate user '{}' found on line {linenum}. ",
2367 String::from_utf8_lossy(who)
2368 ));
2369 continue;
2370 }
2371 let Ok(rules) = glue(words[2..].iter().copied()) else {
2372 errors.push_str(&format!(
2373 "{name}:{linenum}: Unmatched parenthesis in selector definition."
2374 ));
2375 continue;
2376 };
2377 let mut user = User::new(who, open);
2378 let mut said = false;
2379 for rule in &rules {
2380 let Err(why) = set_user(&mut user, trim(rule), open) else {
2381 continue;
2382 };
2383 if why == Bad::Unknown {
2384 // A name nobody has heard of is quoted back, because a command
2385 // name is not a secret and an operator staring at a file wants
2386 // to know which word was wrong. Every other complaint is about
2387 // the shape of a rule that may hold a password hash.
2388 errors.push_str(&format!(
2389 "{name}:{linenum}: Error in applying operation '{}': {}. ",
2390 String::from_utf8_lossy(rule),
2391 why.text()
2392 ));
2393 } else if !said {
2394 // Only the first of the others, because a rule that failed may
2395 // have been what the rules after it were written against, and
2396 // eight complaints about one mistake is worse than one.
2397 errors.push_str(&format!("{name}:{linenum}: {}. ", why.text()));
2398 said = true;
2399 }
2400 }
2401 staged.push(user);
2402 }
2403 if !errors.is_empty() {
2404 errors.push_str(
2405 "WARNING: ACL errors detected, no change to the previously active ACL rules was performed",
2406 );
2407 return Err(errors);
2408 }
2409 staged.sort_by(|a, b| a.name.cmp(&b.name));
2410 server.users().with(|table| {
2411 *table = staged;
2412 // A file with no default user in it still leaves the server with one,
2413 // because there has to be a user for a connection that has not
2414 // authenticated to be. The reference gets there by a different route,
2415 // making a fresh default and copying it over the old one, and lands on
2416 // the same user.
2417 if let Err(at) = table.binary_search_by(|u| u.name.as_slice().cmp(DEFAULT)) {
2418 table.insert(at, User::default_user());
2419 }
2420 (true, ())
2421 });
2422 Ok(())
2423}
2424
2425/// What went wrong with a file, in the words the C library would have used.
2426///
2427/// Rust writes `No such file or directory (os error 2)` where C's `strerror`
2428/// writes `No such file or directory`, and the sentence this ends up in is one
2429/// a client may be matching on, so the number Rust adds comes back off.
2430fn because(e: &std::io::Error) -> String {
2431 let said = e.to_string();
2432 match said.find(" (os error ") {
2433 Some(at) => said[..at].to_string(),
2434 None => said,
2435 }
2436}
2437
2438/// A line with the blanks taken off both ends.
2439///
2440/// The same four characters the reference trims, which is why a file written on
2441/// Windows loads: the carriage return at the end of every line is one of them.
2442fn trim(line: &[u8]) -> &[u8] {
2443 let blank = |b: &u8| matches!(b, b' ' | b'\t' | b'\r' | b'\n');
2444 let from = line.iter().position(|b| !blank(b)).unwrap_or(line.len());
2445 let to = line.iter().rposition(|b| !blank(b)).map_or(from, |i| i + 1);
2446 &line[from..to]
2447}
2448
2449/// What `ACL HELP` says.
2450const HELP: &[&str] = &[
2451 "ACL <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
2452 "CAT [<category>]",
2453 " List all commands that belong to <category>, or all command categories",
2454 " when no category is specified.",
2455 "DELUSER <username> [<username> ...]",
2456 " Delete a list of users.",
2457 "DRYRUN <username> <command> [<arg> ...]",
2458 " Returns whether the user can execute the given command without executing the command.",
2459 "GETUSER <username>",
2460 " Get the user's details.",
2461 "GENPASS [<bits>]",
2462 " Generate a secure 256-bit user password. The optional `bits` argument can",
2463 " be used to specify a different size.",
2464 "LIST",
2465 " Show users details in config file format.",
2466 "LOAD",
2467 " Reload users from the ACL file.",
2468 "LOG [<count> | RESET]",
2469 " Show the ACL log entries.",
2470 "SAVE",
2471 " Save the current config to the ACL file.",
2472 "SETUSER <username> <attribute> [<attribute> ...]",
2473 " Create or modify a user with the specified attributes.",
2474 "USERS",
2475 " List all the registered usernames.",
2476 "WHOAMI",
2477 " Return the current connection username.",
2478 "HELP",
2479 " Print this help.",
2480];
2481
2482#[cfg(test)]
2483mod tests {
2484 use super::*;
2485 use crate::proto::Limits;
2486 use crate::request::{Argv, Step};
2487
2488 /// A user built by applying rules in order, or the first rule that failed.
2489 fn built(rules: &[&str]) -> std::result::Result<User, Bad> {
2490 let mut user = User::new(b"u", false);
2491 for rule in rules {
2492 set_user(&mut user, rule.as_bytes(), false)?;
2493 }
2494 Ok(user)
2495 }
2496
2497 /// The user's line the way `ACL LIST` writes it.
2498 fn listed(rules: &[&str]) -> String {
2499 let user = built(rules).expect("every rule here is a good one");
2500 String::from_utf8(user.describe()).expect("the description is text")
2501 }
2502
2503 /// Whether this user could run this command, said as a word so a failing
2504 /// assertion reads like the question it was asked.
2505 fn allows(rules: &[&str], words: &[&str]) -> bool {
2506 let user = built(rules).expect("every rule here is a good one");
2507 let mut buf = format!("*{}\r\n", words.len()).into_bytes();
2508 for word in words {
2509 buf.extend_from_slice(format!("${}\r\n{word}\r\n", word.len()).as_bytes());
2510 }
2511 let mut argv = Argv::new();
2512 let Ok(Step::Command { .. }) = argv.decode(&buf, &Limits::default()) else {
2513 panic!("the test wrote a command that does not decode");
2514 };
2515 let args = Args::new(&argv, &buf);
2516 let spec = table::lookup(args.name()).expect("a command this server has");
2517 permits(&user, spec, args, 0).is_ok()
2518 }
2519
2520 #[test]
2521 fn a_key_permission_with_no_pattern_after_it_is_an_empty_pattern() {
2522 // 8.10.1 takes a bare `%R` and writes it back with the `~` it never got.
2523 assert_eq!(
2524 listed(&["%R"]),
2525 "u off sanitize-payload %R~ resetchannels -@all"
2526 );
2527 assert_eq!(
2528 listed(&["%RW"]),
2529 "u off sanitize-payload ~ resetchannels -@all"
2530 );
2531 }
2532
2533 #[test]
2534 fn a_key_permission_that_is_not_r_or_w_once_each_is_a_syntax_error() {
2535 assert_eq!(built(&["%"]).err(), Some(Bad::Syntax));
2536 assert_eq!(built(&["%~k:*"]).err(), Some(Bad::Syntax));
2537 assert_eq!(built(&["%RR~k:*"]).err(), Some(Bad::Syntax));
2538 assert_eq!(built(&["%X~k:*"]).err(), Some(Bad::Syntax));
2539 }
2540
2541 #[test]
2542 fn a_subcommand_can_be_taken_back_off_a_container_that_was_allowed() {
2543 assert_eq!(
2544 listed(&["+config", "-config|get"]),
2545 "u off sanitize-payload resetchannels -@all +config -config|get"
2546 );
2547 assert!(allows(
2548 &["+config", "-config|get"],
2549 &["config", "set", "maxmemory", "0"]
2550 ));
2551 assert!(!allows(
2552 &["+config", "-config|get"],
2553 &["config", "get", "maxmemory"]
2554 ));
2555 // And allowing the container again forgets the exception, the same way
2556 // allowing it forgets a first argument it was limited to.
2557 assert_eq!(
2558 listed(&["+config", "-config|get", "+config"]),
2559 "u off sanitize-payload resetchannels -@all +config"
2560 );
2561 assert!(allows(
2562 &["+config", "-config|get", "+config"],
2563 &["config", "get", "maxmemory"]
2564 ));
2565 }
2566
2567 #[test]
2568 fn a_first_argument_can_only_be_taken_off_a_command_that_has_subcommands() {
2569 // GET has no subcommands, so a real server looks up `get|nope`, finds
2570 // nothing, and says so. Allowing one is a different mechanism and works.
2571 assert_eq!(built(&["-get|nope"]).err(), Some(Bad::Unknown));
2572 assert_eq!(built(&["-select|0"]).err(), Some(Bad::Unknown));
2573 assert!(built(&["+get|nope"]).is_ok());
2574 }
2575
2576 #[test]
2577 fn a_category_a_subcommand_holds_reaches_that_subcommand_and_no_further() {
2578 // CONFIG is only `@slow` in the table because 8.10.1 puts `@admin` on
2579 // `config|get` and `config|set` rather than on `config`, so a rule about
2580 // `@admin` has to take CONFIG GET away and leave CONFIG HELP behind.
2581 let deny = ["~*", "+@all", "-@admin"];
2582 assert!(!allows(&deny, &["config", "get", "maxmemory"]));
2583 assert!(allows(&deny, &["config", "help"]));
2584 assert!(!allows(&deny, &["client", "kill", "id", "4"]));
2585 assert!(allows(&deny, &["client", "setname", "x"]));
2586 assert!(allows(&deny, &["acl", "whoami"]));
2587 assert!(!allows(&deny, &["acl", "setuser", "u"]));
2588 // And the other way round, from a user that starts with nothing.
2589 let grant = ["~*", "-@all", "+@admin"];
2590 assert!(allows(&grant, &["config", "get", "maxmemory"]));
2591 assert!(!allows(&grant, &["config", "help"]));
2592 assert!(!allows(&grant, &["get", "k"]));
2593 // None of which changes what the container is listed as being in, or
2594 // `COMMAND INFO config` and `ACL CAT admin` would both start lying.
2595 let config = table::lookup(b"config").expect("a command this server has");
2596 assert_eq!(config.acl, ["@slow"]);
2597 }
2598
2599 #[test]
2600 fn the_flags_a_user_reports_are_the_ones_about_the_account() {
2601 // Not `allkeys`, which an older server did report here and 8.10.1 does
2602 // not. A client that wants to know reads the keys field.
2603 let user = built(&["on", "~*", "&*", "+@all"]).expect("good rules");
2604 assert_eq!(user.flags(), ["on", "sanitize-payload"]);
2605 }
2606
2607 #[test]
2608 fn a_line_of_the_file_loses_the_blanks_on_both_ends() {
2609 assert_eq!(trim(b" user alice "), b"user alice");
2610 assert_eq!(trim(b"user alice\r"), b"user alice");
2611 assert_eq!(trim(b"\t\r\n "), b"");
2612 assert_eq!(trim(b""), b"");
2613 // Only the ends, because a rule is separated from the next one by a
2614 // single space and taking the inner ones out would join two rules.
2615 assert_eq!(trim(b" a b "), b"a b");
2616 }
2617
2618 #[test]
2619 fn the_reason_a_file_would_not_open_reads_the_way_c_writes_it() {
2620 let missing = std::io::Error::from_raw_os_error(2);
2621 // What is claimed here is that the number Rust puts on the end comes
2622 // off, and that is true everywhere. The sentence in front of it is the
2623 // system's own and is not: errno 2 is `No such file or directory` out
2624 // of a C library and `The system cannot find the file specified.` out
2625 // of Windows, and a server built on Windows should say what Windows
2626 // says rather than repeat a sentence from another operating system.
2627 let said = because(&missing);
2628 assert!(!said.contains("(os error"), "{said}");
2629 assert!(missing.to_string().starts_with(&said), "{said}");
2630 #[cfg(unix)]
2631 assert_eq!(said, "No such file or directory");
2632 // Anything with no errno behind it is left alone, since there is no
2633 // number on the end of it to take off.
2634 let made_up = std::io::Error::other("something else");
2635 assert_eq!(because(&made_up), "something else");
2636 }
2637
2638 #[test]
2639 fn a_new_selector_starts_where_acl_pubsub_default_says() {
2640 let mut open = User::new(b"u", true);
2641 set_user(&mut open, b"on", true).expect("a good rule");
2642 assert!(String::from_utf8_lossy(&open.describe()).contains("&*"));
2643 // And a reset goes back to the same place rather than to the built in
2644 // one, which is what makes the setting worth having.
2645 set_user(&mut open, b"reset", true).expect("a good rule");
2646 assert!(String::from_utf8_lossy(&open.describe()).contains("&*"));
2647 let shut = User::new(b"u", false);
2648 assert!(String::from_utf8_lossy(&shut.describe()).contains("resetchannels"));
2649 }
2650}