cli_engine/auth/mod.rs
1//! Auth provider abstraction and built-in auth helpers.
2//!
3//! Consumer CLIs normally register one or more [`AuthProvider`] implementations
4//! with [`crate::CliConfig`]. Middleware then resolves credentials before
5//! business logic runs and passes a [`Credential`] to command handlers.
6//!
7//! The module also contains an [`crate::auth::exec::ExecProvider`] for provider
8//! binaries that speak the JSON stdin/stdout contract.
9//!
10//! When the `pkce-auth` feature is enabled, `pkce::PkceAuthProvider` adds a
11//! built-in browser-based OAuth 2.0 PKCE flow with system keychain storage.
12
13/// Built-in `auth login`, `auth status`, and `auth logout` command helpers.
14pub mod commands;
15mod credential;
16mod dispatcher;
17/// External process auth provider implementation.
18pub mod exec;
19/// OAuth 2.0 PKCE auth provider (requires the `pkce-auth` feature).
20#[cfg(feature = "pkce-auth")]
21pub mod pkce;
22/// Pluggable credential storage backends (keychain, file, auto).
23pub mod storage;
24
25use async_trait::async_trait;
26
27pub use commands::{
28 AuthLoginResult, AuthStatusEntry, auth_command_group, login_and_build,
29 login_and_build_with_scopes, logout_result, status_result, to_status_entry,
30};
31pub use credential::{CACHE_TTL, Credential};
32pub use dispatcher::{Dispatcher, SingleProvider, StatusEntry};
33pub use exec::{
34 ACTION_AUTHENTICATE, ACTION_LIST_ENVIRONMENTS, ACTION_LIST_REALMS, ACTION_LOGOUT,
35 ACTION_STATUS, AuthnRequest, EnvironmentsResponse, ExecProvider,
36};
37#[cfg(feature = "pkce-auth")]
38pub use storage::{AutoStorage, KeyringStorage};
39pub use storage::{CredentialKey, CredentialStorage, FileStorage, default_storage, storage_for};
40
41use crate::Result;
42use crate::middleware::CommandMeta;
43
44/// Everything an [`AuthProvider`] may inspect about the command requesting a
45/// credential.
46///
47/// This bundles the routing fields passed to [`AuthProvider::get_credential`]
48/// (`env`, colon command path, and tier) together with the command's
49/// [`CommandMeta`], so a provider can read richer metadata — for example an
50/// OAuth provider reading [`CommandMeta::scopes`] to decide whether the cached
51/// token is sufficient. Providers that do not need metadata can ignore it.
52///
53/// Marked `#[non_exhaustive]` because the framework constructs it (providers only
54/// read it) and more request fields may be added over time; build one with
55/// [`CredentialRequest::new`] rather than a struct literal so adding a field is
56/// not a breaking change for downstream crates.
57#[derive(Clone, Copy, Debug)]
58#[non_exhaustive]
59pub struct CredentialRequest<'req> {
60 /// Target environment name.
61 pub env: &'req str,
62 /// Colon-separated command path, for example `project:list`.
63 pub command: &'req str,
64 /// Risk tier as a string, for example `read` or `mutate`.
65 pub tier: &'req str,
66 /// Metadata for the command requesting the credential.
67 pub meta: &'req CommandMeta,
68}
69
70impl<'req> CredentialRequest<'req> {
71 /// Creates a request from the routing fields and command metadata.
72 #[must_use]
73 pub fn new(
74 env: &'req str,
75 command: &'req str,
76 tier: &'req str,
77 meta: &'req CommandMeta,
78 ) -> Self {
79 Self {
80 env,
81 command,
82 tier,
83 meta,
84 }
85 }
86}
87
88#[async_trait]
89/// Named auth provider used by middleware and transport injectors.
90///
91/// Implementations own their credential cache strategy. The framework only
92/// routes calls and passes command context (`env`, colon command path, and tier).
93pub trait AuthProvider: Send + Sync + std::fmt::Debug {
94 /// Stable provider registration name, for example `primary` or `oauth`.
95 fn name(&self) -> &str;
96
97 /// Returns a credential for `env`, `command`, and `tier`.
98 async fn get_credential(&self, env: &str, command: &str, tier: &str) -> Result<Credential>;
99
100 /// Returns a credential for a command, given its full [`CredentialRequest`].
101 ///
102 /// The default implementation ignores the metadata and delegates to
103 /// [`get_credential`](AuthProvider::get_credential). Providers that act on
104 /// command metadata — such as an OAuth provider performing scope step-up
105 /// from [`CommandMeta::scopes`] — override this. The framework calls this
106 /// method (not `get_credential`) when resolving credentials, so an override
107 /// receives the command's metadata.
108 async fn get_credential_for(&self, req: &CredentialRequest<'_>) -> Result<Credential> {
109 self.get_credential(req.env, req.command, req.tier).await
110 }
111
112 /// Returns cached credential status for one environment.
113 async fn status(&self, env: &str) -> Result<Credential>;
114
115 /// Clears cached credentials for one environment.
116 async fn logout(&self, env: &str) -> Result<()>;
117
118 /// Lists environments with cached credentials.
119 async fn list_environments(&self) -> Result<Vec<String>>;
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn credential_request_new_sets_all_fields() {
128 let meta = CommandMeta::default();
129 let req = CredentialRequest::new("dev", "app:list", "read", &meta);
130 assert_eq!(req.env, "dev");
131 assert_eq!(req.command, "app:list");
132 assert_eq!(req.tier, "read");
133 // `Copy` is preserved (using `req` after copying it must compile).
134 let copy = req;
135 assert_eq!(copy.env, req.env);
136 }
137}