Skip to content

Commit 66095dc

Browse files
committed
Auto merge of #3470 - jakeswenson:restrict-api-token-access-on-GET-requests, r=pietroalbini
Add API Token Auth restrictions on GET APIs This PR adds explicit denial of API token authentication on most GET APIs. Currently, CloudFront was never passing along the `Authorization` header for `GET` requests. (See rust-lang/simpleinfra#43 for more details.) In addressing that issue it was pointed out that there may be GET APIs that the team might not want to commit to allowing API tokens access to. This PR attempts to limit the exposed APIs to: - `/api/v1/crates?following=1` (the crates "search" API, which is the only way to get the list of crates a user is following.) - `/api/v1/me` > Note: There were already [tests verifying token access to this API][me-tests], but with the CloudFront issue this API was not actually accessible. [me-tests]: https://github.com/rust-lang/crates.io/blob/03f7b273eb7a024de8282ac2297daaa47df33fa6/src/tests/token.rs#L270
2 parents d7ec981 + 894460f commit 66095dc

File tree

9 files changed

+97
-4
lines changed

9 files changed

+97
-4
lines changed

src/controllers/crate_owner_invitation.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::collections::HashMap;
99
/// Handles the `GET /me/crate_owner_invitations` route.
1010
pub fn list(req: &mut dyn RequestExt) -> EndpointResult {
1111
// Ensure that the user is authenticated
12-
let user = req.authenticate()?.user();
12+
let user = req.authenticate()?.forbid_api_token_auth()?.user();
1313

1414
// Load all pending invitations for the user
1515
let conn = &*req.db_read_only()?;

src/controllers/krate/follow.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub fn unfollow(req: &mut dyn RequestExt) -> EndpointResult {
4646
pub fn following(req: &mut dyn RequestExt) -> EndpointResult {
4747
use diesel::dsl::exists;
4848

49-
let user_id = req.authenticate()?.user_id();
49+
let user_id = req.authenticate()?.forbid_api_token_auth()?.user_id();
5050
let conn = req.db_read_only()?;
5151
let follow = follow_target(req, &conn, user_id)?;
5252
let following = diesel::select(exists(follows::table.find(follow.id()))).get_result(&*conn)?;

src/controllers/token.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use serde_json as json;
99

1010
/// Handles the `GET /me/tokens` route.
1111
pub fn list(req: &mut dyn RequestExt) -> EndpointResult {
12-
let authenticated_user = req.authenticate()?;
12+
let authenticated_user = req.authenticate()?.forbid_api_token_auth()?;
1313
let conn = req.db_conn()?;
1414
let user = authenticated_user.user();
1515

src/controllers/user/me.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub fn me(req: &mut dyn RequestExt) -> EndpointResult {
5454
pub fn updates(req: &mut dyn RequestExt) -> EndpointResult {
5555
use diesel::dsl::any;
5656

57-
let authenticated_user = req.authenticate()?;
57+
let authenticated_user = req.authenticate()?.forbid_api_token_auth()?;
5858
let user = authenticated_user.user();
5959

6060
let followed_crates = Follow::belonging_to(&user).select(follows::crate_id);

src/controllers/util.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ impl AuthenticatedUser {
2828
pub fn user(self) -> User {
2929
self.user
3030
}
31+
32+
/// Disallows token authenticated users
33+
pub fn forbid_api_token_auth(self) -> AppResult<Self> {
34+
if self.token_id.is_none() {
35+
Ok(self)
36+
} else {
37+
Err(
38+
internal("API Token authentication was explicitly disallowed for this API")
39+
.chain(forbidden()),
40+
)
41+
}
42+
}
3143
}
3244

3345
/// The Origin header (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin)

src/tests/krate/following.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,63 @@ fn following() {
5757
assert!(!is_following());
5858
assert_eq!(user.search("following=1").crates.len(), 0);
5959
}
60+
61+
#[test]
62+
fn disallow_api_token_auth_for_get_crate_following_status() {
63+
let (app, _, _, token) = TestApp::init().with_token();
64+
let api_token = token.as_model();
65+
66+
let a_crate = "a_crate";
67+
68+
app.db(|conn| {
69+
CrateBuilder::new(a_crate, api_token.user_id).expect_build(conn);
70+
});
71+
72+
// Token auth on GET for get following status is disallowed
73+
token
74+
.get(&format!("/api/v1/crates/{}/following", a_crate))
75+
.assert_forbidden();
76+
}
77+
78+
#[test]
79+
fn getting_followed_crates_allows_api_token_auth() {
80+
let (app, _, user, token) = TestApp::init().with_token();
81+
let api_token = token.as_model();
82+
83+
let crate_to_follow = "some_crate_to_follow";
84+
let crate_not_followed = "another_crate";
85+
86+
app.db(|conn| {
87+
CrateBuilder::new(crate_to_follow, api_token.user_id).expect_build(conn);
88+
CrateBuilder::new(crate_not_followed, api_token.user_id).expect_build(conn);
89+
});
90+
91+
let is_following = |crate_name: &str| -> bool {
92+
#[derive(Deserialize)]
93+
struct F {
94+
following: bool,
95+
}
96+
97+
// Token auth on GET for get following status is disallowed
98+
user.get::<F>(&format!("/api/v1/crates/{}/following", crate_name))
99+
.good()
100+
.following
101+
};
102+
103+
let follow = |crate_name: &str| {
104+
assert!(
105+
token
106+
.put::<OkBool>(&format!("/api/v1/crates/{}/follow", crate_name), b"")
107+
.good()
108+
.ok
109+
);
110+
};
111+
112+
follow(crate_to_follow);
113+
114+
assert!(is_following(crate_to_follow));
115+
assert!(!is_following(crate_not_followed));
116+
117+
let json = token.search("following=1");
118+
assert_eq!(json.crates.len(), 1);
119+
}

src/tests/owners.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,15 @@ fn invitations_are_empty_by_default() {
421421
assert_eq!(json.crate_owner_invitations.len(), 0);
422422
}
423423

424+
#[test]
425+
fn api_token_cannot_list_invitations() {
426+
let (_, _, _, token) = TestApp::init().with_token();
427+
428+
token
429+
.get("/api/v1/me/crate_owner_invitations")
430+
.assert_forbidden();
431+
}
432+
424433
#[test]
425434
fn invitations_list() {
426435
let (app, _, owner, token) = TestApp::init().with_token();

src/tests/token.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ fn list_logged_out() {
3636
anon.get(URL).assert_forbidden();
3737
}
3838

39+
#[test]
40+
fn list_with_api_token_is_forbidden() {
41+
let (_, _, _, token) = TestApp::init().with_token();
42+
token.get(URL).assert_forbidden();
43+
}
44+
3945
#[test]
4046
fn list_empty() {
4147
let (_, _, user) = TestApp::init().with_user();

src/tests/user.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,12 @@ fn crates_by_user_id_not_including_deleted_owners() {
206206
assert_eq!(response.crates.len(), 0);
207207
}
208208

209+
#[test]
210+
fn api_token_cannot_get_user_updates() {
211+
let (_, _, _, token) = TestApp::init().with_token();
212+
token.get("/api/v1/me/updates").assert_forbidden();
213+
}
214+
209215
#[test]
210216
fn following() {
211217
use cargo_registry::schema::versions;

0 commit comments

Comments
 (0)