Skip to content

Commit 3f827e0

Browse files
committed
Move the source of truth for auth checks into config::Server.
1 parent 23c1447 commit 3f827e0

File tree

13 files changed

+108
-186
lines changed

13 files changed

+108
-186
lines changed

src/auth.rs

Lines changed: 36 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::config::Server;
12
use crate::controllers;
23
use crate::controllers::util::RequestPartsExt;
34
use crate::middleware::log_request::RequestLogExt;
@@ -73,12 +74,17 @@ impl AuthCheck {
7374
pub fn check<T: RequestPartsExt>(
7475
&self,
7576
request: &T,
77+
config: &Server,
7678
conn: &mut PgConnection,
7779
) -> AppResult<Authentication> {
78-
self.check_authentication(authenticate(request, conn)?)
80+
self.check_authentication(authenticate(request, conn)?, config)
7981
}
8082

81-
fn check_authentication(&self, auth: Authentication) -> AppResult<Authentication> {
83+
fn check_authentication(
84+
&self,
85+
auth: Authentication,
86+
config: &Server,
87+
) -> AppResult<Authentication> {
8288
if let Some(token) = auth.api_token() {
8389
if !self.allow_token {
8490
let error_message =
@@ -97,8 +103,7 @@ impl AuthCheck {
97103
}
98104
}
99105

100-
// FIXME: better admin detection through chemistry^Wstate.
101-
if self.require_admin && auth.user().admin().is_err() {
106+
if self.require_admin && !config.gh_admin_user_ids.contains(&auth.user().gh_id) {
102107
let error_message = "User is unauthorized";
103108
return Err(internal(error_message).chain(forbidden()));
104109
}
@@ -269,10 +274,6 @@ fn ensure_not_locked(user: &User) -> AppResult<()> {
269274

270275
#[cfg(test)]
271276
mod tests {
272-
use std::{collections::HashSet, iter::Cycle};
273-
274-
use crate::models::helpers;
275-
276277
use super::*;
277278

278279
fn cs(scope: &str) -> CrateScope {
@@ -373,81 +374,42 @@ mod tests {
373374
#[test]
374375
fn require_admin() {
375376
let auth_check = AuthCheck::default().require_admin();
377+
let config = Server {
378+
gh_admin_user_ids: [42, 43].into_iter().collect(),
379+
..Default::default()
380+
};
376381

377-
let mut gen = MockUserGenerator::default();
378-
let admin = gen.admin();
379-
let non_admin = gen.regular();
382+
assert_ok!(auth_check.check_authentication(mock_cookie(42), &config));
383+
assert_err!(auth_check.check_authentication(mock_cookie(44), &config));
384+
assert_ok!(auth_check.check_authentication(mock_token(43), &config));
385+
assert_err!(auth_check.check_authentication(mock_token(45), &config));
386+
}
380387

381-
assert_ok!(auth_check.check_authentication(mock_cookie(admin.clone())));
382-
assert_err!(auth_check.check_authentication(mock_cookie(non_admin.clone())));
383-
assert_ok!(auth_check.check_authentication(mock_token(admin)));
384-
assert_err!(auth_check.check_authentication(mock_token(non_admin)));
388+
fn mock_user(gh_id: i32) -> User {
389+
User {
390+
id: 3,
391+
gh_access_token: "arbitrary".into(),
392+
gh_login: "literally_anything".into(),
393+
name: None,
394+
gh_avatar: None,
395+
gh_id,
396+
account_lock_reason: None,
397+
account_lock_until: None,
398+
}
385399
}
386400

387-
fn mock_cookie(user: User) -> Authentication {
388-
Authentication::Cookie(CookieAuthentication { user })
401+
fn mock_cookie(gh_id: i32) -> Authentication {
402+
Authentication::Cookie(CookieAuthentication {
403+
user: mock_user(gh_id),
404+
})
389405
}
390406

391-
fn mock_token(user: User) -> Authentication {
407+
fn mock_token(gh_id: i32) -> Authentication {
392408
Authentication::Token(TokenAuthentication {
393409
token: crate::models::token::tests::build_mock()
394-
.user_id(user.id)
410+
.user_id(gh_id)
395411
.token(),
396-
user,
412+
user: mock_user(gh_id),
397413
})
398414
}
399-
400-
// TODO: cleanup what isn't used in this set of tests.
401-
pub struct MockUserGenerator {
402-
authorized_uids: HashSet<i32>,
403-
authorized_uid_iter: Cycle<std::vec::IntoIter<i32>>,
404-
last_unauthorized_uid: i32,
405-
}
406-
407-
impl Default for MockUserGenerator {
408-
fn default() -> Self {
409-
let authorized_uids = helpers::admin::authorized_user_ids().clone();
410-
let authorized_uid_iter = authorized_uids
411-
.iter()
412-
.copied()
413-
.collect::<Vec<i32>>()
414-
.into_iter()
415-
.cycle();
416-
417-
Self {
418-
authorized_uids,
419-
authorized_uid_iter,
420-
last_unauthorized_uid: 0,
421-
}
422-
}
423-
}
424-
425-
impl MockUserGenerator {
426-
pub fn admin(&mut self) -> User {
427-
Self::mock_user(self.authorized_uid_iter.next().unwrap())
428-
}
429-
430-
pub fn regular(&mut self) -> User {
431-
let mut uid = self.last_unauthorized_uid + 1;
432-
while self.authorized_uids.contains(&uid) {
433-
uid += 1;
434-
}
435-
436-
self.last_unauthorized_uid = uid;
437-
Self::mock_user(uid)
438-
}
439-
440-
fn mock_user(gh_id: i32) -> User {
441-
User {
442-
id: 3,
443-
gh_access_token: "arbitrary".into(),
444-
gh_login: "literally_anything".into(),
445-
name: None,
446-
gh_avatar: None,
447-
gh_id,
448-
account_lock_reason: None,
449-
account_lock_until: None,
450-
}
451-
}
452-
}
453415
}

src/config.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@ pub use self::base::Base;
1313
pub use self::database_pools::{DatabasePools, DbPoolConfig};
1414
pub use crate::config::balance_capacity::BalanceCapacityConfig;
1515
use http::HeaderValue;
16-
use std::collections::HashSet;
17-
use std::time::Duration;
16+
use std::{collections::HashSet, num::ParseIntError, str::FromStr, time::Duration};
1817

1918
const DEFAULT_VERSION_ID_CACHE_SIZE: u64 = 10_000;
2019
const DEFAULT_VERSION_ID_CACHE_TTL: u64 = 5 * 60; // 5 minutes
2120

21+
// The defaults correspond to the current crates.io team, which as at the time of writing, is the
22+
// GitHub user names carols10cents, jtgeibel, Turbo87, JohnTitor, LawnGnome, and mdtro.
23+
//
24+
// FIXME: this needs to be removed once we can detect the admins from the Rust teams API.
25+
const DEFAULT_GH_ADMIN_USER_IDS: [i32; 6] = [193874, 22186, 141300, 25030997, 229984, 20070360];
26+
2227
pub struct Server {
2328
pub base: Base,
2429
pub db: DatabasePools,
@@ -47,6 +52,7 @@ pub struct Server {
4752
pub version_id_cache_ttl: Duration,
4853
pub cdn_user_agent: String,
4954
pub balance_capacity: BalanceCapacityConfig,
55+
pub gh_admin_user_ids: HashSet<i32>,
5056
}
5157

5258
impl Default for Server {
@@ -82,6 +88,9 @@ impl Default for Server {
8288
/// endpoint even with a healthy database pool.
8389
/// - `BLOCKED_ROUTES`: A comma separated list of HTTP route patterns that are manually blocked
8490
/// by an operator (e.g. `/crates/:crate_id/:version/download`).
91+
/// - `GH_ADMIN_USER_IDS`: A comma separated list of GitHub user IDs that will be considered
92+
/// admins. If not set, a default list comprised of the crates.io team as of May 2023 will be
93+
/// used.
8594
///
8695
/// # Panics
8796
///
@@ -151,6 +160,9 @@ impl Default for Server {
151160
cdn_user_agent: dotenvy::var("WEB_CDN_USER_AGENT")
152161
.unwrap_or_else(|_| "Amazon CloudFront".into()),
153162
balance_capacity: BalanceCapacityConfig::from_environment(),
163+
gh_admin_user_ids: env_optional("GH_ADMIN_USER_IDS")
164+
.map(parse_gh_admin_user_ids)
165+
.unwrap_or(DEFAULT_GH_ADMIN_USER_IDS.into_iter().collect()),
154166
}
155167
}
156168
}
@@ -289,3 +301,29 @@ fn parse_ipv6_based_cidr_blocks() {
289301
.unwrap()
290302
);
291303
}
304+
305+
fn parse_gh_admin_user_ids(users: String) -> HashSet<i32> {
306+
users
307+
.split(|c: char| !(c.is_ascii_digit()))
308+
.filter(|uid| !uid.is_empty())
309+
.map(i32::from_str)
310+
.collect::<Result<HashSet<i32>, ParseIntError>>()
311+
.expect("invalid GH_ADMIN_USER_IDS")
312+
}
313+
314+
#[test]
315+
fn test_parse_authorized_admin_users() {
316+
fn assert_authorized(input: &str, expected: &[i32]) {
317+
assert_eq!(
318+
parse_gh_admin_user_ids(input.into()),
319+
expected.iter().copied().collect()
320+
);
321+
}
322+
323+
assert_authorized("", &[]);
324+
assert_authorized("12345", &[12345]);
325+
assert_authorized("12345, 6789", &[12345, 6789]);
326+
assert_authorized(" 12345 6789 ", &[12345, 6789]);
327+
assert_authorized("12345;6789", &[12345, 6789]);
328+
assert_authorized("12345;6789;12345", &[12345, 6789]);
329+
}

src/controllers/crate_owner_invitation.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use std::collections::{HashMap, HashSet};
1919
pub async fn list(app: AppState, req: Parts) -> AppResult<Json<Value>> {
2020
conduit_compat(move || {
2121
let conn = &mut app.db_read()?;
22-
let auth = AuthCheck::only_cookie().check(&req, conn)?;
22+
let auth = AuthCheck::only_cookie().check(&req, &app.config, conn)?;
2323
let user_id = auth.user_id();
2424

2525
let PrivateListResponse {
@@ -59,7 +59,7 @@ pub async fn list(app: AppState, req: Parts) -> AppResult<Json<Value>> {
5959
pub async fn private_list(app: AppState, req: Parts) -> AppResult<Json<PrivateListResponse>> {
6060
conduit_compat(move || {
6161
let conn = &mut app.db_read()?;
62-
let auth = AuthCheck::only_cookie().check(&req, conn)?;
62+
let auth = AuthCheck::only_cookie().check(&req, &app.config, conn)?;
6363

6464
let filter = if let Some(crate_name) = req.query().get("crate_name") {
6565
ListFilter::CrateName(crate_name.clone())
@@ -267,7 +267,7 @@ pub async fn handle_invite(state: AppState, req: BytesRequest) -> AppResult<Json
267267

268268
let conn = &mut state.db_write()?;
269269

270-
let auth = AuthCheck::default().check(&req, conn)?;
270+
let auth = AuthCheck::default().check(&req, &state.config, conn)?;
271271
let user_id = auth.user_id();
272272

273273
let config = &state.config;

src/controllers/krate/follow.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ pub async fn follow(
2020
) -> AppResult<Response> {
2121
conduit_compat(move || {
2222
let conn = &mut *app.db_write()?;
23-
let user_id = AuthCheck::default().check(&req, conn)?.user_id();
23+
let user_id = AuthCheck::default()
24+
.check(&req, &app.config, conn)?
25+
.user_id();
2426
let follow = follow_target(&crate_name, conn, user_id)?;
2527
diesel::insert_into(follows::table)
2628
.values(&follow)
@@ -40,7 +42,9 @@ pub async fn unfollow(
4042
) -> AppResult<Response> {
4143
conduit_compat(move || {
4244
let conn = &mut *app.db_write()?;
43-
let user_id = AuthCheck::default().check(&req, conn)?.user_id();
45+
let user_id = AuthCheck::default()
46+
.check(&req, &app.config, conn)?
47+
.user_id();
4448
let follow = follow_target(&crate_name, conn, user_id)?;
4549
diesel::delete(&follow).execute(conn)?;
4650

@@ -59,7 +63,9 @@ pub async fn following(
5963
use diesel::dsl::exists;
6064

6165
let conn = &mut *app.db_read_prefer_primary()?;
62-
let user_id = AuthCheck::only_cookie().check(&req, conn)?.user_id();
66+
let user_id = AuthCheck::only_cookie()
67+
.check(&req, &app.config, conn)?
68+
.user_id();
6369
let follow = follow_target(&crate_name, conn, user_id)?;
6470
let following =
6571
diesel::select(exists(follows::table.find(follow.id()))).get_result::<bool>(conn)?;

src/controllers/krate/owners.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ fn modify_owners(
106106
let auth = AuthCheck::default()
107107
.with_endpoint_scope(EndpointScope::ChangeOwners)
108108
.for_crate(crate_name)
109-
.check(req, conn)?;
109+
.check(req, &app.config, conn)?;
110110

111111
let user = auth.user();
112112

src/controllers/krate/publish.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
9494
let auth = AuthCheck::default()
9595
.with_endpoint_scope(endpoint_scope)
9696
.for_crate(&new_crate.name)
97-
.check(&req, conn)?;
97+
.check(&req,&app.config, conn)?;
9898

9999
let api_token_id = auth.api_token_id();
100100
let user = auth.user();

src/controllers/krate/search.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ pub async fn search(app: AppState, req: Parts) -> AppResult<Json<Value>> {
190190
// Calculating the total number of results with filters is not supported yet.
191191
supports_seek = false;
192192

193-
let user_id = AuthCheck::default().check(&req, conn)?.user_id();
193+
let user_id = AuthCheck::default()
194+
.check(&req, &app.config, conn)?
195+
.user_id();
194196

195197
query = query.filter(
196198
crates::id.eq_any(

src/controllers/token.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use serde_json as json;
1313
pub async fn list(app: AppState, req: Parts) -> AppResult<Json<Value>> {
1414
conduit_compat(move || {
1515
let conn = &mut *app.db_read_prefer_primary()?;
16-
let auth = AuthCheck::only_cookie().check(&req, conn)?;
16+
let auth = AuthCheck::only_cookie().check(&req, &app.config, conn)?;
1717
let user = auth.user();
1818

1919
let tokens: Vec<ApiToken> = ApiToken::belonging_to(user)
@@ -53,7 +53,7 @@ pub async fn new(app: AppState, req: BytesRequest) -> AppResult<Json<Value>> {
5353

5454
let conn = &mut *app.db_write()?;
5555

56-
let auth = AuthCheck::default().check(&req, conn)?;
56+
let auth = AuthCheck::default().check(&req, &app.config, conn)?;
5757
if auth.api_token_id().is_some() {
5858
return Err(bad_request(
5959
"cannot use an API token to create a new API token",
@@ -107,7 +107,7 @@ pub async fn new(app: AppState, req: BytesRequest) -> AppResult<Json<Value>> {
107107
pub async fn revoke(app: AppState, Path(id): Path<i32>, req: Parts) -> AppResult<Json<Value>> {
108108
conduit_compat(move || {
109109
let conn = &mut *app.db_write()?;
110-
let auth = AuthCheck::default().check(&req, conn)?;
110+
let auth = AuthCheck::default().check(&req, &app.config, conn)?;
111111
let user = auth.user();
112112
diesel::update(ApiToken::belonging_to(user).find(id))
113113
.set(api_tokens::revoked.eq(true))
@@ -122,7 +122,7 @@ pub async fn revoke(app: AppState, Path(id): Path<i32>, req: Parts) -> AppResult
122122
pub async fn revoke_current(app: AppState, req: Parts) -> AppResult<Response> {
123123
conduit_compat(move || {
124124
let conn = &mut *app.db_write()?;
125-
let auth = AuthCheck::default().check(&req, conn)?;
125+
let auth = AuthCheck::default().check(&req, &app.config, conn)?;
126126
let api_token_id = auth
127127
.api_token_id()
128128
.ok_or_else(|| bad_request("token not provided"))?;

0 commit comments

Comments
 (0)