Skip to content

Commit 80b4e1f

Browse files
authored
Merge pull request #5570 from Turbo87/token-scope-tests
tests/owners: Add tests with token scopes
2 parents 652c12b + 0c1f9ca commit 80b4e1f

File tree

5 files changed

+148
-6
lines changed

5 files changed

+148
-6
lines changed

src/models.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ pub mod krate;
2828
mod owner;
2929
mod rights;
3030
mod team;
31-
mod token;
31+
pub mod token;
3232
pub mod user;
3333
mod version;

src/models/token.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod scopes;
33
use chrono::NaiveDateTime;
44
use diesel::prelude::*;
55

6+
pub use self::scopes::{CrateScope, EndpointScope};
67
use crate::models::User;
78
use crate::schema::api_tokens;
89
use crate::util::errors::{AppResult, InsecurelyGeneratedTokenRevoked};
@@ -27,22 +28,34 @@ pub struct ApiToken {
2728
pub revoked: bool,
2829
/// `None` or a list of crate scope patterns (see RFC #2947)
2930
#[serde(skip)]
30-
pub crate_scopes: Option<Vec<scopes::CrateScope>>,
31+
pub crate_scopes: Option<Vec<CrateScope>>,
3132
/// A list of endpoint scopes or `None` for the `legacy` endpoint scope (see RFC #2947)
3233
#[serde(skip)]
33-
pub endpoint_scopes: Option<Vec<scopes::EndpointScope>>,
34+
pub endpoint_scopes: Option<Vec<EndpointScope>>,
3435
}
3536

