use std::sync::Arc;
use crate::admin_recovery::{RECOVERED_ADMIN_LABEL, recover};
use crate::storage::{TokenStore, build_token_store};
use crate::token::{ADMIN_SCOPE, IssueRequest, TokenManager};
const SECRET: &str = "a-test-signing-secret-for-admin-recovery";
fn store() -> (TokenManager, Arc<dyn TokenStore>, tempfile::TempDir) {
let data = tempfile::tempdir().expect("a temporary data directory");
let store = build_token_store(crate::config::StoragePolicy::Text, data.path())
.expect("a text token store");
let manager = TokenManager::with_store(SECRET, Arc::clone(&store));
(manager, store, data)
}
fn lose_an_admin_token(manager: &TokenManager) -> String {
let token = manager
.issue_admin_token(24, "bootstrap-admin")
.expect("a bootstrap admin token");
crate::managed_server::token_subject(&token).expect("its subject")
}
#[test]
fn a_store_whose_printed_admin_token_was_lost_can_be_administered_again() {
let (manager, store, _data) = store();
let lost = lose_an_admin_token(&manager);
let recovery = recover(&manager, &store, 24, RECOVERED_ADMIN_LABEL, false)
.expect("recovery from a local store");
let claims = manager
.validate_admin_token(&recovery.token)
.expect("the recovered token validates as an admin credential");
assert!(claims.is_admin(), "the recovered token carries admin scope");
assert_eq!(claims.sub, recovery.token_id);
assert_ne!(
recovery.token_id, lost,
"recovery mints a new credential rather than reprinting the lost one"
);
}
#[test]
fn recovery_preserves_the_client_tokens_the_deployment_already_issued() {
let (manager, store, _data) = store();
lose_an_admin_token(&manager);
let client = manager
.issue_token(24, "a-client")
.expect("a client token issued before recovery");
recover(&manager, &store, 24, RECOVERED_ADMIN_LABEL, false).expect("recovery");
let claims = manager
.validate_token(&client)
.expect("a client token minted before recovery still validates");
assert!(
!claims.is_admin(),
"the client token is not promoted by recovery"
);
}
#[test]
fn a_recovery_is_visible_in_the_store_afterwards() {
let (manager, store, _data) = store();
lose_an_admin_token(&manager);
let recovery = recover(&manager, &store, 24, RECOVERED_ADMIN_LABEL, false).expect("recovery");
let record = store
.get(&recovery.token_id)
.expect("the store can be read")
.expect("the recovered token has a record");
assert_eq!(record.label, RECOVERED_ADMIN_LABEL);
assert_eq!(record.scope, ADMIN_SCOPE);
assert_ne!(
record.label, "bootstrap-admin",
"a recovery is distinguishable from a first boot"
);
}
#[test]
fn recovery_reports_that_the_lost_administrator_is_still_live() {
let (manager, store, _data) = store();
lose_an_admin_token(&manager);
let recovery = recover(&manager, &store, 24, RECOVERED_ADMIN_LABEL, false).expect("recovery");
assert_eq!(recovery.retained_admins, 1);
assert!(recovery.revoked.is_empty());
}
#[test]
fn revoking_others_retires_the_lost_administrator() {
let (manager, store, _data) = store();
let lost = lose_an_admin_token(&manager);
let recovery = recover(&manager, &store, 24, "after-compromise", true).expect("recovery");
assert_eq!(recovery.revoked, vec![lost.clone()]);
assert_eq!(recovery.retained_admins, 0);
let lost_record = store
.get(&lost)
.expect("the store can be read")
.expect("the lost token still has a record");
assert!(lost_record.revoked, "the lost administrator is revoked");
manager
.validate_admin_token(&recovery.token)
.expect("the replacement survives --revoke-others");
}
#[test]
fn revoking_others_leaves_client_tokens_alone() {
let (manager, store, _data) = store();
lose_an_admin_token(&manager);
let client = manager
.issue_token(24, "a-client")
.expect("a client token issued before recovery");
recover(&manager, &store, 24, "after-compromise", true).expect("recovery");
manager
.validate_token(&client)
.expect("a client token survives --revoke-others");
}
#[test]
fn an_expired_or_revoked_administrator_is_not_counted_as_retained() {
let (manager, store, _data) = store();
let expired = manager
.issue(&IssueRequest {
ttl_hours: -1,
label: "long-expired-admin",
account: None,
max_requests: None,
max_tokens: None,
rate_limit_per_minute: None,
scope: ADMIN_SCOPE,
github_repos: Vec::new(),
sliding_window_seconds: None,
client_kind: None,
principal_id: None,
})
.expect("an expired admin token");
let expired_id = crate::managed_server::token_subject(&expired).expect("its subject");
let recovery = recover(&manager, &store, 24, RECOVERED_ADMIN_LABEL, false).expect("recovery");
assert_eq!(
recovery.retained_admins, 0,
"an expired administrator holds no access"
);
assert!(
store
.get(&expired_id)
.expect("the store can be read")
.is_some(),
"the expired record is left in place rather than tidied away"
);
}
#[test]
fn a_store_with_no_administrator_at_all_can_still_be_recovered() {
let (manager, store, _data) = store();
let recovery = recover(&manager, &store, 24, RECOVERED_ADMIN_LABEL, false)
.expect("recovery against a store holding no administrator");
assert_eq!(recovery.retained_admins, 0);
manager
.validate_admin_token(&recovery.token)
.expect("the first administrator of this store validates");
}
#[test]
fn a_non_positive_lifetime_is_refused_rather_than_minting_a_dead_token() {
let (manager, store, _data) = store();
let error = recover(&manager, &store, 0, RECOVERED_ADMIN_LABEL, false)
.expect_err("a zero lifetime is refused");
assert!(
error.contains("positive lifetime"),
"the refusal explains itself: {error}"
);
assert!(
store.list().expect("the store can be read").is_empty(),
"a refused recovery mints nothing"
);
}
#[test]
fn a_token_recovered_from_one_store_is_rejected_by_a_deployment_with_another_secret() {
let (manager, store, _data) = store();
let recovery = recover(&manager, &store, 24, RECOVERED_ADMIN_LABEL, false).expect("recovery");
let elsewhere = TokenManager::new("a-different-deployments-signing-secret");
assert!(
elsewhere.validate_admin_token(&recovery.token).is_err(),
"a foreign deployment rejects a token recovered from this store"
);
}
fn unreadable() -> crate::storage::StorageError {
crate::storage::StorageError::Io(std::io::Error::other("the data directory is gone"))
}
#[derive(Debug)]
struct UnreadableStore;
impl TokenStore for UnreadableStore {
fn list(&self) -> Result<Vec<crate::storage::TokenRecord>, crate::storage::StorageError> {
Err(unreadable())
}
fn get(
&self,
_id: &str,
) -> Result<Option<crate::storage::TokenRecord>, crate::storage::StorageError> {
Err(unreadable())
}
fn put(
&self,
_record: crate::storage::TokenRecord,
) -> Result<(), crate::storage::StorageError> {
Err(unreadable())
}
fn delete(&self, _id: &str) -> Result<bool, crate::storage::StorageError> {
Err(unreadable())
}
}
#[test]
fn an_unreadable_store_is_reported_rather_than_mistaken_for_having_no_administrator() {
let store: Arc<dyn TokenStore> = Arc::new(UnreadableStore);
let manager = TokenManager::with_store(SECRET, Arc::clone(&store));
let error = recover(&manager, &store, 24, RECOVERED_ADMIN_LABEL, false)
.expect_err("an unreadable store fails recovery");
assert!(
error.contains("could not read the token store"),
"the failure names what could not be done: {error}"
);
assert!(
error.contains("data directory is gone"),
"the storage layer's own words survive: {error}"
);
}
#[test]
fn the_debug_rendering_of_a_recovery_redacts_the_credential() {
let (manager, store, _data) = store();
let recovery = recover(&manager, &store, 24, RECOVERED_ADMIN_LABEL, false).expect("recovery");
let rendered = format!("{recovery:?}");
assert!(
!rendered.contains(&recovery.token),
"the token is not in the debug output: {rendered}"
);
assert!(rendered.contains("<redacted>"), "{rendered}");
assert!(rendered.contains(&recovery.token_id), "{rendered}");
}
#[test]
fn the_refusal_to_recover_over_http_names_the_selected_deployment() {
let server = crate::managed_server::ResolvedServer::at(
"https://router.example:8443",
Some("la_sk_example".to_string()),
"test",
);
let refusal = crate::admin_recovery::refusal(&server);
assert!(
refusal.contains("https://router.example:8443"),
"the refusal names the target: {refusal}"
);
assert!(
refusal.contains("docker exec"),
"the refusal says how to run it on the deployment: {refusal}"
);
}