ai-server --enroll-link: mint one more device's link while the server runs
Prints the enrollment URI, one line on stdout, and exits; the running server adopts the token the first time that device presents it, via the spool wg-app-link's enroll module now provides (submodule bumped to d35c880). This is the server half of enrolling through Dev Updater: its coming per-component Enroll button runs this command and opens whatever it prints on the phone, which is what a reinstall -- a signing change, a new phone -- needs when nobody is at the terminal the QR is printed on. Verified against the sandbox server: minted while it ran, first request with the token served and the token moved into config.ron, spool empty, second request served as an ordinary token. 108 tests, clippy clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
68e77c7b37
commit
1656b058bf
7 files changed
+138
-13
No files matched your search
+63
-4
@@ -21,8 +21,9 @@ use axum::extract::{ConnectInfo, Request, State};
|
||||
use axum::http::{StatusCode, header};
|
||||
use axum::middleware::Next;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use wg_app_link::enroll::token_matches;
|
||||
use wg_app_link::enroll::{take_pending, token_hash_hex, token_matches};
|
||||
|
||||
use crate::config::TokenEntry;
|
||||
use crate::session::SessionManager;
|
||||
|
||||
/// Applied to every rejection. Not against brute force -- infeasible at 256
|
||||
@@ -49,6 +50,26 @@ pub async fn require_token(
|
||||
if token_matches(token, &hashes) {
|
||||
return next.run(request).await;
|
||||
}
|
||||
// A link minted by `--enroll-link` while this server was running:
|
||||
// the entry moves from the spool into the config here, on first
|
||||
// use, and is an ordinary token from then on.
|
||||
match take_pending(&manager.pending_enrollments_dir(), token) {
|
||||
Ok(Some(name)) => {
|
||||
let entry = TokenEntry {
|
||||
name,
|
||||
sha256: token_hash_hex(token),
|
||||
};
|
||||
match manager.add_token(entry) {
|
||||
Ok(()) => {
|
||||
tracing::info!("adopted an enrollment minted with --enroll-link");
|
||||
return next.run(request).await;
|
||||
}
|
||||
Err(error) => tracing::error!("couldn't adopt the enrollment: {error:#}"),
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(error) => tracing::warn!("couldn't read the enrollment spool: {error:#}"),
|
||||
}
|
||||
}
|
||||
|
||||
// Peer address only -- never the header value. Absent when there is no
|
||||
@@ -73,9 +94,7 @@ mod tests {
|
||||
use axum::routing::get;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use wg_app_link::enroll::{generate_token, token_hash_hex};
|
||||
|
||||
use crate::config::TokenEntry;
|
||||
use wg_app_link::enroll::{generate_token, spool_pending};
|
||||
|
||||
fn manager_with_token(dir: &std::path::Path, token: &str) -> Arc<SessionManager> {
|
||||
let manager = Arc::new(
|
||||
@@ -184,4 +203,44 @@ mod tests {
|
||||
// The rejections themselves do get logged (that's the point).
|
||||
assert!(logged.contains("missing or invalid bearer token"));
|
||||
}
|
||||
|
||||
/// A token spooled by `--enroll-link` is refused by nothing: the first
|
||||
/// request carrying it is served, and from then on it is in the config
|
||||
/// like any other.
|
||||
#[tokio::test]
|
||||
async fn a_spooled_enrollment_is_adopted_on_first_use() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let manager = manager_with_token(dir.path(), "first");
|
||||
let spooled = generate_token();
|
||||
spool_pending(&manager.pending_enrollments_dir(), "tablet", &spooled).unwrap();
|
||||
|
||||
let router = guarded_router(Arc::clone(&manager));
|
||||
let bearer = format!("Bearer {spooled}");
|
||||
let served = router
|
||||
.clone()
|
||||
.oneshot(request("/probe", Some(&bearer)))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(served.status(), StatusCode::OK);
|
||||
let names: Vec<String> = manager.tokens().into_iter().map(|t| t.name).collect();
|
||||
assert_eq!(names, vec!["phone".to_string(), "tablet".to_string()]);
|
||||
assert!(
|
||||
std::fs::read_dir(manager.pending_enrollments_dir())
|
||||
.unwrap()
|
||||
.next()
|
||||
.is_none(),
|
||||
"the spool entry is consumed"
|
||||
);
|
||||
|
||||
let again = router
|
||||
.oneshot(request("/probe", Some(&bearer)))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(again.status(), StatusCode::OK, "now an ordinary token");
|
||||
let stranger = guarded_router(manager)
|
||||
.oneshot(request("/probe", Some("Bearer nope")))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(stranger.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user