From f86ffa17fda4e4c522b99368dd5551d709fddf2a Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 4 May 2023 16:01:50 -0700 Subject: [PATCH 1/3] Define admin users and extend `AuthCheck` to handle them This adds a concept of admin users, who are defined by their GitHub IDs, and allows them to be defined through an environment variable, falling back to a static list of the current `crates.io` team. `AuthCheck` now has a builder method to require that the current cookie or token belong to an admin user. In the future, this will be extended to use Rust's team API for the fallback. --- .env.sample | 4 ++ src/auth.rs | 72 ++++++++++++++++++++++++++++- src/config/server.rs | 41 +++++++++++++++++ src/models/token.rs | 94 +++++++++++++++++++++++++++++++------- src/tests/util/test_app.rs | 1 + 5 files changed, 195 insertions(+), 17 deletions(-) diff --git a/.env.sample b/.env.sample index 38bdda11fb..44beb79a15 100644 --- a/.env.sample +++ b/.env.sample @@ -86,3 +86,7 @@ export SENTRY_ENV_API=local # export TEST_S3_INDEX_REGION=http://127.0.0.1:19000 # export TEST_AWS_ACCESS_KEY=minio # export TEST_AWS_SECRET_KEY=miniominio + +# IDs of GitHub users that are admins on this instance, separated by commas. +# Whitespace will be ignored. +export GH_ADMIN_USER_IDS= diff --git a/src/auth.rs b/src/auth.rs index 980783d918..7bbf0b3638 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,5 +1,8 @@ +use std::collections::HashSet; + use crate::controllers; use crate::controllers::util::RequestPartsExt; +use crate::middleware::app::RequestApp; use crate::middleware::log_request::RequestLogExt; use crate::middleware::session::RequestSession; use crate::models::token::{CrateScope, EndpointScope}; @@ -16,6 +19,7 @@ pub struct AuthCheck { allow_token: bool, endpoint_scope: Option, crate_name: Option, + require_admin: bool, } impl AuthCheck { @@ -27,6 +31,7 @@ impl AuthCheck { allow_token: true, endpoint_scope: None, crate_name: None, + require_admin: false, } } @@ -36,6 +41,7 @@ impl AuthCheck { allow_token: false, endpoint_scope: None, crate_name: None, + require_admin: false, } } @@ -44,6 +50,7 @@ impl AuthCheck { allow_token: self.allow_token, endpoint_scope: Some(endpoint_scope), crate_name: self.crate_name.clone(), + require_admin: self.require_admin, } } @@ -52,6 +59,16 @@ impl AuthCheck { allow_token: self.allow_token, endpoint_scope: self.endpoint_scope, crate_name: Some(crate_name.to_string()), + require_admin: self.require_admin, + } + } + + pub fn require_admin(&self) -> Self { + Self { + allow_token: self.allow_token, + endpoint_scope: self.endpoint_scope, + crate_name: self.crate_name.clone(), + require_admin: true, } } @@ -61,8 +78,17 @@ impl AuthCheck { request: &T, conn: &mut PgConnection, ) -> AppResult { - let auth = authenticate(request, conn)?; + self.check_authentication( + authenticate(request, conn)?, + &request.app().config.gh_admin_user_ids, + ) + } + fn check_authentication( + &self, + auth: Authentication, + gh_admin_user_ids: &HashSet, + ) -> AppResult { if let Some(token) = auth.api_token() { if !self.allow_token { let error_message = @@ -81,6 +107,11 @@ impl AuthCheck { } } + if self.require_admin && !gh_admin_user_ids.contains(&auth.user().gh_id) { + let error_message = "User is unauthorized"; + return Err(internal(error_message).chain(forbidden())); + } + Ok(auth) } @@ -347,4 +378,43 @@ mod tests { assert!(!auth_check.crate_scope_matches(Some(&vec![cs("anyhow")]))); assert!(!auth_check.crate_scope_matches(Some(&vec![cs("actix-*")]))); } + + #[test] + fn require_admin() { + let auth_check = AuthCheck::default().require_admin(); + let gh_admin_user_ids = [42, 43].into_iter().collect(); + + assert_ok!(auth_check.check_authentication(mock_cookie(42), &gh_admin_user_ids)); + assert_err!(auth_check.check_authentication(mock_cookie(44), &gh_admin_user_ids)); + assert_ok!(auth_check.check_authentication(mock_token(43), &gh_admin_user_ids)); + assert_err!(auth_check.check_authentication(mock_token(45), &gh_admin_user_ids)); + } + + fn mock_user(gh_id: i32) -> User { + User { + id: 3, + gh_access_token: "arbitrary".into(), + gh_login: "literally_anything".into(), + name: None, + gh_avatar: None, + gh_id, + account_lock_reason: None, + account_lock_until: None, + } + } + + fn mock_cookie(gh_id: i32) -> Authentication { + Authentication::Cookie(CookieAuthentication { + user: mock_user(gh_id), + }) + } + + fn mock_token(gh_id: i32) -> Authentication { + Authentication::Token(TokenAuthentication { + token: crate::models::token::tests::build_mock() + .user_id(gh_id) + .token(), + user: mock_user(gh_id), + }) + } } diff --git a/src/config/server.rs b/src/config/server.rs index 2a2ac2028e..838635eceb 100644 --- a/src/config/server.rs +++ b/src/config/server.rs @@ -12,11 +12,19 @@ use crate::storage::StorageConfig; use http::HeaderValue; use std::collections::HashSet; use std::net::IpAddr; +use std::num::ParseIntError; +use std::str::FromStr; use std::time::Duration; const DEFAULT_VERSION_ID_CACHE_SIZE: u64 = 10_000; const DEFAULT_VERSION_ID_CACHE_TTL: u64 = 5 * 60; // 5 minutes +// The defaults correspond to the current crates.io team, which as at the time of writing, is the +// GitHub user names carols10cents, jtgeibel, Turbo87, JohnTitor, LawnGnome, and mdtro. +// +// FIXME: this needs to be removed once we can detect the admins from the Rust teams API. +const DEFAULT_GH_ADMIN_USER_IDS: [i32; 6] = [193874, 22186, 141300, 25030997, 229984, 20070360]; + pub struct Server { pub base: Base, pub ip: IpAddr, @@ -50,6 +58,7 @@ pub struct Server { pub version_id_cache_ttl: Duration, pub cdn_user_agent: String, pub balance_capacity: BalanceCapacityConfig, + pub gh_admin_user_ids: HashSet, /// Should the server serve the frontend assets in the `dist` directory? pub serve_dist: bool, @@ -94,6 +103,9 @@ impl Default for Server { /// endpoint even with a healthy database pool. /// - `BLOCKED_ROUTES`: A comma separated list of HTTP route patterns that are manually blocked /// by an operator (e.g. `/crates/:crate_id/:version/download`). + /// - `GH_ADMIN_USER_IDS`: A comma separated list of GitHub user IDs that will be considered + /// admins. If not set, a default list comprised of the crates.io team as of May 2023 will be + /// used. /// /// # Panics /// @@ -185,6 +197,10 @@ impl Default for Server { cdn_user_agent: dotenvy::var("WEB_CDN_USER_AGENT") .unwrap_or_else(|_| "Amazon CloudFront".into()), balance_capacity: BalanceCapacityConfig::from_environment(), + gh_admin_user_ids: env_optional("GH_ADMIN_USER_IDS") + .map(parse_gh_admin_user_ids) + .unwrap_or_else(|| Ok(DEFAULT_GH_ADMIN_USER_IDS.into_iter().collect())) + .expect("invalid GH_ADMIN_USER_IDS"), serve_dist: true, serve_html: true, use_fastboot: dotenvy::var("USE_FASTBOOT").ok(), @@ -326,3 +342,28 @@ fn parse_ipv6_based_cidr_blocks() { .unwrap() ); } + +fn parse_gh_admin_user_ids(users: String) -> Result, ParseIntError> { + users + .split(|c: char| !(c.is_ascii_digit())) + .filter(|uid| !uid.is_empty()) + .map(i32::from_str) + .collect() +} + +#[test] +fn test_gh_admin_user_ids() { + fn assert_authorized(input: &str, expected: &[i32]) { + assert_eq!( + parse_gh_admin_user_ids(input.into()).unwrap(), + expected.iter().copied().collect() + ); + } + + assert_authorized("", &[]); + assert_authorized("12345", &[12345]); + assert_authorized("12345, 6789", &[12345, 6789]); + assert_authorized(" 12345 6789 ", &[12345, 6789]); + assert_authorized("12345;6789", &[12345, 6789]); + assert_authorized("12345;6789;12345", &[12345, 6789]); +} diff --git a/src/models/token.rs b/src/models/token.rs index 0b9dfe1a78..f5336abbfb 100644 --- a/src/models/token.rs +++ b/src/models/token.rs @@ -98,28 +98,28 @@ pub struct CreatedApiToken { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; use chrono::NaiveDate; #[test] fn api_token_serializes_to_rfc3339() { - let tok = ApiToken { - id: 12345, - user_id: 23456, - revoked: false, - name: "".to_string(), - created_at: NaiveDate::from_ymd_opt(2017, 1, 6) - .unwrap() - .and_hms_opt(14, 23, 11) + let tok = build_mock() + .created_at( + NaiveDate::from_ymd_opt(2017, 1, 6) + .unwrap() + .and_hms_opt(14, 23, 11) + .unwrap(), + ) + .last_used_at( + Some( + NaiveDate::from_ymd_opt(2017, 1, 6) + .unwrap() + .and_hms_opt(14, 23, 12), + ) .unwrap(), - last_used_at: NaiveDate::from_ymd_opt(2017, 1, 6) - .unwrap() - .and_hms_opt(14, 23, 12), - crate_scopes: None, - endpoint_scopes: None, - expired_at: None, - }; + ) + .token(); let json = serde_json::to_string(&tok).unwrap(); assert_some!(json .as_str() @@ -128,4 +128,66 @@ mod tests { .as_str() .find(r#""last_used_at":"2017-01-06T14:23:12+00:00""#)); } + + pub struct MockBuilder(ApiToken); + + impl MockBuilder { + pub fn token(self) -> ApiToken { + self.0 + } + + pub fn id(mut self, id: i32) -> Self { + self.0.id = id; + self + } + + pub fn user_id(mut self, user_id: i32) -> Self { + self.0.user_id = user_id; + self + } + + pub fn name(mut self, name: String) -> Self { + self.0.name = name; + self + } + + pub fn created_at(mut self, created_at: NaiveDateTime) -> Self { + self.0.created_at = created_at; + self + } + + pub fn last_used_at(mut self, last_used_at: Option) -> Self { + self.0.last_used_at = last_used_at; + self + } + + pub fn revoked(mut self, revoked: bool) -> Self { + self.0.revoked = revoked; + self + } + + pub fn crate_scopes(mut self, crate_scopes: Option>) -> Self { + self.0.crate_scopes = crate_scopes; + self + } + + pub fn endpoint_scopes(mut self, endpoint_scopes: Option>) -> Self { + self.0.endpoint_scopes = endpoint_scopes; + self + } + } + + pub fn build_mock() -> MockBuilder { + MockBuilder(ApiToken { + id: 12345, + user_id: 23456, + revoked: false, + name: "".to_string(), + created_at: NaiveDateTime::default(), + last_used_at: None, + crate_scopes: None, + endpoint_scopes: None, + expired_at: None, + }) + } } diff --git a/src/tests/util/test_app.rs b/src/tests/util/test_app.rs index 3d27d9137e..844c224f78 100644 --- a/src/tests/util/test_app.rs +++ b/src/tests/util/test_app.rs @@ -439,6 +439,7 @@ fn simple_config() -> config::Server { version_id_cache_ttl: Duration::from_secs(5 * 60), cdn_user_agent: "Amazon CloudFront".to_string(), balance_capacity, + gh_admin_user_ids: HashSet::new(), // The frontend code is not needed for the backend tests. serve_dist: false, From 6ce80a7dba9c3e18548d2c44d256dfe006bbb12e Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Wed, 7 Jun 2023 14:43:47 -0700 Subject: [PATCH 2/3] tests: define `assert_forbidden` and `assert_not_found` for any response The existing uses of these functions meant that uses of `get` and `post` didn't have to explicitly turbofish into `()`, but defining them this way means they can't be used on other responses (for example, the ones returned from `yank` and `unyank`). Moving the definitions into `Response` means we can now use these assertion helpers on any response type, at the cost of having some more turbofish. --- src/tests/owners.rs | 2 +- src/tests/routes/categories/get.rs | 2 +- src/tests/routes/crates/following.rs | 4 +-- src/tests/routes/crates/versions/download.rs | 2 +- src/tests/routes/keywords/read.rs | 4 +-- src/tests/routes/me/get.rs | 2 +- src/tests/routes/me/tokens/create.rs | 3 ++- src/tests/routes/me/tokens/list.rs | 4 +-- src/tests/routes/me/updates.rs | 2 +- src/tests/token.rs | 2 +- src/tests/util/response.rs | 26 +++++++++----------- 11 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/tests/owners.rs b/src/tests/owners.rs index ee774e12ab..7a89cd2a93 100644 --- a/src/tests/owners.rs +++ b/src/tests/owners.rs @@ -589,7 +589,7 @@ fn api_token_cannot_list_invitations_v1() { let (_, _, _, token) = TestApp::init().with_token(); token - .get("/api/v1/me/crate_owner_invitations") + .get::<()>("/api/v1/me/crate_owner_invitations") .assert_forbidden(); } diff --git a/src/tests/routes/categories/get.rs b/src/tests/routes/categories/get.rs index 068eaf3c23..4dcd148fcd 100644 --- a/src/tests/routes/categories/get.rs +++ b/src/tests/routes/categories/get.rs @@ -11,7 +11,7 @@ fn show() { let url = "/api/v1/categories/foo-bar"; // Return not found if a category doesn't exist - anon.get(url).assert_not_found(); + anon.get::<()>(url).assert_not_found(); // Create a category and a subcategory app.db(|conn| { diff --git a/src/tests/routes/crates/following.rs b/src/tests/routes/crates/following.rs index daaab20143..b5b6fa59a9 100644 --- a/src/tests/routes/crates/following.rs +++ b/src/tests/routes/crates/following.rs @@ -5,7 +5,7 @@ use crate::util::{RequestHelper, TestApp}; fn diesel_not_found_results_in_404() { let (_, _, user) = TestApp::init().with_user(); - user.get("/api/v1/crates/foo_following/following") + user.get::<()>("/api/v1/crates/foo_following/following") .assert_not_found(); } @@ -22,6 +22,6 @@ fn disallow_api_token_auth_for_get_crate_following_status() { // Token auth on GET for get following status is disallowed token - .get(&format!("/api/v1/crates/{a_crate}/following")) + .get::<()>(&format!("/api/v1/crates/{a_crate}/following")) .assert_forbidden(); } diff --git a/src/tests/routes/crates/versions/download.rs b/src/tests/routes/crates/versions/download.rs index 8c5e0a70f5..213b064bee 100644 --- a/src/tests/routes/crates/versions/download.rs +++ b/src/tests/routes/crates/versions/download.rs @@ -10,7 +10,7 @@ fn download_nonexistent_version_of_existing_crate_404s() { CrateBuilder::new("foo_bad", user.id).expect_build(conn); }); - anon.get("/api/v1/crates/foo_bad/0.1.0/download") + anon.get::<()>("/api/v1/crates/foo_bad/0.1.0/download") .assert_not_found(); } diff --git a/src/tests/routes/keywords/read.rs b/src/tests/routes/keywords/read.rs index a463ecf2fb..a98c9d5dbd 100644 --- a/src/tests/routes/keywords/read.rs +++ b/src/tests/routes/keywords/read.rs @@ -12,7 +12,7 @@ struct GoodKeyword { fn show() { let url = "/api/v1/keywords/foo"; let (app, anon) = TestApp::init().empty(); - anon.get(url).assert_not_found(); + anon.get::<()>(url).assert_not_found(); app.db(|conn| { Keyword::find_or_create_all(conn, &["foo"]).unwrap(); @@ -25,7 +25,7 @@ fn show() { fn uppercase() { let url = "/api/v1/keywords/UPPER"; let (app, anon) = TestApp::init().empty(); - anon.get(url).assert_not_found(); + anon.get::<()>(url).assert_not_found(); app.db(|conn| { Keyword::find_or_create_all(conn, &["UPPER"]).unwrap(); diff --git a/src/tests/routes/me/get.rs b/src/tests/routes/me/get.rs index 2bc038d1f6..2b38209a88 100644 --- a/src/tests/routes/me/get.rs +++ b/src/tests/routes/me/get.rs @@ -19,7 +19,7 @@ pub struct UserShowPrivateResponse { fn me() { let url = "/api/v1/me"; let (app, anon) = TestApp::init().empty(); - anon.get(url).assert_forbidden(); + anon.get::<()>(url).assert_forbidden(); let user = app.db_new_user("foo"); let json = user.show_me(); diff --git a/src/tests/routes/me/tokens/create.rs b/src/tests/routes/me/tokens/create.rs index 56ec7abbc2..8b2ceb447c 100644 --- a/src/tests/routes/me/tokens/create.rs +++ b/src/tests/routes/me/tokens/create.rs @@ -11,7 +11,8 @@ static NEW_BAR: &[u8] = br#"{ "api_token": { "name": "bar" } }"#; #[test] fn create_token_logged_out() { let (_, anon) = TestApp::init().empty(); - anon.put("/api/v1/me/tokens", NEW_BAR).assert_forbidden(); + anon.put::<()>("/api/v1/me/tokens", NEW_BAR) + .assert_forbidden(); } #[test] diff --git a/src/tests/routes/me/tokens/list.rs b/src/tests/routes/me/tokens/list.rs index 871ae0b8d6..8f85355170 100644 --- a/src/tests/routes/me/tokens/list.rs +++ b/src/tests/routes/me/tokens/list.rs @@ -8,13 +8,13 @@ use http::StatusCode; #[test] fn list_logged_out() { let (_, anon) = TestApp::init().empty(); - anon.get("/api/v1/me/tokens").assert_forbidden(); + anon.get::<()>("/api/v1/me/tokens").assert_forbidden(); } #[test] fn list_with_api_token_is_forbidden() { let (_, _, _, token) = TestApp::init().with_token(); - token.get("/api/v1/me/tokens").assert_forbidden(); + token.get::<()>("/api/v1/me/tokens").assert_forbidden(); } #[test] diff --git a/src/tests/routes/me/updates.rs b/src/tests/routes/me/updates.rs index 5fbdeb6596..27950bfc3b 100644 --- a/src/tests/routes/me/updates.rs +++ b/src/tests/routes/me/updates.rs @@ -10,7 +10,7 @@ use http::StatusCode; #[test] fn api_token_cannot_get_user_updates() { let (_, _, _, token) = TestApp::init().with_token(); - token.get("/api/v1/me/updates").assert_forbidden(); + token.get::<()>("/api/v1/me/updates").assert_forbidden(); } #[test] diff --git a/src/tests/token.rs b/src/tests/token.rs index e31ef36465..bf18321808 100644 --- a/src/tests/token.rs +++ b/src/tests/token.rs @@ -9,7 +9,7 @@ fn using_token_updates_last_used_at() { let url = "/api/v1/me"; let (app, anon, user, token) = TestApp::init().with_token(); - anon.get(url).assert_forbidden(); + anon.get::<()>(url).assert_forbidden(); user.get::(url).good(); assert_none!(token.as_model().last_used_at); diff --git a/src/tests/util/response.rs b/src/tests/util/response.rs index 513327e349..b3a0406587 100644 --- a/src/tests/util/response.rs +++ b/src/tests/util/response.rs @@ -23,6 +23,18 @@ where } json(self.response) } + + /// Assert that the status code is 404 + #[track_caller] + pub fn assert_not_found(&self) { + assert_eq!(StatusCode::NOT_FOUND, self.status()); + } + + /// Assert that the status code is 403 + #[track_caller] + pub fn assert_forbidden(&self) { + assert_eq!(StatusCode::FORBIDDEN, self.status()); + } } impl Response { @@ -59,20 +71,6 @@ impl Response { } } -impl Response<()> { - /// Assert that the status code is 404 - #[track_caller] - pub fn assert_not_found(&self) { - assert_eq!(StatusCode::NOT_FOUND, self.status()); - } - - /// Assert that the status code is 403 - #[track_caller] - pub fn assert_forbidden(&self) { - assert_eq!(StatusCode::FORBIDDEN, self.status()); - } -} - impl Deref for Response { type Target = reqwest::blocking::Response; From 0a28df6fe8a8cf407c46dd2775a1b50648bdb365 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 13 Jul 2023 17:00:19 -0700 Subject: [PATCH 3/3] admin: add a new admin backend to yank and unyank crates This uses our existing `minijinja` dependency to implement a (mostly) static HTML admin console that the crates.io team can use to administer crates without needing direct database access. For now, the only administrative action allowed is yanking and unyanking crate versions, but further actions are anticipated to be added in the near future. The spiciest part of this commit is probably the routing changes, rather than the actual templating code and controller changes, since these need to be applied across the development server, nginx, and anything else that's in front of our frontend and backend servers. --- Cargo.lock | 127 ++++++++++++++ Cargo.toml | 4 +- admin/templates/base.html | 56 ++++++ admin/templates/crates/index.html | 136 +++++++++++++++ admin/templates/macros.html | 53 ++++++ app/components/header.hbs | 3 + app/models/user.js | 1 + config/nginx.conf.erb | 4 + deny.toml | 1 + package.json | 25 ++- pnpm-lock.yaml | 44 +++++ server/index.js | 2 +- src/app.rs | 6 + src/controllers.rs | 1 + src/controllers/admin.rs | 100 +++++++++++ src/controllers/helpers/pagination.rs | 75 ++++++++ src/controllers/user/me.rs | 3 +- src/controllers/version/yank.rs | 4 +- src/middleware/ember_html.rs | 7 +- src/router.rs | 4 +- src/tests/krate/yanking.rs | 33 ++++ src/tests/util/test_app.rs | 37 +++- src/views.rs | 9 +- src/views/admin.rs | 2 + src/views/admin/crates.rs | 59 +++++++ src/views/admin/templating.rs | 156 +++++++++++++++++ src/views/admin/templating/components.rs | 8 + .../admin/templating/components/datetime.rs | 161 ++++++++++++++++++ src/views/admin/templating/components/page.rs | 80 +++++++++ src/views/admin/templating/components/user.rs | 18 ++ src/views/admin/templating/helpers.rs | 9 + 31 files changed, 1214 insertions(+), 14 deletions(-) create mode 100644 admin/templates/base.html create mode 100644 admin/templates/crates/index.html create mode 100644 admin/templates/macros.html create mode 100644 src/controllers/admin.rs create mode 100644 src/views/admin.rs create mode 100644 src/views/admin/crates.rs create mode 100644 src/views/admin/templating.rs create mode 100644 src/views/admin/templating/components.rs create mode 100644 src/views/admin/templating/components/datetime.rs create mode 100644 src/views/admin/templating/components/page.rs create mode 100644 src/views/admin/templating/components/user.rs create mode 100644 src/views/admin/templating/helpers.rs diff --git a/Cargo.lock b/Cargo.lock index 64381d556e..013b4f17cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -681,6 +681,7 @@ dependencies = [ "ipnetwork", "lettre", "minijinja", + "minijinja-autoreload", "moka", "oauth2", "object_store", @@ -692,6 +693,7 @@ dependencies = [ "reqwest", "retry", "ring", + "rust-embed", "scheduled-thread-pool", "secrecy", "semver", @@ -1126,6 +1128,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futf" version = "0.1.5" @@ -1597,6 +1608,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.3" @@ -1701,6 +1732,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1872,6 +1923,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memo-map" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec276c09560ce4447087aaefc19eb0c18d97e31bd05ebac38881c4723400c40" + [[package]] name = "memoffset" version = "0.9.0" @@ -1924,9 +1981,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "822ebf53abf914648234330ca59b88226ed278551b1b42b47e472957c12660d6" dependencies = [ + "memo-map", + "self_cell", "serde", ] +[[package]] +name = "minijinja-autoreload" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc3c1e0fe1a3a8a88778f4d9624514c35529813b73d2780d832b08fd13fb0799" +dependencies = [ + "minijinja", + "notify", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1949,6 +2018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -2012,6 +2082,23 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729f63e1ca555a43fe3efa4f3efdf4801c479da85b432242a7b726f353c88486" +dependencies = [ + "bitflags 1.3.2", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2666,6 +2753,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "rust-embed" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.25", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2826,6 +2947,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" + [[package]] name = "semver" version = "1.0.17" diff --git a/Cargo.toml b/Cargo.toml index b08cb44fd1..7ddde731b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,8 @@ indicatif = "=0.17.5" ipnetwork = "=0.20.0" tikv-jemallocator = { version = "=0.5.0", features = ['unprefixed_malloc_on_supported_platforms', 'profiling'] } lettre = { version = "=0.10.4", default-features = false, features = ["file-transport", "smtp-transport", "native-tls", "hostname", "builder"] } -minijinja = "=1.0.4" +minijinja = { version = "=1.0.4", features = ["loader"] } +minijinja-autoreload = "=1.0.4" moka = { version = "=0.11.2", features = ["future"] } oauth2 = { version = "=4.4.1", default-features = false, features = ["reqwest"] } object_store = { version = "=0.6.1", features = ["aws"] } @@ -75,6 +76,7 @@ rand = "=0.8.5" reqwest = { version = "=0.11.18", features = ["blocking", "gzip", "json"] } retry = "=2.0.0" ring = "=0.16.20" +rust-embed = "=6.8.1" scheduled-thread-pool = "=0.2.7" secrecy = "=0.8.0" semver = { version = "=1.0.17", features = ["serde"] } diff --git a/admin/templates/base.html b/admin/templates/base.html new file mode 100644 index 0000000000..1fadea264e --- /dev/null +++ b/admin/templates/base.html @@ -0,0 +1,56 @@ + + + + + + + {% block title %} + + {% endblock %}:: crates.io + + + + + {% block head %} + + {% endblock %} + + + + +
+ {% block content %} + + {% endblock %} +
+ + diff --git a/admin/templates/crates/index.html b/admin/templates/crates/index.html new file mode 100644 index 0000000000..6a95971112 --- /dev/null +++ b/admin/templates/crates/index.html @@ -0,0 +1,136 @@ +{% extends 'base.html' %} +{% from 'macros.html' import datetime, page_list, user %} + +{% block head %} + + + + +{% endblock %} + +{% block title %} + Crates +{% endblock %} + +{% block content %} +
+

