Skip to content

Commit bb2530e

Browse files
authored
Merge pull request #5571 from Turbo87/auth-check-scopes
AuthCheck: Add `with_endpoint_scope()` and `for_crate()` options
2 parents 80b4e1f + a8ff67d commit bb2530e

File tree

2 files changed

+204
-32
lines changed

2 files changed

+204
-32
lines changed

src/auth.rs

Lines changed: 184 additions & 12 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,19 +74,63 @@ impl AuthCheck {
4774
log_request::add_custom_metadata("tokenid", id);
4875
}
4976

50-
if !self.allow_token && auth.token_id.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)]
60131
pub struct AuthenticatedUser {
61132
user: User,
62-
token_id: Option<i32>,
133+
token: Option<ApiToken>,
63134
}
64135

65136
impl AuthenticatedUser {
@@ -68,7 +139,11 @@ impl AuthenticatedUser {
68139
}
69140

70141
pub fn api_token_id(&self) -> Option<i32> {
71-
self.token_id
142+
self.api_token().map(|token| token.id)
143+
}
144+
145+
pub fn api_token(&self) -> Option<&ApiToken> {
146+
self.token.as_ref()
72147
}
73148

74149
pub fn user(self) -> User {
@@ -86,10 +161,7 @@ fn authenticate_user(req: &dyn RequestExt) -> AppResult<AuthenticatedUser> {
86161
let user = User::find(&conn, id)
87162
.map_err(|err| err.chain(internal("user_id from cookie not found in database")))?;
88163

89-
return Ok(AuthenticatedUser {
90-
user,
91-
token_id: None,
92-
});
164+
return Ok(AuthenticatedUser { user, token: None });
93165
}
94166

95167
// Otherwise, look for an `Authorization` header on the request
@@ -112,10 +184,110 @@ fn authenticate_user(req: &dyn RequestExt) -> AppResult<AuthenticatedUser> {
112184

113185
return Ok(AuthenticatedUser {
114186
user,
115-
token_id: Some(token.id),
187+
token: Some(token),
116188
});
117189
}
118190

119191
// Unable to authenticate the user
120192
return Err(internal("no cookie session or auth header found").chain(forbidden()));
121193
}
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+
}

src/tests/owners.rs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -321,11 +321,17 @@ fn owner_change_via_change_owner_token() {
321321
let body = json!({ "owners": [user2.gh_login] });
322322
let body = serde_json::to_vec(&body).unwrap();
323323
let response = token.put::<()>(&url, &body);
324-
assert_eq!(response.status(), StatusCode::OK);
324+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
325325
assert_eq!(
326326
response.into_json(),
327-
json!({ "ok": true, "msg": "user user-2 has been invited to be an owner of crate foo_crate" })
327+
json!({ "errors": [{ "detail": "must be logged in to perform that action" }] })
328328
);
329+
// TODO swap these assertions once token scopes are activated for this endpoint
330+
// assert_eq!(response.status(), StatusCode::OK);
331+
// assert_eq!(
332+
// response.into_json(),
333+
// json!({ "ok": true, "msg": "user user-2 has been invited to be an owner of crate foo_crate" })
334+
// );
329335
}
330336

331337
#[test]
@@ -344,11 +350,17 @@ fn owner_change_via_change_owner_token_with_matching_crate_scope() {
344350
let body = json!({ "owners": [user2.gh_login] });
345351
let body = serde_json::to_vec(&body).unwrap();
346352
let response = token.put::<()>(&url, &body);
347-
assert_eq!(response.status(), StatusCode::OK);
353+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
348354
assert_eq!(
349355
response.into_json(),
350-
json!({ "ok": true, "msg": "user user-2 has been invited to be an owner of crate foo_crate" })
356+
json!({ "errors": [{ "detail": "must be logged in to perform that action" }] })
351357
);
358+
// TODO swap these assertions once token scopes are activated for this endpoint
359+
// assert_eq!(response.status(), StatusCode::OK);
360+
// assert_eq!(
361+
// response.into_json(),
362+
// json!({ "ok": true, "msg": "user user-2 has been invited to be an owner of crate foo_crate" })
363+
// );
352364
}
353365

354366
#[test]
@@ -367,17 +379,11 @@ fn owner_change_via_change_owner_token_with_wrong_crate_scope() {
367379
let body = json!({ "owners": [user2.gh_login] });
368380
let body = serde_json::to_vec(&body).unwrap();
369381
let response = token.put::<()>(&url, &body);
370-
assert_eq!(response.status(), StatusCode::OK);
382+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
371383
assert_eq!(
372384
response.into_json(),
373-
json!({ "ok": true, "msg": "user user-2 has been invited to be an owner of crate foo_crate" })
385+
json!({ "errors": [{ "detail": "must be logged in to perform that action" }] })
374386
);
375-
// TODO this should return "403 Forbidden" once token scopes are implemented for this endpoint
376-
// assert_eq!(response.status(), StatusCode::FORBIDDEN);
377-
// assert_eq!(
378-
// response.into_json(),
379-
// json!({ "errors": [{ "detail": "must be logged in to perform that action" }] })
380-
// );
381387
}
382388

383389
#[test]
@@ -395,17 +401,11 @@ fn owner_change_via_publish_token() {
395401
let body = json!({ "owners": [user2.gh_login] });
396402
let body = serde_json::to_vec(&body).unwrap();
397403
let response = token.put::<()>(&url, &body);
398-
assert_eq!(response.status(), StatusCode::OK);
404+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
399405
assert_eq!(
400406
response.into_json(),
401-
json!({ "ok": true, "msg": "user user-2 has been invited to be an owner of crate foo_crate" })
407+
json!({ "errors": [{ "detail": "must be logged in to perform that action" }] })
402408
);
403-
// TODO this should return "403 Forbidden" once token scopes are implemented for this endpoint
404-
// assert_eq!(response.status(), StatusCode::FORBIDDEN);
405-
// assert_eq!(
406-
// response.into_json(),
407-
// json!({ "errors": [{ "detail": "must be logged in to perform that action" }] })
408-
// );
409409
}
410410

411411
#[test]

0 commit comments

Comments
 (0)