review: post-adoption pass over the alkcall-0.10 wiring pair (0766f4b, ad63507)

Three inline findings from the session review of ADR-072/ADR-073:

- fix(auth): Basic door parsing no longer requires UTF-8 — the decoded
  password bytes pass through to resolve_from_token byte-faithfully, so
  RFC 7617 legacy ISO-8859-1 credentials resolve instead of parsing to
  None. ADR-073 already specced raw-bytes passthrough ("decoded and fed
  as the AuthToken raw bytes"); the String::from_utf8 round-trip
  contradicted it.
- test: pin the with_ws_max_sessions fix against the shape it fixed —
  a builder call after with_ws_max_sessions (the pre-router_state()
  builder silently reverted the cap).
- test: pin that ownership wiring survives a later HttpAdapter builder
  rebuild at the HTTP surface.

Verified: cargo test (451), --all-features (530), clippy --all-targets
-D warnings, fmt --check, fuzz corpus replay 21/21, cargo doc.
This commit is contained in:
glm-5.3-flash committed 2026-10-17 16:25:07 +00:00
1 parent 0766f4bebe
commit dfa0216186
4 files changed
+76 -17

No files matched your search

+8 -1
View File
@@ -61,7 +61,14 @@ this crate adheres to [Semantic Versioning](https://semver.org/).
rebuilt the router with the pre-cap-change semaphore, silently
reverting the cap. Builder state is now assembled in one shared
`router_state()` helper, so every `with_*` rebuild sees the current
configuration.
configuration. Pinned by the WS session-cap integration test running
with a builder call after `with_ws_max_sessions`, and by an
ownership-wiring-survives-rebuild test at the HTTP surface.
- Door-surface Basic parsing no longer requires the password field to
be UTF-8: the decoded password bytes reach
`IdentityProvider::resolve_from_token()` byte-faithfully, so RFC 7617
legacy ISO-8859-1 credentials (e.g. a latin-1 user password) that a
provider resolves still authenticate instead of parsing to `None`.
Minor bump 0.7.0 → 0.8.0 per the dep-wave convention (0.4.0 rode
alkcall 0.7.0, 0.5.0 rode 0.8.0, 0.7.0 rode 0.9.0): the re-exported
+26
View File
@@ -1066,6 +1066,32 @@ mod tests {
);
}
#[tokio::test]
async fn ownership_fallback_wiring_survives_a_later_builder_rebuild() {
let provider: Arc<dyn IdentityProvider> =
Arc::new(StaticIdentityProvider::new().with_token(
"tok-a",
identity_with_resources("token-carrier", "blob-namespace", Some(vec!["read"])),
));
let adapter = crate::server::HttpAdapter::new(provider, registry_with_namespace_gated_op())
.with_static_ownership_fallback()
.with_decoy(crate::server::DecoyConfig::NotFound);
let (status, body) = post_call(
adapter.router().clone(),
"tok-a",
"ns/stat",
json!({ "namespace": "ns-1" }),
)
.await;
assert_eq!(
status,
StatusCode::OK,
"the wiring must survive the with_decoy state rebuild — the pre-router_state() \
builder shape silently dropped values set by earlier calls: {body}"
);
}
fn registry_with_echo() -> Arc<OperationRegistry> {
let registry = OperationRegistry::new();
registry
+35 -13
View File
@@ -100,7 +100,7 @@ pub fn extract_bearer_identity(
identity_provider: &dyn IdentityProvider,
) -> Option<Identity> {
resolve_token_str(
bearer_token(request.headers().get(AUTHORIZATION)),
bearer_token(request.headers().get(AUTHORIZATION)).map(String::into_bytes),
identity_provider,
)
}
@@ -110,7 +110,10 @@ pub fn extract_bearer_identity(
/// password field (the token; the Git-PAT shape — user, `x-access-token`
/// and similar are ignored by resolution) through the same
/// `IdentityProvider::resolve_from_token()` backend as a Bearer token.
/// A colon-less value is treated as a bare password. Unparseable or
/// The password bytes are passed to the backend with no UTF-8
/// round-trip, so RFC 7617 legacy ISO-8859-1 credentials reach the
/// provider byte-faithfully. A colon-less value is treated as a bare
/// password. Unparseable or
/// unresolvable credentials resolve to `None` — the same no-token
/// fallback as [`extract_bearer_identity`].
pub fn extract_door_identity(
@@ -119,7 +122,7 @@ pub fn extract_door_identity(
) -> Option<Identity> {
let header = request.headers().get(AUTHORIZATION)?;
let credential = match bearer_token(Some(header)) {
Some(token) => token,
Some(token) => token.into_bytes(),
None => basic_password(header)?,
};
resolve_token_str(Some(credential), identity_provider)
@@ -133,28 +136,25 @@ fn bearer_token(header: Option<&http::HeaderValue>) -> Option<String> {
.map(str::to_owned)
}
fn basic_password(header: &http::HeaderValue) -> Option<String> {
fn basic_password(header: &http::HeaderValue) -> Option<Vec<u8>> {
use base64::Engine as _;
let encoded = header.to_str().ok()?.strip_prefix("Basic ")?;
let decoded = base64::engine::general_purpose::STANDARD
.decode(encoded.trim())
.ok()?;
let decoded = String::from_utf8(decoded).ok()?;
let password = match decoded.split_once(':') {
Some((_user, password)) => password,
None => &decoded,
let password = match decoded.iter().position(|&byte| byte == b':') {
Some(colon) => &decoded[colon + 1..],
None => &decoded[..],
};
Some(password.to_owned())
Some(password.to_vec())
}
fn resolve_token_str(
token_str: Option<String>,
token_bytes: Option<Vec<u8>>,
identity_provider: &dyn IdentityProvider,
) -> Option<Identity> {
let token = AuthToken {
raw: token_str?.into_bytes(),
};
let token = AuthToken { raw: token_bytes? };
identity_provider.resolve_from_token(&token)
}
@@ -287,6 +287,28 @@ mod tests {
assert!(crate::server::auth::extract_door_identity(&req, &idp).is_some());
}
#[test]
fn door_extract_passes_non_utf8_basic_password_bytes_through() {
// RFC 7617 legacy ISO-8859-1 credential: "passwörd" (ö = 0xF6 in
// latin-1) — invalid UTF-8, must still reach `resolve_from_token`
// as the raw bytes rather than parsing to `None`.
struct ByteProvider;
impl IdentityProvider for ByteProvider {
fn resolve_from_fingerprint(&self, _: &str) -> Option<Identity> {
None
}
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity> {
(token.raw == b"passw\xf6rd"[..]).then(sample_identity)
}
}
let req = request_with_authorization(Some("Basic dXNlcjpwYXNzd/ZyZA=="));
let identity = crate::server::auth::extract_door_identity(&req, &ByteProvider);
assert!(
identity.is_some(),
"a latin-1 password resolves byte-faithfully"
);
}
#[test]
fn door_extract_treats_colonless_basic_as_a_bare_password() {
struct BareProvider;
+7 -3
View File
@@ -806,9 +806,13 @@ async fn session_cap_rejects_over_limit_with_503_and_frees_slots_on_end() {
}
// The adapter's built-in surface: WS route + bearer middleware, the
// whole deployment shape the cap knob configures.
let adapter =
HttpAdapter::new(Arc::new(StaticTok), Arc::clone(&registry)).with_ws_max_sessions(1);
// whole deployment shape the cap knob configures. A builder call
// after `with_ws_max_sessions` must not revert the cap (the
// pre-`router_state()` builder shape rebuilt the router from the
// un-stored fresh semaphore).
let adapter = HttpAdapter::new(Arc::new(StaticTok), Arc::clone(&registry))
.with_ws_max_sessions(1)
.with_decoy(alkhttp::server::DecoyConfig::default());
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = format!("ws://{}", listener.local_addr().unwrap());
let app: axum::Router = adapter.router().clone();