Skip to main content

cli_forge/
auth.rs

1//! The authentication seam.
2//!
3//! cli-forge holds the *seam*, not the logic. A command marked
4//! [`requires_auth`](crate::Command::requires_auth) runs only if the app's auth
5//! hook authorizes it; the hook — supplied by the consumer or a sibling
6//! `cli-auth` crate — is where login/logout state actually lives. The core just
7//! asks. This module is compiled only with the `auth` feature; without it,
8//! `requires_auth` is inert.
9//!
10//! The hook is `Fn(&`[`AuthRequest`]`) -> bool`. It is consulted before an
11//! auth-gated command's handler runs (returning `false` refuses the command with
12//! [`ParseError::Unauthorized`](crate::ParseError::Unauthorized)) and again when
13//! generating help (an unauthorized command is omitted from the listing). If no
14//! hook is set, auth-gated commands are never authorized — the seam fails closed.
15//!
16//! Because it also runs during help generation, the hook should be pure and
17//! cheap: check already-loaded session state rather than doing I/O or printing.
18
19/// The boxed authorization hook stored on an [`App`](crate::App).
20pub(crate) type AuthHook = Box<dyn Fn(&AuthRequest<'_>) -> bool>;
21
22/// The context passed to the auth hook: which command is being authorized.
23///
24/// Marked `#[non_exhaustive]` so future context (roles, the parsed arguments,
25/// …) can be added without a breaking change.
26///
27/// # Examples
28///
29/// ```
30/// # #[cfg(feature = "auth")]
31/// # {
32/// use cli_forge::{App, Command};
33///
34/// let mut app = App::new("demo").auth(|req| {
35///     // Only authorize `publish` when some session check passes.
36///     req.command() != "publish" || session_is_valid()
37/// });
38/// app.register(Command::new("publish").requires_auth(true).run(|_| { /* ... */ }));
39///
40/// fn session_is_valid() -> bool { true }
41/// # let _ = app.try_parse_from(["publish"]);
42/// # }
43/// ```
44#[non_exhaustive]
45pub struct AuthRequest<'a> {
46    path: &'a [&'a str],
47}
48
49impl<'a> AuthRequest<'a> {
50    /// Build a request for the command reached by `path` (the command-name chain
51    /// from the app root to the command being authorized).
52    pub(crate) fn new(path: &'a [&'a str]) -> AuthRequest<'a> {
53        AuthRequest { path }
54    }
55
56    /// The name of the command being authorized (the last element of the path).
57    #[must_use]
58    pub fn command(&self) -> &str {
59        self.path.last().copied().unwrap_or("")
60    }
61
62    /// The full command-name chain from the app root to this command, e.g.
63    /// `["remote", "add"]`.
64    #[must_use]
65    pub fn path(&self) -> &[&str] {
66        self.path
67    }
68}