manta-server 2.0.0-beta.62

Manta HTTP server — single API that proxies to CSM / Ochami backends.
//! Authentication service — proxies CLI credential exchange to the
//! configured CSM/OCHAMI backend.
//!
//! The CLI never talks to Keycloak directly; it POSTs username+password
//! to `manta-server /api/v1/auth/token`, which calls
//! `backend.get_api_token` on the user's behalf and returns the CSM
//! bearer token. `validate_api_token` exposes a lightweight
//! "is-this-token-still-valid" probe the CLI can call before sending
//! a long-running request that would otherwise fail mid-flight.

use std::time::Instant;

use manta_backend_dispatcher::error::Error;
use manta_backend_dispatcher::interfaces::authentication::AuthenticationTrait;

use crate::server::common::app_context::InfraContext;

/// Exchange `username` + `password` for a CSM bearer token via the
/// site's configured backend.
///
/// The CLI's `auth` command posts to `/api/v1/auth/token`, which
/// reaches this function. The returned token is the same bearer the
/// caller then sends as `Authorization: Bearer ...` on every
/// subsequent request.
///
/// # Errors
///
/// Whatever the backend's
/// [`AuthenticationTrait::get_api_token`] returns — typically a
/// `BackendError::Unauthorized` for bad credentials, or a
/// `NetError` when the backend's IDP is unreachable.
#[tracing::instrument(
  skip_all,
  fields(
    site = %infra.site_name,
    backend = %infra.backend_kind(),
    backend_url = %infra.shasta_base_url,
  )
)]
pub async fn get_api_token(
  infra: &InfraContext<'_>,
  username: &str,
  password: &str,
) -> Result<String, Error> {
  tracing::info!(user = %username, "backend: requesting token");
  let started = Instant::now();
  infra
    .backend
    .get_api_token(username, password)
    .await
    .inspect(|_| {
      tracing::debug!(
        user = %username,
        elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
        "backend: token issued"
      );
    })
    .inspect_err(|e| {
      tracing::warn!(
        user = %username,
        elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
        error = %e,
        "backend: token request rejected"
      );
    })
}

/// Verify that `token` is still accepted by the site's backend.
///
/// Lightweight probe the CLI calls before kicking off a long-running
/// operation that would otherwise fail mid-flight (e.g. a multi-node
/// power transition followed by a poll loop). Does not return any
/// claim from the token — for that, decode it locally with
/// `jwt_ops` instead.
///
/// # Errors
///
/// Whatever
/// [`AuthenticationTrait::validate_api_token`] returns —
/// typically `Unauthorized` for an expired or revoked token.
#[tracing::instrument(
  skip_all,
  fields(
    site = %infra.site_name,
    backend = %infra.backend_kind(),
    backend_url = %infra.shasta_base_url,
  )
)]
pub async fn validate_api_token(
  infra: &InfraContext<'_>,
  token: &str,
) -> Result<(), Error> {
  tracing::info!("backend: validating token");
  let started = Instant::now();
  infra
    .backend
    .validate_api_token(token)
    .await
    .inspect(|()| {
      tracing::debug!(
        elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
        "backend: token accepted"
      );
    })
    .inspect_err(|e| {
      tracing::warn!(
        elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
        error = %e,
        "backend: token validation rejected"
      );
    })
}