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:
1 parent
0766f4bebe
commit
dfa0216186
4 files changed
+76
-17
No files matched your search
+8
-1
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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(®istry)).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(®istry))
|
||||
.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();
|
||||
|
||||
Reference in new issue
Block a user