Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion crates/uv-auth/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,16 @@ impl Indexes {
}

fn find_prefix_index(&self, url: &Url) -> Option<&Index> {
self.0.iter().find(|&index| index.is_prefix_for(url))
self.0
.iter()
.filter(|&index| index.is_prefix_for(url))
.max_by_key(|index| {
(
index.root_url.path().trim_end_matches('/').len(),
matches!(index.auth_policy, AuthPolicy::Never),
matches!(index.auth_policy, AuthPolicy::Always),
)
})
}
}

Expand Down Expand Up @@ -167,4 +176,43 @@ mod tests {
);
}
}

#[test]
fn test_index_path_prefix_prefers_most_specific_root() {
let indexes = Indexes::from_indexes([
index("https://example.com", AuthPolicy::Always),
index("https://example.com/public", AuthPolicy::Never),
]);
let url = Url::parse("https://example.com/public/simple/anyio").unwrap();

assert_eq!(indexes.auth_policy_for(&url), AuthPolicy::Never);
assert_eq!(
indexes.index_for(&url).map(|index| index.root_url.path()),
Some("/public")
);
}

#[test]
fn test_index_path_prefix_prefers_never_for_equal_roots() {
let indexes = Indexes::from_indexes([
index("https://example.com/public", AuthPolicy::Auto),
index("https://example.com/public", AuthPolicy::Always),
index("https://example.com/public", AuthPolicy::Never),
]);
let url = Url::parse("https://example.com/public/simple/anyio").unwrap();

assert_eq!(indexes.auth_policy_for(&url), AuthPolicy::Never);
}

#[test]
fn test_index_path_prefix_prefers_never_for_trailing_slash_roots() {
let indexes = Indexes::from_indexes([
index("https://example.com/public/", AuthPolicy::Auto),
index("https://example.com/public/", AuthPolicy::Always),
index("https://example.com/public", AuthPolicy::Never),
]);
let url = Url::parse("https://example.com/public/simple/anyio").unwrap();

assert_eq!(indexes.auth_policy_for(&url), AuthPolicy::Never);
}
}
136 changes: 126 additions & 10 deletions crates/uv-auth/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,12 @@ impl Middleware for AuthMiddleware {
let auth_policy = self.indexes.auth_policy_for(request.url());
trace!("Handling request for {url} with authentication policy {auth_policy}");

if matches!(auth_policy, AuthPolicy::Never) && request_credentials.is_some() {
return Err(Error::Middleware(format_err!(
"Credentials were provided for {url}, but authentication is disabled for this index"
)));
}

let credentials: Option<Arc<Authentication>> = if matches!(auth_policy, AuthPolicy::Never) {
None
} else {
Expand Down Expand Up @@ -2454,19 +2460,15 @@ mod tests {
)
.build();

let mut url = base_url.clone();
let mut url = base_url.join("foo")?;
url.set_username(username).unwrap();
url.set_password(Some(password)).unwrap();

assert_eq!(
client
.get(format!("{}/foo", server.uri()))
.send()
.await?
.status(),
401,
"Requests should not be completed if credentials are required"
);
assert!(matches!(
client.get(url).send().await,
Err(reqwest_middleware::Error::Middleware(_))
));
assert!(server.received_requests().await.unwrap().is_empty());

Ok(())
}
Expand Down Expand Up @@ -2508,6 +2510,120 @@ mod tests {
Ok(())
}

/// A nested index with `authenticate = never` must not receive credentials cached for a
/// broader index root.
#[test(tokio::test)]
async fn test_auth_policy_never_with_overlapping_index_roots() -> Result<(), Error> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path_regex("/public/.*"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;

let broad_url = DisplaySafeUrl::parse(&server.uri())?;
let nested_url = DisplaySafeUrl::parse(&format!("{}/public", server.uri()))?;
let indexes = Indexes::from_indexes([
Index {
url: broad_url.clone(),
root_url: broad_url.clone(),
auth_policy: AuthPolicy::Auto,
},
Index {
url: nested_url.clone(),
root_url: nested_url,
auth_policy: AuthPolicy::Never,
},
]);
let cache = CredentialsCache::new();
cache.store_credentials(
&broad_url,
Credentials::Basic {
username: Username::new(Some("user".to_string())),
password: Some(Password::new("password".to_string())),
},
);
let client = test_client_builder()
.with(
AuthMiddleware::new()
.with_cache(cache)
.with_indexes(indexes),
)
.build();

assert_eq!(
client
.get(format!("{}/public/simple/anyio", server.uri()))
.send()
.await?
.status(),
200
);

let requests = server.received_requests().await.unwrap();
assert_eq!(requests.len(), 1);
assert!(!requests[0].headers.contains_key("authorization"));

Ok(())
}

/// Equivalent index roots with and without a trailing slash must not let a broader policy
/// send cached credentials when the other root disables authentication.
#[test(tokio::test)]
async fn test_auth_policy_never_with_trailing_slash_index_roots() -> Result<(), Error> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path_regex("/public/.*"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;

let authenticated_url = DisplaySafeUrl::parse(&format!("{}/public/", server.uri()))?;
let unauthenticated_url = DisplaySafeUrl::parse(&format!("{}/public", server.uri()))?;
let indexes = Indexes::from_indexes([
Index {
url: authenticated_url.clone(),
root_url: authenticated_url.clone(),
auth_policy: AuthPolicy::Auto,
},
Index {
url: unauthenticated_url.clone(),
root_url: unauthenticated_url,
auth_policy: AuthPolicy::Never,
},
]);
let cache = CredentialsCache::new();
cache.store_credentials(
&authenticated_url,
Credentials::Basic {
username: Username::new(Some("user".to_string())),
password: Some(Password::new("password".to_string())),
},
);
let client = test_client_builder()
.with(
AuthMiddleware::new()
.with_cache(cache)
.with_indexes(indexes),
)
.build();

assert_eq!(
client
.get(format!("{}/public/simple/anyio", server.uri()))
.send()
.await?
.status(),
200
);

let requests = server.received_requests().await.unwrap();
assert_eq!(requests.len(), 1);
assert!(!requests[0].headers.contains_key("authorization"));

Ok(())
}

#[test]
fn test_tracing_url() {
// No credentials
Expand Down
Loading