Browse Source

Pass provider 5xx through and test against a mock provider

Add Error::is_server_error, return the provider's own 5xx status from
user_info, and replace the injected-error tests with tests that drive
the real client against a local mock provider.
pull/7785/head
Justin Willhite 2 weeks ago
parent
commit
d1a9822f7d
  1. 4
      src/api/identity.rs
  2. 5
      src/error.rs
  3. 139
      src/sso_client.rs

4
src/api/identity.rs

@ -166,8 +166,8 @@ async fn refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonR
// --- // ---
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await; // let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
match auth::refresh_tokens(ip, &refresh_token, data.client_id, conn).await { match auth::refresh_tokens(ip, &refresh_token, data.client_id, conn).await {
// Clients log out on 400 invalid_grant, so a provider outage is returned as-is (503). // Clients log out on 400 invalid_grant, so a provider outage is returned as-is (5xx).
Err(err) if err.get_code() == 503 => Err(err), Err(err) if err.is_server_error() => Err(err),
Err(err) => { Err(err) => {
err_json!( err_json!(
json!({"error": "invalid_grant"}), json!({"error": "invalid_grant"}),

5
src/error.rs

@ -159,11 +159,14 @@ impl Error {
self self
} }
#[must_use]
pub const fn get_code(&self) -> u16 { pub const fn get_code(&self) -> u16 {
self.code self.code
} }
pub const fn is_server_error(&self) -> bool {
matches!(self.code, 500..=599)
}
#[must_use] #[must_use]
pub fn with_event(mut self, event: ErrorEvent) -> Self { pub fn with_event(mut self, event: ErrorEvent) -> Self {
self.event = Some(event); self.event = Some(event);

139
src/sso_client.rs

@ -376,59 +376,142 @@ where
{ {
match err { match err {
UserInfoError::Request(_) => 503, UserInfoError::Request(_) => 503,
UserInfoError::Response(status, ..) if status.is_server_error() => 503, UserInfoError::Response(status, ..) if status.is_server_error() => status.as_u16(),
_ => 400, _ => 400,
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{
io::{Read, Write},
net::{TcpListener, TcpStream},
};
use openidconnect::{AuthUrl, IssuerUrl, JsonWebKeySet, TokenUrl, UserInfoUrl};
use super::*; use super::*;
type TokenErr = RequestTokenError<HttpClientError<reqwest::Error>, StandardErrorResponse<CoreErrorResponseType>>; // Answers every request with `response`, after reading the whole request.
fn mock_provider(response: &'static str) -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}", listener.local_addr().unwrap());
std::thread::spawn(move || {
for mut stream in listener.incoming().flatten() {
read_request(&mut stream);
stream.write_all(response.as_bytes()).ok();
}
});
base
}
fn read_request(stream: &mut TcpStream) {
stream.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut data = Vec::new();
let mut buf = [0u8; 1024];
loop {
let n = stream.read(&mut buf).unwrap_or(0);
if n == 0 {
return;
}
data.extend_from_slice(&buf[..n]);
if let Some(end) = data.windows(4).position(|w| w == b"\r\n\r\n") {
let headers = String::from_utf8_lossy(&data[..end]).to_ascii_lowercase();
let length = headers
.lines()
.find_map(|line| line.strip_prefix("content-length:"))
.and_then(|value| value.trim().parse().ok())
.unwrap_or(0);
if data.len() >= end + 4 + length {
return;
}
}
}
}
fn unreachable_provider() -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
format!("http://{}", listener.local_addr().unwrap())
}
fn test_client(base: &str) -> Client {
// main() installs this at startup. Err means another test already installed it.
rustls::crypto::ring::default_provider().install_default().ok();
let core_client = CoreClient::new(
ClientId::new("vaultwarden".to_owned()),
IssuerUrl::new(base.to_owned()).unwrap(),
JsonWebKeySet::new(Vec::new()),
)
.set_client_secret(ClientSecret::new("secret".to_owned()))
.set_auth_uri(AuthUrl::new(format!("{base}/auth")).unwrap())
.set_token_uri(TokenUrl::new(format!("{base}/token")).unwrap())
.set_user_info_url(UserInfoUrl::new(format!("{base}/userinfo")).unwrap());
Client {
http_client: OidcHttpClient {
client: reqwest::Client::new(),
},
core_client,
}
}
fn run<F: Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(future)
}
fn refresh_status(base: &str) -> u16 {
let result = run(test_client(base).exchange_refresh_token_impl("refresh".to_owned()));
result.map_or_else(|(code, _)| code, |_| 200)
}
fn user_info_status(base: &str) -> u16 {
let result = run(test_client(base).user_info(AccessToken::new("access".to_owned())));
result.map_or_else(|err| err.get_code(), |_| 200)
}
#[test] #[test]
fn test_refresh_error_status_invalid_grant() { fn test_refresh_invalid_grant() {
let err = TokenErr::ServerResponse(StandardErrorResponse::new(CoreErrorResponseType::InvalidGrant, None, None)); let base = mock_provider(
assert_eq!(refresh_error_status(&err), 400); "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: 25\r\nConnection: close\r\n\r\n{\"error\":\"invalid_grant\"}",
);
assert_eq!(refresh_status(&base), 400);
} }
#[test] #[test]
fn test_refresh_error_status_temporarily_unavailable() { fn test_refresh_temporarily_unavailable() {
let err = TokenErr::ServerResponse(StandardErrorResponse::new( let base = mock_provider(
CoreErrorResponseType::Extension("temporarily_unavailable".to_owned()), "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: 35\r\nConnection: close\r\n\r\n{\"error\":\"temporarily_unavailable\"}",
None, );
None, assert_eq!(refresh_status(&base), 503);
));
assert_eq!(refresh_error_status(&err), 503);
} }
#[test] #[test]
fn test_refresh_error_status_other() { fn test_refresh_provider_error_page() {
let err = TokenErr::Other("unexpected response from the token endpoint".to_owned()); let base = mock_provider(
assert_eq!(refresh_error_status(&err), 503); "HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/html\r\nContent-Length: 11\r\nConnection: close\r\n\r\nBad Gateway",
);
assert_eq!(refresh_status(&base), 503);
} }
#[test] #[test]
fn test_refresh_error_status_request() { fn test_refresh_provider_unreachable() {
let err = TokenErr::Request(HttpClientError::Io(std::io::Error::new( assert_eq!(refresh_status(&unreachable_provider()), 503);
std::io::ErrorKind::ConnectionRefused,
"connection refused",
)));
assert_eq!(refresh_error_status(&err), 503);
} }
type UserInfoErr = UserInfoError<HttpClientError<reqwest::Error>>; #[test]
fn test_user_info_invalid_token() {
let base = mock_provider("HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
assert_eq!(user_info_status(&base), 400);
}
#[test] #[test]
fn test_user_info_error_status_unauthorized() { fn test_user_info_provider_unavailable() {
let err = UserInfoErr::Response(http::StatusCode::UNAUTHORIZED, Vec::new(), "invalid_token".to_owned()); let base = mock_provider("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
assert_eq!(user_info_error_status(&err), 400); assert_eq!(user_info_status(&base), 502);
} }
#[test] #[test]
fn test_user_info_error_status_unavailable() { fn test_user_info_provider_unreachable() {
let err = UserInfoErr::Response(http::StatusCode::SERVICE_UNAVAILABLE, Vec::new(), "down".to_owned()); assert_eq!(user_info_status(&unreachable_provider()), 503);
assert_eq!(user_info_error_status(&err), 503);
} }
} }

Loading…
Cancel
Save