manta_server/service/
auth.rs

1//! Authentication service — proxies CLI credential exchange to the
2//! configured CSM/OCHAMI backend.
3//!
4//! The CLI never talks to Keycloak directly; it POSTs username+password
5//! to `manta-server /api/v1/auth/token`, which calls
6//! `backend.get_api_token` on the user's behalf and returns the CSM
7//! bearer token. `validate_api_token` exposes a lightweight
8//! "is-this-token-still-valid" probe the CLI can call before sending
9//! a long-running request that would otherwise fail mid-flight.
10
11use std::time::Instant;
12
13use manta_backend_dispatcher::error::Error;
14use manta_backend_dispatcher::interfaces::authentication::AuthenticationTrait;
15
16use crate::server::common::app_context::InfraContext;
17
18/// Exchange `username` + `password` for a CSM bearer token via the
19/// site's configured backend.
20///
21/// The CLI's `auth` command posts to `/api/v1/auth/token`, which
22/// reaches this function. The returned token is the same bearer the
23/// caller then sends as `Authorization: Bearer ...` on every
24/// subsequent request.
25///
26/// # Errors
27///
28/// Whatever the backend's
29/// [`AuthenticationTrait::get_api_token`] returns — typically a
30/// `BackendError::Unauthorized` for bad credentials, or a
31/// `NetError` when the backend's IDP is unreachable.
32#[tracing::instrument(
33  skip_all,
34  fields(
35    site = %infra.site_name,
36    backend = %infra.backend_kind(),
37    backend_url = %infra.shasta_base_url,
38  )
39)]
40pub async fn get_api_token(
41  infra: &InfraContext<'_>,
42  username: &str,
43  password: &str,
44) -> Result<String, Error> {
45  tracing::info!(user = %username, "backend: requesting token");
46  let started = Instant::now();
47  infra
48    .backend
49    .get_api_token(username, password)
50    .await
51    .inspect(|_| {
52      tracing::debug!(
53        user = %username,
54        elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
55        "backend: token issued"
56      );
57    })
58    .inspect_err(|e| {
59      tracing::warn!(
60        user = %username,
61        elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
62        error = %e,
63        "backend: token request rejected"
64      );
65    })
66}
67
68/// Verify that `token` is still accepted by the site's backend.
69///
70/// Lightweight probe the CLI calls before kicking off a long-running
71/// operation that would otherwise fail mid-flight (e.g. a multi-node
72/// power transition followed by a poll loop). Does not return any
73/// claim from the token — for that, decode it locally with
74/// `jwt_ops` instead.
75///
76/// # Errors
77///
78/// Whatever
79/// [`AuthenticationTrait::validate_api_token`] returns —
80/// typically `Unauthorized` for an expired or revoked token.
81#[tracing::instrument(
82  skip_all,
83  fields(
84    site = %infra.site_name,
85    backend = %infra.backend_kind(),
86    backend_url = %infra.shasta_base_url,
87  )
88)]
89pub async fn validate_api_token(
90  infra: &InfraContext<'_>,
91  token: &str,
92) -> Result<(), Error> {
93  tracing::info!("backend: validating token");
94  let started = Instant::now();
95  infra
96    .backend
97    .validate_api_token(token)
98    .await
99    .inspect(|()| {
100      tracing::debug!(
101        elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
102        "backend: token accepted"
103      );
104    })
105    .inspect_err(|e| {
106      tracing::warn!(
107        elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
108        error = %e,
109        "backend: token validation rejected"
110      );
111    })
112}