move stored_secret from swarm-queue-client to swarm-secret-client
The module is a rename (97% similar); everything else in the diff follows from the move: - stored_secret.rs: private `chain()` helper, the 8-line copy of the queue crate's `chain` the module called as `crate::chain`; the queue crate's public `chain` stays. - stored_secret.rs: `Duration::from_mins(5)` in `MAX_REREAD_DELAY` is unchanged moved code, not a new use. - swarm-secret-client Cargo.toml: gains futures-util, tokio and tracing as dependencies and tokio `test-util` plus tracing-subscriber as dev dependencies; the module and its tests use them. - swarm-queue-client Cargo.toml: drops futures-util and the tracing-subscriber dev dependency, which only the moved module used; its tokio dev comment is reworded to the tests that remain. - Cargo.lock: the same dependency moves, nothing else. - swarm-queue-client lib.rs: imports `swarm_secret_client::stored_secret`, and its tests use a small local `Store` in place of the moved module's test-only `Fake`, which is no longer reachable. - callers (hive-c0re, swarm-builder, swarm-ci-metrics, swarm-claude-login, swarm-controller, swarm-nats-auth, swarm-review-classifier): import path only; no re-export is left behind.
This commit is contained in:
parent
6ac637c7e3
commit
f4f459f0b2
16 changed files with 83 additions and 33 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -5022,7 +5022,6 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"backon",
|
"backon",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"futures-util",
|
|
||||||
"hive-sh4re",
|
"hive-sh4re",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -5032,7 +5031,6 @@ dependencies = [
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
|
||||||
"utoipa",
|
"utoipa",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -5062,6 +5060,7 @@ dependencies = [
|
||||||
name = "swarm-secret-client"
|
name = "swarm-secret-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rustify",
|
"rustify",
|
||||||
"rustify_derive",
|
"rustify_derive",
|
||||||
|
|
@ -5070,6 +5069,9 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum",
|
"strum",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
"vaultrs",
|
"vaultrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
//! environment. The client secret comes from the swarm's secret store, at
|
//! environment. The client secret comes from the swarm's secret store, at
|
||||||
//! `swarm_secret_client::queue::hive_client_path`, read under this hive's own
|
//! `swarm_secret_client::queue::hive_client_path`, read under this hive's own
|
||||||
//! certificate, held only in memory and read again when authelia refuses it
|
//! certificate, held only in memory and read again when authelia refuses it
|
||||||
//! (`swarm_queue_client::stored_secret`). `swarm-secret-publish`, in authelia's
|
//! (`swarm_secret_client::stored_secret`). `swarm-secret-publish`, in authelia's
|
||||||
//! container, puts it there.
|
//! container, puts it there.
|
||||||
//!
|
//!
|
||||||
//! Connects lazily on first use rather than at boot — nothing here
|
//! Connects lazily on first use rather than at boot — nothing here
|
||||||
|
|
@ -28,9 +28,9 @@ use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
use swarm_queue_client::stored_secret::StoredSecret;
|
|
||||||
use swarm_queue_client::{ClientSecret, QueueConfig};
|
use swarm_queue_client::{ClientSecret, QueueConfig};
|
||||||
use swarm_secret_client::queue::{self, HiveClientCredential};
|
use swarm_secret_client::queue::{self, HiveClientCredential};
|
||||||
|
use swarm_secret_client::stored_secret::StoredSecret;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,9 @@ use swarm_queue_client::builder::{
|
||||||
ARCHETYPES_DEADLINE, ARCHETYPES_SUBJECT, BUILD_DEADLINE, BuildAgent, QUEUE_GROUP,
|
ARCHETYPES_DEADLINE, ARCHETYPES_SUBJECT, BUILD_DEADLINE, BuildAgent, QUEUE_GROUP,
|
||||||
REPLY_DEADLINE, Reply, SUBJECT,
|
REPLY_DEADLINE, Reply, SUBJECT,
|
||||||
};
|
};
|
||||||
use swarm_queue_client::stored_secret::StoredSecret;
|
|
||||||
use swarm_queue_client::{ClientSecret, QueueConfig};
|
use swarm_queue_client::{ClientSecret, QueueConfig};
|
||||||
use swarm_secret_client::forge::Credential;
|
use swarm_secret_client::forge::Credential;
|
||||||
|
use swarm_secret_client::stored_secret::StoredSecret;
|
||||||
|
|
||||||
mod archetypes;
|
mod archetypes;
|
||||||
mod nix_build;
|
mod nix_build;
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ use std::time::Duration;
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use forgejo_api::structs::{ActionRun, ListActionRunsQuery, RepoSearchQuery};
|
use forgejo_api::structs::{ActionRun, ListActionRunsQuery, RepoSearchQuery};
|
||||||
use forgejo_api::{Auth, Forgejo};
|
use forgejo_api::{Auth, Forgejo};
|
||||||
use swarm_queue_client::stored_secret::StoredSecret;
|
|
||||||
use swarm_queue_client::{ClientSecret, QueueConfig};
|
use swarm_queue_client::{ClientSecret, QueueConfig};
|
||||||
use swarm_secret_client::SecretStore;
|
use swarm_secret_client::SecretStore;
|
||||||
use swarm_secret_client::ci_metrics::{ClientSecret as StoredClientSecret, client_secret_path};
|
use swarm_secret_client::ci_metrics::{ClientSecret as StoredClientSecret, client_secret_path};
|
||||||
use swarm_secret_client::forge::Credential;
|
use swarm_secret_client::forge::Credential;
|
||||||
|
use swarm_secret_client::stored_secret::StoredSecret;
|
||||||
|
|
||||||
mod cursor;
|
mod cursor;
|
||||||
mod otlp;
|
mod otlp;
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ use std::sync::Arc;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use futures_util::StreamExt as _;
|
use futures_util::StreamExt as _;
|
||||||
use swarm_queue_client::claude_login::{Reply, Request, SESSION_DEADLINE, STEP_DEADLINE, SUBJECT};
|
use swarm_queue_client::claude_login::{Reply, Request, SESSION_DEADLINE, STEP_DEADLINE, SUBJECT};
|
||||||
use swarm_queue_client::stored_secret::StoredSecret;
|
|
||||||
use swarm_queue_client::{ClientSecret, QueueConfig};
|
use swarm_queue_client::{ClientSecret, QueueConfig};
|
||||||
use swarm_secret_client::claude_login::{Credentials, client_secret_path, credentials_path};
|
use swarm_secret_client::claude_login::{Credentials, client_secret_path, credentials_path};
|
||||||
|
use swarm_secret_client::stored_secret::StoredSecret;
|
||||||
|
|
||||||
mod login;
|
mod login;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::webhook::DeliveryKind;
|
use crate::webhook::DeliveryKind;
|
||||||
use swarm_queue_client::review_classifier::Commit;
|
use swarm_queue_client::review_classifier::Commit;
|
||||||
use swarm_queue_client::stored_secret::StoredSecret;
|
use swarm_secret_client::stored_secret::StoredSecret;
|
||||||
|
|
||||||
pub mod agent_token;
|
pub mod agent_token;
|
||||||
mod api;
|
mod api;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use std::sync::{Arc, Mutex};
|
||||||
use forgejo_api::{ApiErrorKind, Auth, Forgejo, ForgejoError};
|
use forgejo_api::{ApiErrorKind, Auth, Forgejo, ForgejoError};
|
||||||
use futures_util::future::BoxFuture;
|
use futures_util::future::BoxFuture;
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use swarm_queue_client::stored_secret::StoredSecret;
|
use swarm_secret_client::stored_secret::StoredSecret;
|
||||||
|
|
||||||
/// One forge request, as [`Api::call`] runs it.
|
/// One forge request, as [`Api::call`] runs it.
|
||||||
pub(crate) type Call<'a, R> = BoxFuture<'a, Result<R, ForgejoError>>;
|
pub(crate) type Call<'a, R> = BoxFuture<'a, Result<R, ForgejoError>>;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
//! environment. The client secret comes from the swarm's secret store, at
|
//! environment. The client secret comes from the swarm's secret store, at
|
||||||
//! `swarm_secret_client::queue::controller_client_path`, read at start under
|
//! `swarm_secret_client::queue::controller_client_path`, read at start under
|
||||||
//! this daemon's own certificate, held only in memory and read again when
|
//! this daemon's own certificate, held only in memory and read again when
|
||||||
//! authelia refuses it (`swarm_queue_client::stored_secret`). It is never on
|
//! authelia refuses it (`swarm_secret_client::stored_secret`). It is never on
|
||||||
//! this host's disk. `swarm-secret-publish`, in authelia's
|
//! this host's disk. `swarm-secret-publish`, in authelia's
|
||||||
//! container, puts it there.
|
//! container, puts it there.
|
||||||
//!
|
//!
|
||||||
|
|
@ -15,9 +15,9 @@ use std::path::PathBuf;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use swarm_queue_client::stored_secret::StoredSecret;
|
|
||||||
use swarm_queue_client::{ClientSecret, QueueConfig};
|
use swarm_queue_client::{ClientSecret, QueueConfig};
|
||||||
use swarm_secret_client::queue::{self, ControllerCredential};
|
use swarm_secret_client::queue::{self, ControllerCredential};
|
||||||
|
use swarm_secret_client::stored_secret::StoredSecret;
|
||||||
|
|
||||||
/// The environment prefix the nix module's `queueEnv` sets.
|
/// The environment prefix the nix module's `queueEnv` sets.
|
||||||
const PREFIX: &str = "SWARM_CONTROLLER";
|
const PREFIX: &str = "SWARM_CONTROLLER";
|
||||||
|
|
|
||||||
|
|
@ -714,7 +714,7 @@ mod tests {
|
||||||
.with_agent_token_subjects(vec!["$SWARM.term.{agent}".to_owned()])
|
.with_agent_token_subjects(vec!["$SWARM.term.{agent}".to_owned()])
|
||||||
.expect("valid");
|
.expect("valid");
|
||||||
let client_secret =
|
let client_secret =
|
||||||
oidc_secret::OidcSecret::new(swarm_queue_client::stored_secret::StoredSecret::new(
|
oidc_secret::OidcSecret::new(swarm_secret_client::stored_secret::StoredSecret::new(
|
||||||
"swarm-nats",
|
"swarm-nats",
|
||||||
"unused".to_owned(),
|
"unused".to_owned(),
|
||||||
|| async { Err("never re-read".into()) },
|
|| async { Err("never re-read".into()) },
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use swarm_queue_client::stored_secret::{StoredSecret, is_credential_refusal};
|
|
||||||
use swarm_secret_client::client::{DEFAULT_CERT_MOUNT, Settings};
|
use swarm_secret_client::client::{DEFAULT_CERT_MOUNT, Settings};
|
||||||
use swarm_secret_client::path::{Kind, principal_prefix};
|
use swarm_secret_client::path::{Kind, principal_prefix};
|
||||||
|
use swarm_secret_client::stored_secret::{StoredSecret, is_credential_refusal};
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
|
|
||||||
use crate::introspect::{Answer, Caller};
|
use crate::introspect::{Answer, Caller};
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,6 @@ agent-archetype = ["async-nats/jetstream"]
|
||||||
# above - the connect itself needs none of them.
|
# above - the connect itself needs none of them.
|
||||||
async-nats.workspace = true
|
async-nats.workspace = true
|
||||||
backon.workspace = true
|
backon.workspace = true
|
||||||
# `Shared`, so every caller refused while a secret re-read runs waits on that
|
|
||||||
# one read (`stored_secret`).
|
|
||||||
futures-util.workspace = true
|
|
||||||
hive-sh4re = { workspace = true, optional = true }
|
hive-sh4re = { workspace = true, optional = true }
|
||||||
# `blocking` on top of the workspace default (`form`/`json`/`rustls`) —
|
# `blocking` on top of the workspace default (`form`/`json`/`rustls`) —
|
||||||
# `mint_token_for_blocking` needs `reqwest::blocking::Client` for a caller
|
# `mint_token_for_blocking` needs `reqwest::blocking::Client` for a caller
|
||||||
|
|
@ -85,7 +82,8 @@ reqwest = { workspace = true, features = ["blocking"] }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
# The rustls provider `connect` installs.
|
# `stored_secret::StoredSecret`, the held client secret, and the rustls
|
||||||
|
# provider `connect` installs.
|
||||||
swarm-secret-client.workspace = true
|
swarm-secret-client.workspace = true
|
||||||
# A library, so its errors are a matchable enum rather than an opaque
|
# A library, so its errors are a matchable enum rather than an opaque
|
||||||
# `anyhow::Error`. The binaries that consume this keep anyhow; `?` converts.
|
# `anyhow::Error`. The binaries that consume this keep anyhow; `?` converts.
|
||||||
|
|
@ -98,12 +96,9 @@ utoipa = { workspace = true, optional = true }
|
||||||
# A loopback stand-in token endpoint for the client secret re-read tests.
|
# A loopback stand-in token endpoint for the client secret re-read tests.
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
data-encoding.workspace = true
|
data-encoding.workspace = true
|
||||||
# `test-util` for `#[tokio::test(start_paused = true)]`: the re-read backoff
|
# `test-util` for `#[tokio::test(start_paused = true)]`: the startup-read
|
||||||
# tests run on paused time.
|
# retry tests run on paused time.
|
||||||
tokio = { workspace = true, features = ["test-util"] }
|
tokio = { workspace = true, features = ["test-util"] }
|
||||||
# Capturing what a re-read logs, to check it names the client and never the
|
|
||||||
# secret.
|
|
||||||
tracing-subscriber.workspace = true
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use swarm_secret_client::stored_secret::{self, StoredSecret};
|
||||||
|
|
||||||
/// Everything that can go wrong reaching the swarm queue.
|
/// Everything that can go wrong reaching the swarm queue.
|
||||||
///
|
///
|
||||||
/// `thiserror` and not `anyhow` because this is a library: a caller gets a
|
/// `thiserror` and not `anyhow` because this is a library: a caller gets a
|
||||||
|
|
@ -480,8 +482,6 @@ pub mod subagent_term;
|
||||||
/// same shapes.
|
/// same shapes.
|
||||||
pub mod review_classifier;
|
pub mod review_classifier;
|
||||||
|
|
||||||
pub mod stored_secret;
|
|
||||||
|
|
||||||
mod startup_read;
|
mod startup_read;
|
||||||
pub use startup_read::read_with_startup_retry;
|
pub use startup_read::read_with_startup_retry;
|
||||||
|
|
||||||
|
|
@ -588,13 +588,13 @@ pub enum ClientSecret {
|
||||||
File(PathBuf),
|
File(PathBuf),
|
||||||
/// The plaintext, held only in this process's memory and read again
|
/// The plaintext, held only in this process's memory and read again
|
||||||
/// when the token endpoint refuses it.
|
/// when the token endpoint refuses it.
|
||||||
Stored(stored_secret::StoredSecret),
|
Stored(StoredSecret),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientSecret {
|
impl ClientSecret {
|
||||||
/// The secret to read again after `error`: a held one the token endpoint
|
/// The secret to read again after `error`: a held one the token endpoint
|
||||||
/// refused as a credential ([`stored_secret::is_credential_refusal`]).
|
/// refused as a credential ([`stored_secret::is_credential_refusal`]).
|
||||||
fn refused(&self, error: &Error) -> Option<&stored_secret::StoredSecret> {
|
fn refused(&self, error: &Error) -> Option<&StoredSecret> {
|
||||||
match (self, error) {
|
match (self, error) {
|
||||||
(Self::Stored(stored), Error::TokenRefused { status, .. })
|
(Self::Stored(stored), Error::TokenRefused { status, .. })
|
||||||
if stored_secret::is_credential_refusal(*status) =>
|
if stored_secret::is_credential_refusal(*status) =>
|
||||||
|
|
@ -1096,6 +1096,34 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// A store holding `secret`, counting how often it is read.
|
||||||
|
struct Store {
|
||||||
|
secret: &'static str,
|
||||||
|
reads: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
fn holding(secret: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
secret,
|
||||||
|
reads: Arc::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `client_id`'s secret, `initial` in use, re-read from this store.
|
||||||
|
fn secret(&self, client_id: &str, initial: &str) -> StoredSecret {
|
||||||
|
let (secret, reads) = (self.secret, Arc::clone(&self.reads));
|
||||||
|
StoredSecret::new(client_id, initial.to_owned(), move || {
|
||||||
|
reads.fetch_add(1, Ordering::SeqCst);
|
||||||
|
async move { Ok(secret.to_owned()) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reads(&self) -> usize {
|
||||||
|
self.reads.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The names the publishers, the relays and the queue's grant all spell.
|
/// The names the publishers, the relays and the queue's grant all spell.
|
||||||
#[test]
|
#[test]
|
||||||
fn term_and_agent_state_subjects_are_the_agreed_names() {
|
fn term_and_agent_state_subjects_are_the_agreed_names() {
|
||||||
|
|
@ -1126,7 +1154,7 @@ mod tests {
|
||||||
async fn a_debug_print_of_a_config_does_not_carry_a_held_secret() {
|
async fn a_debug_print_of_a_config_does_not_carry_a_held_secret() {
|
||||||
let cfg = QueueConfig {
|
let cfg = QueueConfig {
|
||||||
client_secret: ClientSecret::Stored(
|
client_secret: ClientSecret::Stored(
|
||||||
stored_secret::tests::Fake::holding("new").secret("hive-alpha", "s3cret"),
|
Store::holding("new").secret("hive-alpha", "s3cret"),
|
||||||
),
|
),
|
||||||
..token_cfg()
|
..token_cfg()
|
||||||
};
|
};
|
||||||
|
|
@ -1180,7 +1208,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_refused_secret_is_re_read_and_the_mint_retried_with_the_new_one() {
|
async fn a_refused_secret_is_re_read_and_the_mint_retried_with_the_new_one() {
|
||||||
let calls = Arc::new(AtomicUsize::new(0));
|
let calls = Arc::new(AtomicUsize::new(0));
|
||||||
let store = stored_secret::tests::Fake::holding("secret-b");
|
let store = Store::holding("secret-b");
|
||||||
let cfg = QueueConfig {
|
let cfg = QueueConfig {
|
||||||
token_endpoint: stub_idp("secret-b", Arc::clone(&calls)).await,
|
token_endpoint: stub_idp("secret-b", Arc::clone(&calls)).await,
|
||||||
client_secret: ClientSecret::Stored(store.secret("hive-alpha", "secret-a")),
|
client_secret: ClientSecret::Stored(store.secret("hive-alpha", "secret-a")),
|
||||||
|
|
@ -1200,7 +1228,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_permanently_refused_secret_is_not_re_read_on_every_mint() {
|
async fn a_permanently_refused_secret_is_not_re_read_on_every_mint() {
|
||||||
let calls = Arc::new(AtomicUsize::new(0));
|
let calls = Arc::new(AtomicUsize::new(0));
|
||||||
let store = stored_secret::tests::Fake::holding("secret-a");
|
let store = Store::holding("secret-a");
|
||||||
let cfg = QueueConfig {
|
let cfg = QueueConfig {
|
||||||
token_endpoint: stub_idp("secret-b", Arc::clone(&calls)).await,
|
token_endpoint: stub_idp("secret-b", Arc::clone(&calls)).await,
|
||||||
client_secret: ClientSecret::Stored(store.secret("hive-alpha", "secret-a")),
|
client_secret: ClientSecret::Stored(store.secret("hive-alpha", "secret-a")),
|
||||||
|
|
@ -1238,7 +1266,7 @@ mod tests {
|
||||||
let url = format!("http://{}/token", listener.local_addr().expect("addr"));
|
let url = format!("http://{}/token", listener.local_addr().expect("addr"));
|
||||||
tokio::spawn(async move { axum::serve(listener, app).await });
|
tokio::spawn(async move { axum::serve(listener, app).await });
|
||||||
|
|
||||||
let store = stored_secret::tests::Fake::holding("secret-b");
|
let store = Store::holding("secret-b");
|
||||||
let cfg = QueueConfig {
|
let cfg = QueueConfig {
|
||||||
token_endpoint: url,
|
token_endpoint: url,
|
||||||
client_secret: ClientSecret::Stored(store.secret("hive-alpha", "secret-a")),
|
client_secret: ClientSecret::Stored(store.secret("hive-alpha", "secret-a")),
|
||||||
|
|
@ -1261,7 +1289,7 @@ mod tests {
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn the_blocking_mint_re_reads_a_refused_secret() {
|
async fn the_blocking_mint_re_reads_a_refused_secret() {
|
||||||
let calls = Arc::new(AtomicUsize::new(0));
|
let calls = Arc::new(AtomicUsize::new(0));
|
||||||
let store = stored_secret::tests::Fake::holding("secret-b");
|
let store = Store::holding("secret-b");
|
||||||
let cfg = QueueConfig {
|
let cfg = QueueConfig {
|
||||||
token_endpoint: stub_idp("secret-b", Arc::clone(&calls)).await,
|
token_endpoint: stub_idp("secret-b", Arc::clone(&calls)).await,
|
||||||
client_secret: ClientSecret::Stored(store.secret("hive-alpha", "secret-a")),
|
client_secret: ClientSecret::Stored(store.secret("hive-alpha", "secret-a")),
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ use std::path::PathBuf;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use futures_util::StreamExt as _;
|
use futures_util::StreamExt as _;
|
||||||
use swarm_queue_client::review_classifier::{QUEUE_GROUP, Reply, SUBJECT, TryOut};
|
use swarm_queue_client::review_classifier::{QUEUE_GROUP, Reply, SUBJECT, TryOut};
|
||||||
use swarm_queue_client::stored_secret::StoredSecret;
|
|
||||||
use swarm_queue_client::{ClientSecret, QueueConfig};
|
use swarm_queue_client::{ClientSecret, QueueConfig};
|
||||||
|
use swarm_secret_client::stored_secret::StoredSecret;
|
||||||
|
|
||||||
mod acp;
|
mod acp;
|
||||||
mod aggregate;
|
mod aggregate;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# `Shared`, so every caller refused while a secret re-read runs waits on that
|
||||||
|
# one read (`stored_secret`).
|
||||||
|
futures-util.workspace = true
|
||||||
# `Identity` is re-exported by vaultrs, but the identity is built here (from
|
# `Identity` is re-exported by vaultrs, but the identity is built here (from
|
||||||
# the `BAO_*` files) rather than by its env defaults, so the dependency is
|
# the `BAO_*` files) rather than by its env defaults, so the dependency is
|
||||||
# direct rather than incidental.
|
# direct rather than incidental.
|
||||||
|
|
@ -21,6 +24,9 @@ serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
# `stored_secret` spawns its re-reads and times them out.
|
||||||
|
tokio.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
vaultrs.workspace = true
|
vaultrs.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|
@ -31,3 +37,9 @@ workspace = true
|
||||||
# test would pass with `install_crypto_provider` doing nothing.
|
# test would pass with `install_crypto_provider` doing nothing.
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rustls = { workspace = true, features = ["ring"] }
|
rustls = { workspace = true, features = ["ring"] }
|
||||||
|
# `test-util` for `#[tokio::test(start_paused = true)]`: the re-read backoff
|
||||||
|
# tests run on paused time.
|
||||||
|
tokio = { workspace = true, features = ["test-util"] }
|
||||||
|
# Capturing what a re-read logs, to check it names the client and never the
|
||||||
|
# secret.
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ pub mod path;
|
||||||
pub mod policy;
|
pub mod policy;
|
||||||
pub mod queue;
|
pub mod queue;
|
||||||
pub mod review_classifier;
|
pub mod review_classifier;
|
||||||
|
pub mod stored_secret;
|
||||||
|
|
||||||
pub use client::SecretStore;
|
pub use client::SecretStore;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ impl Inner {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Ok(Ok(_)) => "the stored client secret is the one the IdP refused".to_owned(),
|
Ok(Ok(_)) => "the stored client secret is the one the IdP refused".to_owned(),
|
||||||
Ok(Err(e)) => format!("could not re-read the client secret: {}", crate::chain(&*e)),
|
Ok(Err(e)) => format!("could not re-read the client secret: {}", chain(&*e)),
|
||||||
Err(_) => format!("re-reading the client secret timed out after {REREAD_TIMEOUT:?}"),
|
Err(_) => format!("re-reading the client secret timed out after {REREAD_TIMEOUT:?}"),
|
||||||
};
|
};
|
||||||
rereads.fruitless = rereads.fruitless.saturating_add(1);
|
rereads.fruitless = rereads.fruitless.saturating_add(1);
|
||||||
|
|
@ -205,6 +205,18 @@ impl Inner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `error` and its sources, joined with `: `.
|
||||||
|
fn chain(error: &dyn std::error::Error) -> String {
|
||||||
|
let mut rendered = error.to_string();
|
||||||
|
let mut source = error.source();
|
||||||
|
while let Some(cause) = source {
|
||||||
|
rendered.push_str(": ");
|
||||||
|
rendered.push_str(&cause.to_string());
|
||||||
|
source = cause.source();
|
||||||
|
}
|
||||||
|
rendered
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether an `IdP` answering `status` refused the client's credential (400 or
|
/// Whether an `IdP` answering `status` refused the client's credential (400 or
|
||||||
/// 401), the one answer a re-read can fix. A 429 or a 5xx says nothing about
|
/// 401), the one answer a re-read can fix. A 429 or a 5xx says nothing about
|
||||||
/// the secret, and re-reading on one would spend the backoff on an outage.
|
/// the secret, and re-reading on one would spend the backoff on an outage.
|
||||||
Loading…
Reference in a new issue