grok-build-upstream-mirror/crates/codegen/xai-grok-shell/src/auth/credential_provider.rs
grokkybara[bot] 98c3b2438a Synced from monorepo
Changes:
- Classify clipboard delivery confidence
- Add durable session update append
- Scope the xAI session bearer to first-party memory embedding endpoints
- Persist subagent outputs to disk and bound long-lived agent state
- Add MiniSweAgent:bash for mini-swe-agent parity
- Revert taking local sessions off the persistent shell
- Contextual tip recommending grok wrap on SSH sessions
- Voice STT bearer from model BYOK env_key/api_key
- Define exact website policies for sandbox
- Gate unsafe shell environments
- Shared pin hoist; single require_sha gate for marketplace plugins
- Server-signed is-managed claim (closes sidecar-removal downgrade)
- Optional require_sha pin for remote plugin installs
- Show session title and last exchange in the exit resume hint
- Gate shell output redirects
- Warn when fail_closed is present but not a boolean
- Add canonical text editing core (ratatui-textarea)
- Keep execution state out of goal scratch
- Add acknowledged persistence primitives
- Inherit child network restrictions in sandbox
- Fail closed when hook matchers fail to recompile
- Add MCP setup preferences for plugin MCPs
- Gate sourced shell scripts
- Gate file-typed project hooks
- grok wrap: restore terminal modes on child death
- Harden owner-only permissions on auth and MCP credentials
- Create crash dump files with owner-only permissions
- Write the agent_id cache owner-only (0600)
- SessionMetrics mode skips Mixpanel profile sync
- Dashboard: slim live-tail peek
- Yank full queued prompt text, not (+N lines)
- Defeat clock-rollback on the signed managed-config cache
- Stop early session/cancel from overtaking the prompt and wedging the turn slot
- Self-heal a diverged agent entrypoint on startup
- Add matched inference expectations in test-support
- Add AuthSingleFlight cancel/successor gap tests
- Remove consumer from external OTEL allowlist and pin scrub coverage
- Enable /copy in minimal mode
- Surface capacity and API-key detail on 429 errors
- Single-flight interactive auth
- Fix PageUp/PageDown skipping lines behind sticky prompt header
2026-07-17 14:19:50 +01:00

884 lines
37 KiB
Rust

