Skip to content

Commit fbd5b9e

Browse files
committed
AuthCheck: Add with_endpoint_scope() and for_crate() options
1 parent 94ca5e9 commit fbd5b9e

File tree

1 file changed

+176
-5
lines changed

1 file changed

+176
-5
lines changed

src/auth.rs

Lines changed: 176 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::controllers;
22
use crate::db::RequestTransaction;
33
use crate::middleware::log_request;
4+
use crate::models::token::{CrateScope, EndpointScope};
45
use crate::models::{ApiToken, User};
56
use crate::util::errors::{
67
account_locked, forbidden, internal, AppError, AppResult, InsecurelyGeneratedTokenRevoked,
@@ -13,17 +14,43 @@ use http::header;
1314
#[derive(Debug, Clone)]
1415
pub struct AuthCheck {
1516
allow_token: bool,
17+
endpoint_scope: Option<EndpointScope>,
18+
crate_name: Option<String>,
1619
}
1720

1821
impl AuthCheck {
1922
#[must_use]
2023
pub fn default() -> Self {
21-
Self { allow_token: true }
24+
Self {
25+
allow_token: true,
26+
endpoint_scope: None,
27+
crate_name: None,
28+
}
2229
}
2330

2431
#[must_use]
2532
pub fn only_cookie() -> Self {
26-
Self { allow_token: false }
33+
Self {
34+
allow_token: false,
35+
endpoint_scope: None,
36+
crate_name: None,
37+
}
38+
}
39+
40+
pub fn with_endpoint_scope(&self, endpoint_scope: EndpointScope) -> Self {
41+
Self {
42+
allow_token: self.allow_token,
43+
endpoint_scope: Some(endpoint_scope),
44+
crate_name: self.crate_name.clone(),
45+
}
46+
}
47+
48+
pub fn for_crate(&self, crate_name: &str) -> Self {
49+
Self {
50+
allow_token: self.allow_token,
51+
endpoint_scope: self.endpoint_scope,
52+
crate_name: Some(crate_name.to_string()),
53+
}
2754
}
2855

2956
pub fn check(&self, request: &dyn RequestExt) -> AppResult<AuthenticatedUser> {
@@ -47,13 +74,57 @@ impl AuthCheck {
4774
log_request::add_custom_metadata("tokenid", id);
4875
}
4976

50-
if !self.allow_token && auth.token.is_some() {
51-
let error_message = "API Token authentication was explicitly disallowed for this API";
52-
return Err(internal(error_message).chain(forbidden()));
77+
if let Some(ref token) = auth.token {
78+
if !self.allow_token {
79+
let error_message =
80+
"API Token authentication was explicitly disallowed for this API";
81+
return Err(internal(error_message).chain(forbidden()));
82+
}
83+
84+
if !self.endpoint_scope_matches(token.endpoint_scopes.as_ref()) {
85+
let error_message = "Endpoint scope mismatch";
86+
return Err(internal(error_message).chain(forbidden()));
87+
}
88+
89+
if !self.crate_scope_matches(token.crate_scopes.as_ref()) {
90+
let error_message = "Crate scope mismatch";
91+
return Err(internal(error_message).chain(forbidden()));
92+
}
5393
}
5494

5595
Ok(auth)
5696
}
97+
98+
fn endpoint_scope_matches(&self, token_scopes: Option<&Vec<EndpointScope>>) -> bool {
99+
match (&token_scopes, &self.endpoint_scope) {
100+
// The token is a legacy token.
101+
(None, _) => true,
102+
103+
// The token is NOT a legacy token, and the endpoint only allows legacy tokens.
104+
(Some(_), None) => false,
105+
106+
// The token is NOT a legacy token, and the endpoint allows a certain endpoint scope or a legacy token.
107+
(Some(token_scopes), Some(endpoint_scope)) => token_scopes.contains(endpoint_scope),
108+
}
109+
}
110+
111+
fn crate_scope_matches(&self, token_scopes: Option<&Vec<CrateScope>>) -> bool {
112+
match (&token_scopes, &self.crate_name) {
113+
// The token is a legacy token.
114+
(None, _) => true,
115+
116+
// The token does not have any crate scopes.
117+
(Some(token_scopes), _) if token_scopes.is_empty() => true,
118+
119+
// The token has crate scopes, but the endpoint does not deal with crates.
120+
(Some(_), None) => false,
121+
122+
// The token is NOT a legacy token, and the endpoint allows a certain endpoint scope or a legacy token.
123+
(Some(token_scopes), Some(crate_name)) => token_scopes
124+
.iter()
125+
.any(|token_scope| token_scope.matches(crate_name)),
126+
}
127+
}
57128
}
58129

59130
#[derive(Debug)]
@@ -120,3 +191,103 @@ fn authenticate_user(req: &dyn RequestExt) -> AppResult<AuthenticatedUser> {
120191
// Unable to authenticate the user
121192
return Err(internal("no cookie session or auth header found").chain(forbidden()));
122193
}
194+
195+
#[cfg(test)]
196+
mod tests {
197+
use super::*;
198+
199+
fn cs(scope: &str) -> CrateScope {
200+
CrateScope::try_from(scope).unwrap()
201+
}
202+
203+
#[test]
204+
fn regular_endpoint() {
205+
let auth_check = AuthCheck::default();
206+
207+
assert!(auth_check.endpoint_scope_matches(None));
208+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::PublishNew])));
209+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::PublishUpdate])));
210+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::Yank])));
211+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::ChangeOwners])));
212+
213+
assert!(auth_check.crate_scope_matches(None));
214+
assert!(!auth_check.crate_scope_matches(Some(&vec![cs("tokio-console")])));
215+
assert!(!auth_check.crate_scope_matches(Some(&vec![cs("tokio-*")])));
216+
}
217+
218+
#[test]
219+
fn publish_new_endpoint() {
220+
let auth_check = AuthCheck::default()
221+
.with_endpoint_scope(EndpointScope::PublishNew)
222+
.for_crate("tokio-console");
223+
224+
assert!(auth_check.endpoint_scope_matches(None));
225+
assert!(auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::PublishNew])));
226+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::PublishUpdate])));
227+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::Yank])));
228+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::ChangeOwners])));
229+
230+
assert!(auth_check.crate_scope_matches(None));
231+
assert!(auth_check.crate_scope_matches(Some(&vec![cs("tokio-console")])));
232+
assert!(auth_check.crate_scope_matches(Some(&vec![cs("tokio-*")])));
233+
assert!(!auth_check.crate_scope_matches(Some(&vec![cs("anyhow")])));
234+
assert!(!auth_check.crate_scope_matches(Some(&vec![cs("actix-*")])));
235+
}
236+
237+
#[test]
238+
fn publish_update_endpoint() {
239+
let auth_check = AuthCheck::default()
240+
.with_endpoint_scope(EndpointScope::PublishUpdate)
241+
.for_crate("tokio-console");
242+
243+
assert!(auth_check.endpoint_scope_matches(None));
244+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::PublishNew])));
245+
assert!(auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::PublishUpdate])));
246+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::Yank])));
247+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::ChangeOwners])));
248+
249+
assert!(auth_check.crate_scope_matches(None));
250+
assert!(auth_check.crate_scope_matches(Some(&vec![cs("tokio-console")])));
251+
assert!(auth_check.crate_scope_matches(Some(&vec![cs("tokio-*")])));
252+
assert!(!auth_check.crate_scope_matches(Some(&vec![cs("anyhow")])));
253+
assert!(!auth_check.crate_scope_matches(Some(&vec![cs("actix-*")])));
254+
}
255+
256+
#[test]
257+
fn yank_endpoint() {
258+
let auth_check = AuthCheck::default()
259+
.with_endpoint_scope(EndpointScope::Yank)
260+
.for_crate("tokio-console");
261+
262+
assert!(auth_check.endpoint_scope_matches(None));
263+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::PublishNew])));
264+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::PublishUpdate])));
265+
assert!(auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::Yank])));
266+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::ChangeOwners])));
267+
268+
assert!(auth_check.crate_scope_matches(None));
269+
assert!(auth_check.crate_scope_matches(Some(&vec![cs("tokio-console")])));
270+
assert!(auth_check.crate_scope_matches(Some(&vec![cs("tokio-*")])));
271+
assert!(!auth_check.crate_scope_matches(Some(&vec![cs("anyhow")])));
272+
assert!(!auth_check.crate_scope_matches(Some(&vec![cs("actix-*")])));
273+
}
274+
275+
#[test]
276+
fn owner_change_endpoint() {
277+
let auth_check = AuthCheck::default()
278+
.with_endpoint_scope(EndpointScope::ChangeOwners)
279+
.for_crate("tokio-console");
280+
281+
assert!(auth_check.endpoint_scope_matches(None));
282+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::PublishNew])));
283+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::PublishUpdate])));
284+
assert!(!auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::Yank])));
285+
assert!(auth_check.endpoint_scope_matches(Some(&vec![EndpointScope::ChangeOwners])));
286+
287+
assert!(auth_check.crate_scope_matches(None));
288+
assert!(auth_check.crate_scope_matches(Some(&vec![cs("tokio-console")])));
289+
assert!(auth_check.crate_scope_matches(Some(&vec![cs("tokio-*")])));
290+
assert!(!auth_check.crate_scope_matches(Some(&vec![cs("anyhow")])));
291+
assert!(!auth_check.crate_scope_matches(Some(&vec![cs("actix-*")])));
292+
}
293+
}

0 commit comments

Comments
 (0)