Merge pull request #422 from readur/fix/update-rust-versions-take1

feat(dev): update rust deps major versions
This commit is contained in:
Jon Fuller
2025-12-19 21:55:44 -08:00
committed by GitHub
7 changed files with 462 additions and 263 deletions

600
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,7 @@ raw-cpuid = { version = "11", optional = true }
reqwest = { version = "0.12", features = ["json", "multipart"] }
quick-xml = { version = "0.37", features = ["serialize"] }
urlencoding = "2.1"
oauth2 = "4.4"
oauth2 = "5"
url = "2.4"
dotenvy = "0.15"
hostname = "0.4"
@@ -65,11 +65,11 @@ aws-credential-types = { version = "1.2", optional = true }
aws-types = { version = "1.3", optional = true }
sha2 = "0.10"
utoipa-swagger-ui = { version = "9", features = ["axum"] }
testcontainers = { version = "0.24", optional = true }
testcontainers-modules = { version = "0.12", features = ["postgres"], optional = true }
testcontainers = { version = "0.26", optional = true }
testcontainers-modules = { version = "0.14", features = ["postgres"], optional = true }
# Office document support - now using XML extraction only
zip = "0.6" # Still needed for other archive handling
rand = "0.8"
zip = "7" # Still needed for other archive handling
rand = "0.9"
[features]
default = ["ocr", "s3"]
@@ -83,10 +83,10 @@ tempfile = "3"
wiremock = "0.6"
tokio-test = "0.4"
futures = "0.3"
rand = "0.8"
rand = "0.9"
# Database testing dependencies
testcontainers = "0.24"
testcontainers-modules = { version = "0.12", features = ["postgres"] }
testcontainers = "0.26"
testcontainers-modules = { version = "0.14", features = ["postgres"] }
# Dependencies for creating proper test Office documents
rust_xlsxwriter = "0.92" # For creating proper XLSX test files
# WebDAV server for realistic stress testing

View File