use crate::auth::AuthManager;
use crate::util::grok_auth_credentials::GrokAuthCredentials;
use reqwest::RequestBuilder;
use std::sync::Arc;
use xai_grok_auth::{
AuthCredentialProvider, CredentialSnapshot, HttpAuth, StaticAuthCredentialProvider,
};
/// `api_key.id` for the active credential: hash the stable API key, never the
/// OIDC bearer (which rotates). `None` for non-API-key auth.
fn api_key_id_for(auth: Option<&crate::auth::GrokAuth>) -> Option<String> {
auth.filter(|a| matches!(a.auth_mode, crate::auth::AuthMode::ApiKey))
.map(|a| xai_grok_telemetry::config::deployment_id_from_key(&a.key))
}
/// Production impl: wraps the live `AuthManager`. 401 recovery
/// delegates to `AuthManager::unauthorized_recovery`.
pub struct ShellAuthCredentialProvider {
auth_manager: Arc<AuthManager>,
static_credentials: GrokAuthCredentials,
}
impl ShellAuthCredentialProvider {
pub(crate) fn new(
auth_manager: Arc<AuthManager>,
deployment_key: Option<String>,
alpha_test_key: Option<String>,
) -> Self {
let mut static_credentials = GrokAuthCredentials::new(None);
static_credentials.deployment_key = deployment_key;
static_credentials.alpha_test_key = alpha_test_key;
Self {
auth_manager,
static_credentials,
}
}
}
impl std::fmt::Debug for ShellAuthCredentialProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShellAuthCredentialProvider")
.field("auth_manager", &"<configured>")
.finish()
}
}
impl HttpAuth for ShellAuthCredentialProvider {
fn apply(&self, builder: RequestBuilder, base_url: &str) -> RequestBuilder {
let mut creds = self.static_credentials.clone();
if creds.deployment_key.is_none()
&& let Some(auth) = self.auth_manager.current_or_expired()
{
creds.user_token = Some(auth.key);
}
creds.apply(builder, base_url)
}
}
#[async_trait::async_trait]
impl AuthCredentialProvider for ShellAuthCredentialProvider {
fn snapshot(&self) -> CredentialSnapshot {
if let Some(ref dk) = self.static_credentials.deployment_key {
return CredentialSnapshot {
token: Some(dk.clone()),
deployment_id: crate::managed_config::resolve_deployment_id(Some(dk)),
..Default::default()
};
}
let auth = self.auth_manager.current_or_expired();
let user_id = auth.as_ref().map(|a| a.user_id.clone());
let team_id = auth.as_ref().and_then(|a| a.team_id.clone());
let organization_id = auth.as_ref().and_then(|a| a.organization_id.clone());
let api_key_id = api_key_id_for(auth.as_ref());
let token = auth.map(|a| a.key);
CredentialSnapshot {
token,
user_id,
team_id,
deployment_id: None,
api_key_id,
organization_id,
}
}
async fn refresh_after_unauthorized(&self) -> bool {
if self.static_credentials.deployment_key.is_some() {
return false;
}
self.auth_manager
.try_recover_unauthorized(crate::auth::recovery::RecoverySource::Background)
.await
}
fn needs_token_auth_header(&self) -> bool {
self.static_credentials.deployment_key.is_none()
}
}
/// Resolves the embedding credentials for `embed_base_url`, attaching the xAI
/// session credential only to xAI-operated endpoints over `https`.
pub(crate) fn embedding_session_credentials(
embed_base_url: &str,
auth_manager: Option<&Arc<AuthManager>>,
api_key_provider: Option<xai_grok_tools::types::SharedApiKeyProvider>,
) -> xai_grok_memory::EndpointScopedCredentials {
let auth_credentials = auth_manager.map(|am| {
Arc::new(ShellAuthCredentialProvider::new(am.clone(), None, None))
as Arc<dyn AuthCredentialProvider>
});
xai_grok_memory::EndpointScopedCredentials::for_endpoint(
embed_base_url,
crate::util::is_xai_api_bearer_url,
auth_credentials,
api_key_provider,
)
}
/// Build a `StorageClient` for proxy uploads (including the high-volume
/// `batch_upload` used for repo context / `repo_changes_dedup`).
///
/// ### Client Identity (Important)
///
/// Every call site **must** pass the correct `client_identifier` so that
/// the API proxy (and telemetry / metrics backends) can properly attribute requests:
///
/// - `"grok-shell"` — classic Grok CLI / TUI (xai-grok-shell)
/// - `"grok-pager"` — new Grok Pager / TUI (xai-grok-pager)
/// - `"grok-desktop"` — Grok Desktop app
/// - `"grok-extension"` — VS Code / browser extension
///
/// The factory forwards both:
/// - `x-grok-client-version` (e.g. "0.1.210-alpha.5 (279ffacddb)")
/// - `x-grok-client-identifier`
///
/// to the underlying `xai-file-utils::StorageClient` via
/// `.with_client_identity(...)`. This is what powers the improved error
/// logging on the request-attribution path.
///
/// - When `auth_manager` is `Some`, uses the live `ShellAuthCredentialProvider`
/// (OIDC refresh, proactive refresh, 401 recovery via `unauthorized_recovery`).
/// - When `auth_manager` is `None`, falls back to a static token:
/// - `user_token` (normal OIDC users via com scope) → sends Bearer + `X-XAI-Token-Auth`
/// - `deployment_key` (enterprise) → sends bare Bearer
/// - the optional extra access key is added only for matching hosts when
/// the non-production feature is enabled.
///
/// `user_token` is only for AuthManager-less one-shots; live paths (incl. pager
/// restore) pass the AuthManager plus an optional deployment key.
pub fn build_storage_client_for_proxy(
proxy_base_url: &str,
deployment_key: Option<String>,
alpha_test_key: Option<String>,
auth_manager: Option<Arc<AuthManager>>,
user_token: Option<String>,
session_id: Option<String>,
client_identifier: &str,
) -> xai_file_utils::storage_client::StorageClient {
let http_client = crate::http::shared_upload_client();
if let Some(am) = auth_manager {
let provider: Arc<dyn AuthCredentialProvider> = Arc::new(ShellAuthCredentialProvider::new(
am.clone(),
deployment_key,
alpha_test_key,
));
let bridge: Arc<dyn xai_file_utils::storage_client::Auth401AttributionCallback> =
Arc::new(StorageClientAttributionBridge::new(am, session_id));
xai_file_utils::storage_client::StorageClient::with_provider(
proxy_base_url,
http_client,
provider,
)
.with_client_identity(xai_grok_version::VERSION, client_identifier)
.with_client_mode(crate::http::process_client_mode())
.with_attribution(bridge)
} else {
let mut creds = GrokAuthCredentials::new(user_token);
creds.deployment_key = deployment_key;
creds.alpha_test_key = alpha_test_key;
let wire_bearer = creds
.deployment_key
.clone()
.or_else(|| creds.user_token.clone());
let provider: Arc<dyn AuthCredentialProvider> = Arc::new(
StaticAuthCredentialProvider::new(Box::new(creds), wire_bearer),
);
xai_file_utils::storage_client::StorageClient::with_provider(
proxy_base_url,
http_client,
provider,
)
.with_client_identity(xai_grok_version::VERSION, client_identifier)
.with_client_mode(crate::http::process_client_mode())
}
}
/// Bridge that lets `StorageClient` (which lives in xai-file-utils)
/// emit shell's 401-attribution event without the data-collector crate
/// having a direct dependency on shell. Holds a reference to the live
/// `AuthManager` so attribution events carry the correct user_id.
pub(crate) struct StorageClientAttributionBridge {
auth_manager: Arc<AuthManager>,
session_id: Option<String>,
}
impl std::fmt::Debug for StorageClientAttributionBridge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StorageClientAttributionBridge")
.finish_non_exhaustive()
}
}
impl StorageClientAttributionBridge {
pub(crate) fn new(auth_manager: Arc<AuthManager>, session_id: Option<String>) -> Self {
Self {
auth_manager,
session_id,
}
}
}
impl xai_file_utils::storage_client::Auth401AttributionCallback for StorageClientAttributionBridge {
fn record_401(&self, operation: &str, sent_bearer_prefix: Option<&str>) {
crate::auth::attribution::record_consumer_401(
self.auth_manager.as_ref(),
self.session_id.as_deref(),
crate::auth::attribution::ConsumerKind::StorageClient,
operation,
sent_bearer_prefix,
);
}
}
/// Credential provider for the OTel layer's `RefreshableSpanExporter`.
///
/// Starts with a bootstrap `AuthManager` (disk-read-only, no refresher)
/// and can be upgraded to the agent's live `Arc<AuthManager>` once the
/// agent is initialized. After upgrade:
///
/// - `snapshot()` reads from the live manager's in-memory cache (kept
/// hot by the proactive refresh task) instead of re-reading disk.
/// - `refresh_after_unauthorized()` routes through
/// `unauthorized_recovery` for active OIDC/external-binary refresh.
///
/// Before upgrade, the bootstrap manager provides disk-read-only
/// behavior (equivalent to the pre-consolidation OTel path).
pub struct OtelAuthCredentialProvider {
/// Bootstrap manager used before the live one is available.
bootstrap: Arc<AuthManager>,
/// Swapped to the agent's live `AuthManager` via `set_live()`.
/// `None` means still in bootstrap mode.
live: arc_swap::ArcSwap<Option<Arc<AuthManager>>>,
/// Enterprise deployment key. Takes precedence over OIDC in `snapshot_inner`.
deployment_key: arc_swap::ArcSwap<Option<String>>,
}
impl OtelAuthCredentialProvider {
fn new(bootstrap: Arc<AuthManager>) -> Self {
Self {
bootstrap,
live: arc_swap::ArcSwap::from_pointee(None),
deployment_key: arc_swap::ArcSwap::from_pointee(None),
}
}
/// Upgrade to the agent's live `AuthManager`. After this call,
/// `snapshot()` reads from the live manager (proactive refresh
/// keeps it hot) and `refresh_after_unauthorized()` drives the
/// full recovery state machine.
pub fn set_live(&self, auth_manager: Arc<AuthManager>) {
self.live.store(Arc::new(Some(auth_manager)));
}
pub fn set_deployment_key(&self, key: String) {
self.deployment_key.store(Arc::new(Some(key)));
}
/// Single-load snapshot of the live/bootstrap state.
fn load_state(&self) -> (Arc<AuthManager>, bool) {
let guard = self.live.load();
match guard.as_ref() {
Some(am) => (am.clone(), true),
None => (self.bootstrap.clone(), false),
}
}
}
impl std::fmt::Debug for OtelAuthCredentialProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (_, is_live) = self.load_state();
f.debug_struct("OtelAuthCredentialProvider")
.field("mode", &if is_live { "live" } else { "bootstrap" })
.finish()
}
}
impl HttpAuth for OtelAuthCredentialProvider {
fn apply(&self, builder: RequestBuilder, base_url: &str) -> RequestBuilder {
let snapshot = self.snapshot_inner();
let mut creds = GrokAuthCredentials::new(None);
if self.deployment_key.load().is_some() {
creds.deployment_key = snapshot.token;
} else {
creds.user_token = snapshot.token;
}
creds.apply(builder, base_url)
}
}
impl OtelAuthCredentialProvider {
fn snapshot_inner(&self) -> CredentialSnapshot {
if let Some(ref dk) = **self.deployment_key.load() {
return CredentialSnapshot {
token: Some(dk.clone()),
deployment_id: crate::managed_config::resolve_deployment_id(Some(dk)),
..Default::default()
};
}
let (am, is_live) = self.load_state();
if !is_live {
am.force_reload_from_disk();
}
let auth = am.current_or_expired();
let user_id = auth.as_ref().map(|a| a.user_id.clone());
let team_id = auth.as_ref().and_then(|a| a.team_id.clone());
let organization_id = auth.as_ref().and_then(|a| a.organization_id.clone());
let api_key_id = api_key_id_for(auth.as_ref());
let token = auth.map(|a| a.key);
CredentialSnapshot {
token,
user_id,
team_id,
deployment_id: None,
api_key_id,
organization_id,
}
}
}
#[async_trait::async_trait]
impl AuthCredentialProvider for OtelAuthCredentialProvider {
fn snapshot(&self) -> CredentialSnapshot {
self.snapshot_inner()
}
fn has_usable_credential(&self) -> bool {
if self.deployment_key.load().is_some() {
return true;
}
self.load_state().0.has_usable_token()
}
async fn refresh_after_unauthorized(&self) -> bool {
let (am, is_live) = self.load_state();
if !is_live {
return false;
}
am.try_recover_unauthorized(crate::auth::recovery::RecoverySource::Background)
.await
}
fn needs_token_auth_header(&self) -> bool {
self.deployment_key.load().is_none()
}
}
/// Process-wide OTel credential provider handle.
///
/// This is one of two acceptable process-wide statics in this crate
/// (the other is `TRACER_PROVIDER` in `otel_layer.rs`). It's set
/// once at tracing init (before any `AuthManager` exists) and holds
/// a bootstrap-mode provider. The `ArcSwap` *inside* the provider
/// handles the runtime auth state swap — the `OnceLock` itself is
/// never re-written.
///
/// Why not thread it? `build_default_otel_layer_config` is called
/// from 15+ `init_tracing*` sites across 3 binaries. Threading a
/// handle through all of them to the agent init site (where the
/// live `AuthManager` is constructed) would touch ~20 files for a
/// single-purpose plumbing change. The `OnceLock` holds a
/// container, not auth state; the auth state lives behind `ArcSwap`
/// and changes at runtime.
static OTEL_PROVIDER: std::sync::OnceLock<Arc<OtelAuthCredentialProvider>> =
std::sync::OnceLock::new();
/// Upgrade the OTel credential provider to use the agent's live
/// `AuthManager`. Call this once after the main `AuthManager` is
/// constructed and has its refresher configured. No-ops if the OTel
/// layer was never initialized (e.g. `InstrumentationMode::Disabled`).
pub fn wire_otel_auth_manager(auth_manager: Arc<AuthManager>) {
if let Some(provider) = OTEL_PROVIDER.get() {
provider.set_live(auth_manager);
tracing::debug!("otel: upgraded credential provider to live AuthManager");
}
sync_external_otel_identity();
}
/// Push the current identity *attributes* (never the token) to the external
/// OTEL stream. Reads the same `CredentialSnapshot` the internal layer
/// stamps per-export, so both pipelines attribute identically. No-op when
/// the OTel provider was never initialized or the external stream is
/// dormant.
pub fn sync_external_otel_identity() {
if let Some(provider) = OTEL_PROVIDER.get() {
let snapshot = provider.snapshot();
xai_grok_telemetry::external::set_identity(
xai_grok_telemetry::external::IdentityAttrs::from_snapshot(&snapshot),
);
}
}
/// No-ops if the OTel layer was never initialized.
pub fn wire_otel_deployment_key(key: String) {
if let Some(provider) = OTEL_PROVIDER.get() {
provider.set_deployment_key(key);
tracing::debug!("otel: set deployment key on credential provider");
sync_external_otel_identity();
}
}
/// Bootstrap helper: build the full [`OtelLayerConfig`] that both
/// `xai-grok-pager` and `xai-grok-tui` need at tracing init time.
///
/// The credential provider starts in bootstrap mode (disk-read-only).
/// Call [`wire_otel_auth_manager`] after agent init to upgrade to the
/// live `AuthManager` with active refresh.
pub fn build_default_otel_layer_config() -> xai_grok_telemetry::otel_layer::OtelLayerConfig {
let endpoints = crate::agent::config::EndpointsConfig::default();
let grok_com_config = crate::auth::GrokComConfig::default();
let exporter = xai_grok_telemetry::otel_layer::OtelExporterConfig {
traces_url: endpoints.resolve_otlp_traces_endpoint(),
extra_headers: endpoints.resolve_otlp_headers(),
export_interval: endpoints.resolve_otlp_export_interval(),
timeout: endpoints.resolve_otlp_timeout(),
enabled: endpoints.resolve_traces_export_enabled()
&& !crate::agent::config::is_telemetry_explicitly_disabled_sync(),
};
let token_header_value = grok_com_config.token_header.clone();
let grok_home = crate::util::grok_home::grok_home();
let bootstrap = Arc::new(AuthManager::new(&grok_home, grok_com_config));
let provider = Arc::new(OtelAuthCredentialProvider::new(bootstrap));
let _ = OTEL_PROVIDER.set(provider.clone());
xai_grok_telemetry::otel_layer::OtelLayerConfig {
credentials: provider as Arc<dyn AuthCredentialProvider>,
token_header_value,
alpha_test_key: None,
exporter,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::GrokAuth;
use crate::auth::GrokComConfig;
use crate::auth::manager::AuthManager;
use chrono::{Duration as ChronoDuration, Utc};
use std::sync::Mutex;
use xai_grok_auth::AuthCredentialProvider;
/// Serializes tests that pin `GROK_AUTH_EARLY_INVALIDATION_SECS`, since
/// env vars are process-global and parallel tests would race.
static EARLY_INVALIDATION_LOCK: Mutex<()> = Mutex::new(());
/// RAII guard: pins `GROK_AUTH_EARLY_INVALIDATION_SECS` to the production
/// default (300s) while held, restoring the previous value on drop.
/// Acquires `EARLY_INVALIDATION_LOCK` so concurrent test runners can't
/// observe a half-mutated env.
struct EarlyInvalidationGuard {
_lock: std::sync::MutexGuard<'static, ()>,
previous: Option<String>,
}
impl EarlyInvalidationGuard {
fn pin_to_default() -> Self {
let lock = EARLY_INVALIDATION_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let previous = std::env::var("GROK_AUTH_EARLY_INVALIDATION_SECS").ok();
unsafe { std::env::set_var("GROK_AUTH_EARLY_INVALIDATION_SECS", "300") };
Self {
_lock: lock,
previous,
}
}
}
impl Drop for EarlyInvalidationGuard {
fn drop(&mut self) {
unsafe {
match self.previous.take() {
Some(prev) => std::env::set_var("GROK_AUTH_EARLY_INVALIDATION_SECS", prev),
None => std::env::remove_var("GROK_AUTH_EARLY_INVALIDATION_SECS"),
}
}
}
}
fn make_auth(key: &str, expires_in: ChronoDuration) -> GrokAuth {
GrokAuth {
key: key.to_string(),
user_id: "test-user".to_string(),
create_time: Utc::now(),
expires_at: Some(Utc::now() + expires_in),
..GrokAuth::test_default()
}
}
/// Build an `AuthManager` rooted at `dir`. Caller keeps `dir` alive for
/// the duration of the test so the `TempDir` `Drop` actually cleans up.
fn make_manager(dir: &tempfile::TempDir, initial: Option<GrokAuth>) -> Arc<AuthManager> {
let mgr = AuthManager::new(dir.path(), GrokComConfig::default());
if let Some(auth) = initial {
mgr.hot_swap(auth);
}
Arc::new(mgr)
}
/// `apply()` and `snapshot()` agree (snapshot==wire invariant) when the
/// in-memory token is fresh.
#[test]
fn apply_and_snapshot_agree_on_live_token() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(
&dir,
Some(make_auth("live-token", ChronoDuration::hours(1))),
);
let provider = ShellAuthCredentialProvider::new(mgr, None, None);
let snap = provider.snapshot();
assert_eq!(snap.token.as_deref(), Some("live-token"));
assert_eq!(snap.user_id.as_deref(), Some("test-user"));
}
/// During the 5-minute pre-refresh buffer window, `auth_manager.current()`
/// returns `None` (the token is treated as expired-soon for refresh
/// scheduling), but the token is still valid at the proxy. The provider
/// must fall back to `expired_auth()` so the in-memory token gets sent
/// instead of nothing -- which is the fix for the bulk of the
/// `POST /v1/storage` 401s observed in production.
#[test]
fn falls_back_to_expired_auth_during_buffer_window() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(
&dir,
Some(make_auth("buffer-token", ChronoDuration::minutes(4))),
);
assert!(mgr.current().is_none(), "buffer-window precondition");
assert!(mgr.expired_auth().is_some(), "buffer-window precondition");
let provider = ShellAuthCredentialProvider::new(mgr, None, None);
let snap = provider.snapshot();
assert_eq!(
snap.token.as_deref(),
Some("buffer-token"),
"snapshot should fall back to expired_auth instead of None"
);
assert_eq!(snap.user_id.as_deref(), Some("test-user"));
}
/// When `auth_manager` has nothing at all (no in-memory auth, expired
/// or otherwise), `snapshot()` returns `None` for the user-token branch.
/// `apply()` would then send no Authorization header.
#[test]
fn no_token_when_auth_manager_is_empty() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(&dir, None);
let provider = ShellAuthCredentialProvider::new(mgr, None, None);
let snap = provider.snapshot();
assert!(
snap.token.is_none(),
"snapshot should be None when manager has no auth"
);
assert!(snap.user_id.is_none());
}
/// 401 recovery routes through `unauthorized_recovery` (pre-fix
/// it no-oped because the refresher arg was hardcoded `None`).
#[tokio::test]
async fn refresh_after_unauthorized_drives_recovery_state_machine() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = Arc::new(AuthManager::new(
dir.path(),
crate::auth::GrokComConfig::default(),
));
mgr.hot_swap(GrokAuth {
key: "stale".into(),
auth_mode: crate::auth::AuthMode::Oidc,
create_time: chrono::Utc::now() - ChronoDuration::hours(2),
user_id: "u".into(),
refresh_token: Some("rt-stale".into()),
expires_at: Some(chrono::Utc::now() - ChronoDuration::hours(1)),
..GrokAuth::test_default()
});
struct OkRefresher {
calls: Arc<std::sync::atomic::AtomicU32>,
}
#[async_trait::async_trait]
impl crate::auth::refresh::TokenRefresher for OkRefresher {
async fn refresh(
&self,
_r: crate::auth::manager::RefreshReason,
) -> crate::auth::refresh::RefreshOutcome {
self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
crate::auth::refresh::RefreshOutcome::Success(Box::new(GrokAuth {
key: "fresh".into(),
auth_mode: crate::auth::AuthMode::Oidc,
create_time: chrono::Utc::now(),
user_id: "u".into(),
refresh_token: Some("rt-new".into()),
expires_at: Some(chrono::Utc::now() + ChronoDuration::hours(1)),
..GrokAuth::test_default()
}))
}
}
let calls = Arc::new(std::sync::atomic::AtomicU32::new(0));
mgr.set_refresher(Arc::new(OkRefresher {
calls: calls.clone(),
}));
let provider = ShellAuthCredentialProvider::new(mgr.clone(), None, None);
assert!(provider.refresh_after_unauthorized().await);
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(mgr.current().unwrap().key, "fresh");
assert_eq!(
provider.snapshot().token.as_deref(),
Some("fresh"),
"snapshot must reflect refreshed token for subsequent apply() calls"
);
}
#[test]
fn embedding_session_credentials_scopes_to_first_party() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(
&dir,
Some(make_auth("xai-session-token", ChronoDuration::hours(1))),
);
let api_key_provider: xai_grok_tools::types::SharedApiKeyProvider =
Arc::new(crate::auth::manager::SharedAuthKeyProvider(mgr.clone()));
for denied in ["https://byok.attacker.example/v1", "http://api.x.ai/v1"] {
let resolved =
embedding_session_credentials(denied, Some(&mgr), Some(api_key_provider.clone()));
assert!(
resolved.is_empty(),
"session credentials must not reach {denied}"
);
}
let resolved = embedding_session_credentials(
"https://api.x.ai/v1",
Some(&mgr),
Some(api_key_provider),
);
assert!(!resolved.is_empty());
}
/// Deployment-key path has no recovery (operator owns the bearer).
#[tokio::test]
async fn refresh_after_unauthorized_is_noop_for_deployment_key() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(&dir, None);
let provider =
ShellAuthCredentialProvider::new(mgr, Some("deployment-key".to_string()), None);
assert!(!provider.refresh_after_unauthorized().await);
}
#[test]
fn snapshot_populates_tenant_id_per_auth_mode() {
use xai_grok_telemetry::config::deployment_id_from_key;
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let dep = ShellAuthCredentialProvider::new(
make_manager(&dir, None),
Some("xai-token-EX".into()),
None,
)
.snapshot();
assert_eq!(
dep.deployment_id.as_deref(),
Some(deployment_id_from_key("xai-token-EX").as_str())
);
assert!(dep.api_key_id.is_none());
let api_auth = GrokAuth {
key: "sk-apikey-xyz".into(),
auth_mode: crate::auth::AuthMode::ApiKey,
expires_at: Some(Utc::now() + ChronoDuration::hours(1)),
..GrokAuth::test_default()
};
let api = ShellAuthCredentialProvider::new(make_manager(&dir, Some(api_auth)), None, None)
.snapshot();
assert_eq!(
api.api_key_id.as_deref(),
Some(deployment_id_from_key("sk-apikey-xyz").as_str())
);
assert!(api.deployment_id.is_none());
let oidc = ShellAuthCredentialProvider::new(
make_manager(
&dir,
Some(make_auth("oidc-token", ChronoDuration::hours(1))),
),
None,
None,
)
.snapshot();
assert!(oidc.deployment_id.is_none() && oidc.api_key_id.is_none());
}
/// Bootstrap mode: `snapshot()` re-reads disk so sibling-rotated
/// tokens are picked up without a live AuthManager.
#[test]
fn otel_bootstrap_snapshot_picks_up_disk_writes() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let scope = crate::auth::GrokComConfig::default().auth_scope();
let auth_path = dir.path().join("auth.json");
let mgr = make_manager(
&dir,
Some(make_auth("initial-token", ChronoDuration::hours(1))),
);
let mut store = crate::auth::read_auth_json(&auth_path).unwrap_or_default();
store.insert(
scope.clone(),
make_auth("initial-token", ChronoDuration::hours(1)),
);
crate::auth::storage::write_auth_json(&auth_path, &store).unwrap();
let provider = OtelAuthCredentialProvider::new(mgr);
assert_eq!(provider.snapshot().token.as_deref(), Some("initial-token"));
let mut store = crate::auth::read_auth_json(&auth_path).unwrap();
store.insert(scope, make_auth("rotated-token", ChronoDuration::hours(1)));
crate::auth::storage::write_auth_json(&auth_path, &store).unwrap();
assert_eq!(
provider.snapshot().token.as_deref(),
Some("rotated-token"),
"must pick up sibling-rotated tokens from disk"
);
}
/// After `set_live()`, `snapshot()` reads from the live manager's
/// in-memory cache (no disk re-read) and `refresh_after_unauthorized()`
/// drives the recovery state machine.
#[tokio::test]
async fn otel_live_mode_uses_shared_auth_manager() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let bootstrap_dir = tempfile::tempdir().unwrap();
let bootstrap_mgr = make_manager(&bootstrap_dir, None);
let provider = OtelAuthCredentialProvider::new(bootstrap_mgr);
assert!(provider.snapshot().token.is_none());
assert!(!provider.refresh_after_unauthorized().await);
let live_dir = tempfile::tempdir().unwrap();
let live_mgr = make_manager(
&live_dir,
Some(make_auth("live-token", ChronoDuration::hours(1))),
);
provider.set_live(live_mgr.clone());
assert_eq!(
provider.snapshot().token.as_deref(),
Some("live-token"),
"must read from live AuthManager after set_live()"
);
live_mgr.hot_swap(make_auth("rotated-live", ChronoDuration::hours(1)));
assert_eq!(
provider.snapshot().token.as_deref(),
Some("rotated-live"),
"must see rotated token from live manager"
);
}
/// `refresh_after_unauthorized` drives recovery when live.
#[tokio::test]
async fn otel_live_refresh_after_unauthorized_drives_recovery() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let bootstrap_dir = tempfile::tempdir().unwrap();
let bootstrap_mgr = make_manager(&bootstrap_dir, None);
let provider = OtelAuthCredentialProvider::new(bootstrap_mgr);
let live_dir = tempfile::tempdir().unwrap();
let live_mgr = Arc::new(AuthManager::new(
live_dir.path(),
crate::auth::GrokComConfig::default(),
));
live_mgr.hot_swap(GrokAuth {
key: "stale".into(),
auth_mode: crate::auth::AuthMode::Oidc,
create_time: chrono::Utc::now() - ChronoDuration::hours(2),
user_id: "u".into(),
refresh_token: Some("rt-stale".into()),
expires_at: Some(chrono::Utc::now() - ChronoDuration::hours(1)),
..GrokAuth::test_default()
});
struct OkRefresher;
#[async_trait::async_trait]
impl crate::auth::refresh::TokenRefresher for OkRefresher {
async fn refresh(
&self,
_r: crate::auth::manager::RefreshReason,
) -> crate::auth::refresh::RefreshOutcome {
crate::auth::refresh::RefreshOutcome::Success(Box::new(GrokAuth {
key: "refreshed".into(),
auth_mode: crate::auth::AuthMode::Oidc,
create_time: chrono::Utc::now(),
user_id: "u".into(),
refresh_token: Some("rt-new".into()),
expires_at: Some(chrono::Utc::now() + ChronoDuration::hours(1)),
..GrokAuth::test_default()
}))
}
}
live_mgr.set_refresher(Arc::new(OkRefresher));
provider.set_live(live_mgr.clone());
assert!(
provider.refresh_after_unauthorized().await,
"live mode must drive recovery"
);
assert_eq!(live_mgr.current().unwrap().key, "refreshed");
}
#[test]
fn otel_deployment_key_sent_when_no_oidc_token() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(&dir, None);
let provider = OtelAuthCredentialProvider::new(mgr);
provider.set_deployment_key("enterprise-key".to_string());
let snap = provider.snapshot();
assert_eq!(
snap.token.as_deref(),
Some("enterprise-key"),
"deployment key must be sent when no OIDC token exists"
);
}
#[test]
fn otel_deployment_key_wins_over_oidc_token() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(
&dir,
Some(make_auth("oidc-token", ChronoDuration::hours(1))),
);
let provider = OtelAuthCredentialProvider::new(mgr);
provider.set_deployment_key("deployment-key-123".to_string());
let snap = provider.snapshot();
assert_eq!(
snap.token.as_deref(),
Some("deployment-key-123"),
"deployment key must win over OIDC token"
);
assert!(snap.user_id.is_none());
}
#[test]
fn has_usable_credential_reflects_auth_state() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(&dir, Some(make_auth("live", ChronoDuration::hours(1))));
let provider = OtelAuthCredentialProvider::new(mgr);
assert!(
provider.has_usable_credential(),
"valid unexpired token is usable"
);
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(&dir, Some(make_auth("stale", ChronoDuration::hours(-1))));
let provider = OtelAuthCredentialProvider::new(mgr);
assert!(
!provider.has_usable_credential(),
"expired token is not usable"
);
let dir = tempfile::tempdir().unwrap();
let provider = OtelAuthCredentialProvider::new(make_manager(&dir, None));
assert!(
!provider.has_usable_credential(),
"absent token is not usable"
);
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(&dir, Some(make_auth("live", ChronoDuration::hours(1))));
mgr.record_permanent_failure(
"live".into(),
crate::auth::error::RefreshTokenFailedReason::RefreshTokenRejected.into(),
);
let provider = OtelAuthCredentialProvider::new(mgr);
assert!(
provider.has_usable_credential(),
"a wire-valid token stays usable despite a permanent refresh verdict"
);
let dir = tempfile::tempdir().unwrap();
let provider = OtelAuthCredentialProvider::new(make_manager(&dir, None));
provider.set_deployment_key("enterprise-key".to_string());
assert!(
provider.has_usable_credential(),
"static deployment key is always usable"
);
}
/// A token inside the early-invalidation buffer is still accepted by the
/// proxy (the buffer is a client-side pre-refresh margin, not a wire
/// expiry), and the sender puts it on the wire via `current_or_expired()`.
/// The export gate must therefore keep it usable even though `current()`
/// reports `None`. Regression for the buffer-window export drop.
#[test]
fn has_usable_credential_true_inside_early_invalidation_buffer() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(
&dir,
Some(make_auth("buffered", ChronoDuration::minutes(4))),
);
assert!(
mgr.current().is_none(),
"4-min token sits inside the pinned 5-min buffer, so current() is None"
);
let provider = OtelAuthCredentialProvider::new(mgr);
assert!(
provider.has_usable_credential(),
"a buffer-window token is still wire-valid, so the gate keeps it usable"
);
}
/// A configured `deployment_key` always wins over the AuthManager-resolved
/// user token, matching the precedence in `GrokAuthCredentials::apply`.
/// The snapshot must report the deployment key (not the user token) so
/// the 401-attribution prefix matches the wire bytes.
#[test]
fn deployment_key_wins_over_resolved_user_token() {
let _guard = EarlyInvalidationGuard::pin_to_default();
let dir = tempfile::tempdir().unwrap();
let mgr = make_manager(
&dir,
Some(make_auth("user-token", ChronoDuration::hours(1))),
);
let provider =
ShellAuthCredentialProvider::new(mgr, Some("deployment-key-12345".to_string()), None);
let snap = provider.snapshot();
assert_eq!(snap.token.as_deref(), Some("deployment-key-12345"));
assert!(snap.user_id.is_none());
}
}