3637
impl ApiToken {
3738
/// Generates a new named API token for a user
3839
pub fn insert(conn: &PgConnection, user_id: i32, name: &str) -> AppResult<CreatedApiToken> {
40+
Self::insert_with_scopes(conn, user_id, name, None, None)
41+
}
42+
43+
pub fn insert_with_scopes(
44+
conn: &PgConnection,
45+
user_id: i32,
46+
name: &str,
47+
crate_scopes: Option<Vec<CrateScope>>,
48+
endpoint_scopes: Option<Vec<EndpointScope>>,
49+
) -> AppResult<CreatedApiToken> {
3950
let token = SecureToken::generate(SecureTokenKind::Api);
4051

4152
let model: ApiToken = diesel::insert_into(api_tokens::table)
4253
.values((
4354
api_tokens::user_id.eq(user_id),
4455
api_tokens::name.eq(name),
4556
api_tokens::token.eq(&*token),
57+
api_tokens::crate_scopes.eq(crate_scopes),
58+
api_tokens::endpoint_scopes.eq(endpoint_scopes),
4659
))
4760
.get_result(conn)?;
4861

src/tests/owners.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use cargo_registry::{
1414
Emails,
1515
};
1616

17+
use cargo_registry::models::token::{CrateScope, EndpointScope};
1718
use chrono::{Duration, Utc};
1819
use conduit::StatusCode;
1920
use diesel::prelude::*;
@@ -305,6 +306,108 @@ fn owner_change_via_token() {
305306
);
306307
}
307308

309+
#[test]
310+
fn owner_change_via_change_owner_token() {
311+
let (app, _, _, token) =
312+
TestApp::full().with_scoped_token(None, Some(vec![EndpointScope::ChangeOwners]));
313+
314+
let user2 = app.db_new_user("user-2");
315+
let user2 = user2.as_model();
316+
317+
let krate =
318+
app.db(|conn| CrateBuilder::new("foo_crate", token.as_model().user_id).expect_build(conn));
319+
320+
let url = format!("/api/v1/crates/{}/owners", krate.name);
321+
let body = json!({ "owners": [user2.gh_login] });
322+
let body = serde_json::to_vec(&body).unwrap();
323+
let response = token.put::<()>(&url, &body);
324+
assert_eq!(response.status(), StatusCode::OK);
325+
assert_eq!(
326+
response.into_json(),
327+
json!({ "ok": true, "msg": "user user-2 has been invited to be an owner of crate foo_crate" })
328+
);
329+
}
330+
331+
#[test]
332+
fn owner_change_via_change_owner_token_with_matching_crate_scope() {
333+
let crate_scopes = Some(vec![CrateScope::try_from("foo_crate").unwrap()]);
334+
let endpoint_scopes = Some(vec![EndpointScope::ChangeOwners]);
335+
let (app, _, _, token) = TestApp::full().with_scoped_token(crate_scopes, endpoint_scopes);
336+
337+
let user2 = app.db_new_user("user-2");
338+
let user2 = user2.as_model();
339+
340+
let krate =
341+
app.db(|conn| CrateBuilder::new("foo_crate", token.as_model().user_id).expect_build(conn));
342+
343+
let url = format!("/api/v1/crates/{}/owners", krate.name);
344+
let body = json!({ "owners": [user2.gh_login] });
345+
let body = serde_json::to_vec(&body).unwrap();
346+
let response = token.put::<()>(&url, &body);
347+
assert_eq!(response.status(), StatusCode::OK);
348+
assert_eq!(
349+
response.into_json(),
350+
json!({ "ok": true, "msg": "user user-2 has been invited to be an owner of crate foo_crate" })
351+
);
352+
}
353+
354+
#[test]
355+
fn owner_change_via_change_owner_token_with_wrong_crate_scope() {
356+
let crate_scopes = Some(vec![CrateScope::try_from("bar").unwrap()]);
357+
let endpoint_scopes = Some(vec![EndpointScope::ChangeOwners]);
358+
let (app, _, _, token) = TestApp::full().with_scoped_token(crate_scopes, endpoint_scopes);
359+
360+
let user2 = app.db_new_user("user-2");
361+
let user2 = user2.as_model();
362+
363+
let krate =
364+
app.db(|conn| CrateBuilder::new("foo_crate", token.as_model().user_id).expect_build(conn));
365+
366+
let url = format!("/api/v1/crates/{}/owners", krate.name);
367+
let body = json!({ "owners": [user2.gh_login] });
368+
let body = serde_json::to_vec(&body).unwrap();
369+
let response = token.put::<()>(&url, &body);
370+
assert_eq!(response.status(), StatusCode::OK);
371+
assert_eq!(
372+
response.into_json(),
373+
json!({ "ok": true, "msg": "user user-2 has been invited to be an owner of crate foo_crate" })
374+
);
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+
// );
381+
}
382+
383+
#[test]
384+
fn owner_change_via_publish_token() {
385+
let (app, _, _, token) =
386+
TestApp::full().with_scoped_token(None, Some(vec![EndpointScope::PublishUpdate]));
387+
388+
let user2 = app.db_new_user("user-2");
389+
let user2 = user2.as_model();
390+
391+
let krate =
392+
app.db(|conn| CrateBuilder::new("foo_crate", token.as_model().user_id).expect_build(conn));
393+
394+
let url = format!("/api/v1/crates/{}/owners", krate.name);
395+
let body = json!({ "owners": [user2.gh_login] });
396+
let body = serde_json::to_vec(&body).unwrap();
397+
let response = token.put::<()>(&url, &body);
398+
assert_eq!(response.status(), StatusCode::OK);
399+
assert_eq!(
400+
response.into_json(),
401+
json!({ "ok": true, "msg": "user user-2 has been invited to be an owner of crate foo_crate" })
402+
);
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+
// );
409+
}
410+
308411
#[test]
309412
fn owner_change_without_auth() {
310413
let (app, anon, cookie) = TestApp::full().with_user();

src/tests/util.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use conduit::{BoxError, Handler, Method};
2929
use conduit_cookie::SessionMiddleware;
3030
use conduit_test::MockRequest;
3131

32+
use cargo_registry::models::token::{CrateScope, EndpointScope};
3233
use conduit::header;
3334
use cookie::Cookie;
3435
use std::collections::HashMap;
@@ -263,9 +264,22 @@ impl MockCookieUser {
263264
///
264265
/// This method updates the database directly
265266
pub fn db_new_token(&self, name: &str) -> MockTokenUser {
266-
let token = self
267-
.app
268-
.db(|conn| ApiToken::insert(conn, self.user.id, name).unwrap());
267+
self.db_new_scoped_token(name, None, None)
268+
}
269+
270+
/// Creates a scoped token and wraps it in a helper struct
271+
///
272+
/// This method updates the database directly
273+
pub fn db_new_scoped_token(
274+
&self,
275+
name: &str,
276+
crate_scopes: Option<Vec<CrateScope>>,
277+
endpoint_scopes: Option<Vec<EndpointScope>>,
278+
) -> MockTokenUser {
279+
let token = self.app.db(|conn| {
280+
ApiToken::insert_with_scopes(conn, self.user.id, name, crate_scopes, endpoint_scopes)
281+
.unwrap()
282+
});
269283
MockTokenUser {
270284
app: self.app.clone(),
271285
token,

src/tests/util/test_app.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use cargo_registry_index::{Credentials, Repository as WorkerRepository, Reposito
88
use std::{rc::Rc, sync::Arc, time::Duration};
99

1010
use crate::util::github::{MockGitHubClient, MOCK_GITHUB_DATA};
11+
use cargo_registry::models::token::{CrateScope, EndpointScope};
1112
use diesel::PgConnection;
1213
use reqwest::{blocking::Client, Proxy};
1314
use std::collections::HashSet;
@@ -292,6 +293,17 @@ impl TestAppBuilder {
292293
(app, anon, user, token)
293294
}
294295

296+
pub fn with_scoped_token(
297+
self,
298+
crate_scopes: Option<Vec<CrateScope>>,
299+
endpoint_scopes: Option<Vec<EndpointScope>>,
300+
) -> (TestApp, MockAnonymousUser, MockCookieUser, MockTokenUser) {
301+
let (app, anon) = self.empty();
302+
let user = app.db_new_user("foo");
303+
let token = user.db_new_scoped_token("bar", crate_scopes, endpoint_scopes);
304+
(app, anon, user, token)
305+
}
306+
295307
pub fn with_config(mut self, f: impl FnOnce(&mut config::Server)) -> Self {
296308
f(&mut self.config);
297309
self

0 commit comments

Comments
 (0)