@@ -1,7 +1,8 @@
use anyhow::{anyhow, Result};
use oauth2::{
basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId,
ClientSecret, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, TokenResponse, TokenUrl,
basic::BasicClient, AuthUrl, AuthorizationCode, ClientId,
ClientSecret, CsrfToken, PkceCodeChallenge, PkceCodeVerifier,
RedirectUrl, Scope, TokenResponse, TokenUrl,
};
use reqwest::Client;
use serde::{Deserialize, Serialize};
@@ -33,7 +34,14 @@ type PkceStore = Mutex<HashMap<String, (PkceCodeVerifier, Instant)>>;
#[derive(Debug)]
pub struct OidcClient {
oauth_client: BasicClient,
// Store configuration instead of the oauth2 client to avoid complex typestate types.
// The oauth2 5.0 crate uses a typestate pattern that makes storing a configured client
// in a struct unwieldy. Instead, we store the config and build the client on-demand.
// This is cheap: just struct construction with pre-validated URLs (no network calls).
// The expensive OIDC discovery happens once in `new()`.
client_id: String,
client_secret: Option<String>,
redirect_uri: String,
discovery: OidcDiscovery,
http_client: Client,
is_public_client: bool,
@@ -49,11 +57,12 @@ impl OidcClient {
let client_id = config
.oidc_client_id
.as_ref()
.ok_or_else(|| anyhow!("OIDC client ID not configured"))?;
.ok_or_else(|| anyhow!("OIDC client ID not configured"))?
.clone();
// Client secret is optional - if not provided, this is a public client
let client_secret_opt = config.oidc_client_secret.as_ref();
let is_public_client = client_secret_opt.is_none();
let client_secret = config.oidc_client_secret.clone();
let is_public_client = client_secret.is_none();
let issuer_url = config
.oidc_issuer_url
@@ -62,24 +71,18 @@ impl OidcClient {
let redirect_uri = config
.oidc_redirect_uri
.as_ref()
.ok_or_else(|| anyhow!("OIDC redirect URI not configured"))?;
.ok_or_else(|| anyhow!("OIDC redirect URI not configured"))?
.clone();
let http_client = Client::new();
// Discover OIDC endpoints
let discovery = Self::discover_endpoints(&http_client, issuer_url).await?;
// Create OAuth2 client
let oauth_client = BasicClient::new(
ClientId::new(client_id.clone()),
client_secret_opt.map(|s| ClientSecret::new(s.clone())),
AuthUrl::new(discovery.authorization_endpoint.clone())?,
Some(TokenUrl::new(discovery.token_endpoint.clone())?),
)
.set_redirect_uri(RedirectUrl::new(redirect_uri.clone())?);
Ok(Self {
oauth_client,
client_id,
client_secret,
redirect_uri,
discovery,
http_client,
is_public_client,
@@ -89,7 +92,7 @@ impl OidcClient {
async fn discover_endpoints(client: &Client, issuer_url: &str) -> Result<OidcDiscovery> {
let discovery_url = format!("{}/.well-known/openid-configuration", issuer_url.trim_end_matches('/'));
let response = client
.get(&discovery_url)
.send()
@@ -111,11 +114,21 @@ impl OidcClient {
Ok(discovery)
}
pub fn get_authorization_url(&self) -> (Url, CsrfToken) {
pub fn get_authorization_url(&self) -> Result<(Url, CsrfToken)> {
// Clean up expired PKCE verifiers (older than 10 minutes)
self.cleanup_expired_verifiers();
let mut auth_request = self.oauth_client
// Build OAuth2 client on-demand - this is cheap (just struct construction, no I/O)
let mut oauth_client = BasicClient::new(ClientId::new(self.client_id.clone()))
.set_auth_uri(AuthUrl::new(self.discovery.authorization_endpoint.clone())?)
.set_token_uri(TokenUrl::new(self.discovery.token_endpoint.clone())?)
.set_redirect_uri(RedirectUrl::new(self.redirect_uri.clone())?);
if let Some(secret) = &self.client_secret {
oauth_client = oauth_client.set_client_secret(ClientSecret::new(secret.clone()));
}
let mut auth_request = oauth_client
.authorize_url(CsrfToken::new_random)
.add_scope(Scope::new("openid".to_string()))
.add_scope(Scope::new("email".to_string()))
@@ -134,10 +147,10 @@ impl OidcClient {
csrf_token.secret().clone(),
(pkce_verifier, Instant::now() + Duration::from_secs(600)), // 10 minute expiry
);
(url, csrf_token)
Ok((url, csrf_token))
} else {
// Confidential client - no PKCE needed
auth_request.url()
Ok(auth_request.url())
}
}
@@ -148,8 +161,17 @@ impl OidcClient {
}
pub async fn exchange_code(&self, code: &str, state: Option<&str>) -> Result<String> {
let mut token_request = self
.oauth_client
// Build OAuth2 client on-demand - this is cheap (just struct construction, no I/O)
let mut oauth_client = BasicClient::new(ClientId::new(self.client_id.clone()))
.set_auth_uri(AuthUrl::new(self.discovery.authorization_endpoint.clone())?)
.set_token_uri(TokenUrl::new(self.discovery.token_endpoint.clone())?)
.set_redirect_uri(RedirectUrl::new(self.redirect_uri.clone())?);
if let Some(secret) = &self.client_secret {
oauth_client = oauth_client.set_client_secret(ClientSecret::new(secret.clone()));
}
let mut token_request = oauth_client
.exchange_code(AuthorizationCode::new(code.to_string()));
// For public clients, retrieve and use the PKCE verifier
@@ -166,8 +188,14 @@ impl OidcClient {
}
}
// Create HTTP client for token exchange with redirect disabled for SSRF protection
let oauth_http_client = Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?;
let token_result = token_request
.request_async(async_http_client)
.request_async(&oauth_http_client)
.await
.map_err(|e| anyhow!("Failed to exchange authorization code: {}", e))?;
@@ -206,4 +234,3 @@ pub struct OidcAuthResponse {
pub email: Option<String>,
pub is_new_user: bool,
}

View File

@@ -197,8 +197,10 @@ async fn oidc_login(State(state): State<Arc<AppState>>) -> Result<Redirect, Stat
.as_ref()
.ok_or(StatusCode::BAD_REQUEST)?;
let (auth_url, _csrf_token) = oidc_client.get_authorization_url();
let (auth_url, _csrf_token) = oidc_client
.get_authorization_url()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Redirect::to(auth_url.as_str()))
}

View File

@@ -4,7 +4,7 @@ use readur::services::file_service::FileService;
use std::fs;
use std::io::Write;
use tempfile::TempDir;
use zip::write::FileOptions;
use zip::write::SimpleFileOptions;
use zip::{ZipWriter, CompressionMethod};
/// Helper function to create a proper DOCX file for testing
@@ -13,7 +13,7 @@ fn create_test_docx(content: &str) -> Vec<u8> {
let mut buffer = Vec::new();
{
let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buffer));
let options = FileOptions::default().compression_method(CompressionMethod::Deflated);
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
// Add [Content_Types].xml - More comprehensive structure
zip.start_file("[Content_Types].xml", options).unwrap();

View File

@@ -30,7 +30,7 @@ impl OfficeTestDocuments {
let mut zip = zip::ZipWriter::new(file);
// Add [Content_Types].xml
zip.start_file("[Content_Types].xml", zip::write::FileOptions::default())?;
zip.start_file("[Content_Types].xml", zip::write::SimpleFileOptions::default())?;
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
@@ -39,14 +39,14 @@ impl OfficeTestDocuments {
</Types>"#)?;
// Add _rels/.rels
zip.start_file("_rels/.rels", zip::write::FileOptions::default())?;
zip.start_file("_rels/.rels", zip::write::SimpleFileOptions::default())?;
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>"#)?;
// Add word/document.xml with the actual content
zip.start_file("word/document.xml", zip::write::FileOptions::default())?;
zip.start_file("word/document.xml", zip::write::SimpleFileOptions::default())?;
let document_xml = format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
@@ -72,7 +72,7 @@ impl OfficeTestDocuments {
let mut zip = zip::ZipWriter::new(file);
// Add [Content_Types].xml with shared strings support
zip.start_file("[Content_Types].xml", zip::write::FileOptions::default())?;
zip.start_file("[Content_Types].xml", zip::write::SimpleFileOptions::default())?;
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
@@ -83,14 +83,14 @@ impl OfficeTestDocuments {
</Types>"#)?;
// Add _rels/.rels
zip.start_file("_rels/.rels", zip::write::FileOptions::default())?;
zip.start_file("_rels/.rels", zip::write::SimpleFileOptions::default())?;
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>"#)?;
// Add xl/workbook.xml
zip.start_file("xl/workbook.xml", zip::write::FileOptions::default())?;
zip.start_file("xl/workbook.xml", zip::write::SimpleFileOptions::default())?;
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets>
@@ -99,7 +99,7 @@ impl OfficeTestDocuments {
</workbook>"#)?;
// Add xl/_rels/workbook.xml.rels with shared strings relationship
zip.start_file("xl/_rels/workbook.xml.rels", zip::write::FileOptions::default())?;
zip.start_file("xl/_rels/workbook.xml.rels", zip::write::SimpleFileOptions::default())?;
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
@@ -107,7 +107,7 @@ impl OfficeTestDocuments {
</Relationships>"#)?;
// Add xl/sharedStrings.xml with the text content
zip.start_file("xl/sharedStrings.xml", zip::write::FileOptions::default())?;
zip.start_file("xl/sharedStrings.xml", zip::write::SimpleFileOptions::default())?;
let mut shared_strings_xml = String::from(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="{count}" uniqueCount="{count}">"#);
shared_strings_xml = shared_strings_xml.replace("{count}", &content.len().to_string());
@@ -122,7 +122,7 @@ impl OfficeTestDocuments {
zip.write_all(shared_strings_xml.as_bytes())?;
// Add xl/worksheets/sheet1.xml with references to shared strings
zip.start_file("xl/worksheets/sheet1.xml", zip::write::FileOptions::default())?;
zip.start_file("xl/worksheets/sheet1.xml", zip::write::SimpleFileOptions::default())?;
let mut worksheet_xml = String::from(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>"#);

View File

@@ -81,7 +81,7 @@ async fn test_get_authorization_url() {
let config = create_test_config_with_oidc(&mock_server.uri());
let oidc_client = OidcClient::new(&config).await.unwrap();
let (auth_url, csrf_token) = oidc_client.get_authorization_url();
let (auth_url, csrf_token) = oidc_client.get_authorization_url().unwrap();
assert!(auth_url.to_string().contains("/auth"));
assert!(auth_url.to_string().contains("client_id=test-client-id"));