Crates

+
+ +
+
+ +
+

Error

+ +

+  
+ + + + + + + + + + + {% for version in view.versions %} + + + + + + + {% endfor %} + +
CrateVersionPublished +
+ + + {{ version.num }} + {{ datetime(version.created_at) }} + by + {{ user(version.publisher) }} + +
+ {% if version.yanked %} + + {% else %} + + {% endif %} +
+ + {{ page_list(view.page) }} + + +{% endblock %} diff --git a/admin/templates/macros.html b/admin/templates/macros.html new file mode 100644 index 0000000000..555fd8b29a --- /dev/null +++ b/admin/templates/macros.html @@ -0,0 +1,53 @@ +{% macro datetime(dt) %} + {{ dt.human }} +{% endmacro %} + +{% macro page_list(page) %} + {% if page.paginated %} + + {% endif %} +{% endmacro %} + +{% macro user(user) %} + + {% if user.avatar %} + + {%- endif -%} + {{- user.username }} + +{% endmacro %} diff --git a/app/components/header.hbs b/app/components/header.hbs index 9c1368d776..cda3d543d3 100644 --- a/app/components/header.hbs +++ b/app/components/header.hbs @@ -29,6 +29,9 @@ Dashboard Account Settings Owner Invites + {{#if this.session.currentUser.admin}} + Site Admin + {{/if}}