From b60dd4ea78d2ba2eac2ffd8492392eaf29056bf6 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Thu, 09 Sep 2021 19:35:58 +0100 Subject: [PATCH] implement /api/v1/crate/[crate]/owners endpoint (returning members with the MANAGE_USERS perm) --- chartered-db/src/crates.rs | 23 +++++++++++++++++++++++ chartered-db/src/lib.rs | 20 +++++++++++++++----- chartered-db/src/users.rs | 20 +++++++++++++++++--- chartered-web/src/main.rs | 5 ++++- chartered-web/src/endpoints/mod.rs | 19 ++++++++++++------- chartered-web/src/endpoints/cargo_api/download.rs | 25 +++++-------------------- chartered-web/src/endpoints/cargo_api/mod.rs | 22 ++++++++++++++++++++++ chartered-web/src/endpoints/cargo_api/owners.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 141 insertions(+), 40 deletions(-) diff --git a/chartered-db/src/crates.rs b/chartered-db/src/crates.rs index 76715af..666f97f 100644 --- a/chartered-db/src/crates.rs +++ a/chartered-db/src/crates.rs @@ -1,6 +1,6 @@ use super::{ schema::{crate_versions, crates}, - ConnectionPool, Result, + BitwiseExpressionMethods, ConnectionPool, Result, }; use diesel::{ insert_into, insert_or_ignore_into, prelude::*, Associations, Identifiable, Queryable, @@ -67,6 +67,27 @@ .filter(version.eq(crate_version)) .get_result::(&conn) .optional()?) + }) + .await? + } + + pub async fn owners(self: Arc, conn: ConnectionPool) -> Result> { + tokio::task::spawn_blocking(move || { + use crate::schema::user_crate_permissions::{ + dsl::permissions, dsl::user_crate_permissions, + }; + + let conn = conn.get()?; + + Ok(user_crate_permissions + .filter( + permissions + .bitwise_and(crate::users::UserCratePermissionValue::MANAGE_USERS.bits()) + .ne(0), + ) + .inner_join(crate::schema::users::dsl::users) + .select(crate::schema::users::all_columns) + .load::(&conn)?) }) .await? } diff --git a/chartered-db/src/lib.rs b/chartered-db/src/lib.rs index 564a2a4..59d0a71 100644 --- a/chartered-db/src/lib.rs +++ a/chartered-db/src/lib.rs @@ -8,7 +8,10 @@ #[macro_use] extern crate diesel; -use diesel::r2d2::{ConnectionManager, Pool}; +use diesel::{ + expression::{AsExpression, Expression}, + r2d2::{ConnectionManager, Pool}, +}; use displaydoc::Display; use std::sync::Arc; use thiserror::Error; @@ -30,10 +33,15 @@ TaskJoin(#[from] tokio::task::JoinError), } -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); +diesel_infix_operator!(BitwiseAnd, " & ", diesel::sql_types::Integer); + +trait BitwiseExpressionMethods: Expression + Sized { + fn bitwise_and>( + self, + other: T, + ) -> BitwiseAnd { + BitwiseAnd::new(self.as_expression(), other.as_expression()) } } + +impl> BitwiseExpressionMethods for T {} diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index 905ae65..a0c25dd 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -37,6 +37,8 @@ ) -> Result> { use crate::schema::user_ssh_keys::dsl::ssh_key; + eprintln!("ssh key: {:x?}", given_ssh_key); + tokio::task::spawn_blocking(move || { let conn = conn.get()?; @@ -61,13 +63,23 @@ Ok(UserCratePermission::belonging_to(&*self) .inner_join(crate::schema::crates::table) - .select(( - user_crate_permissions::permissions, - (crates::dsl::id, crates::dsl::name), - )) + .select((user_crate_permissions::permissions, crates::all_columns)) .load(&conn)?) }) .await? + } + + pub async fn has_crate_permission( + self: Arc, + conn: ConnectionPool, + crate_id: i32, + requested_permissions: UserCratePermissionValue, + ) -> Result { + let perms = UserCratePermission::find(conn, self.id, crate_id) + .await? + .unwrap_or_default(); + + Ok(perms.permissions.contains(requested_permissions)) } } diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs index 1ea1fee..de33976 100644 --- a/chartered-web/src/main.rs +++ a/chartered-web/src/main.rs @@ -39,7 +39,10 @@ let api_authenticated = axum_box_after_every_route!(Router::new() .route("/crates/new", put(endpoints::cargo_api::publish)) .route("/crates/search", get(hello_world)) - .route("/crates/:crate/owners", get(hello_world)) + .route( + "/crates/:crate/owners", + get(endpoints::cargo_api::get_owners) + ) .route("/crates/:crate/owners", put(hello_world)) .route("/crates/:crate/owners", delete(hello_world)) .route("/crates/:crate/:version/yank", delete(hello_world)) diff --git a/chartered-web/src/endpoints/mod.rs b/chartered-web/src/endpoints/mod.rs index b853cc8..700aed2 100644 --- a/chartered-web/src/endpoints/mod.rs +++ a/chartered-web/src/endpoints/mod.rs @@ -1,3 +1,8 @@ +#[derive(serde::Serialize)] +pub struct ErrorResponse { + error: &'static str, +} + macro_rules! define_error { ($($kind:ident$(($inner_name:ident: $inner:ty))? => $status:ident / $public_text:expr,)*) => { #[derive(thiserror::Error, Debug)] @@ -16,23 +21,21 @@ } impl axum::response::IntoResponse for Error { - type Body = axum::body::Body; + type Body = axum::body::Full; type BodyError = ::Error; fn into_response(self) -> axum::http::Response { - log::error!("Failed to handle request: {:?}", self); - let (status, body) = match self { $(Self::$kind$(($inner_name))? => ( axum::http::StatusCode::$status, - $public_text.into(), + serde_json::to_vec(&crate::endpoints::ErrorResponse { error: $public_text }).unwrap(), )),* }; - axum::http::Response::builder() - .status(status) - .body(body) - .unwrap() + let mut res = axum::http::Response::new(axum::body::Full::from(body)); + *res.status_mut() = status; + res.headers_mut().insert(axum::http::header::CONTENT_TYPE, axum::http::header::HeaderValue::from_static("application/json")); + res } } }; diff --git a/chartered-web/src/endpoints/cargo_api/download.rs b/chartered-web/src/endpoints/cargo_api/download.rs index 7f4e64a..99e1b74 100644 --- a/chartered-web/src/endpoints/cargo_api/download.rs +++ a/chartered-web/src/endpoints/cargo_api/download.rs @@ -1,7 +1,7 @@ use axum::extract; use chartered_db::{ crates::Crate, - users::{User, UserCratePermission, UserCratePermissionValue}, + users::{User, UserCratePermissionValue as Permission}, ConnectionPool, }; use chartered_fs::FileSystem; @@ -10,7 +10,7 @@ define_error!( Database(_e: chartered_db::Error) => INTERNAL_SERVER_ERROR / "Failed to query database", File(_e: std::io::Error) => INTERNAL_SERVER_ERROR / "Failed to fetch crate file", - NoVersion => NOT_FOUND / "That requested version does not exist for the crate", + NoVersion => NOT_FOUND / "The requested version does not exist for the crate", NoCrate => NOT_FOUND / "The requested crate does not exist", ); @@ -19,25 +19,10 @@ extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, ) -> Result, Error> { - let c = Crate::find_by_name(db.clone(), name) - .await? - .ok_or(Error::NoCrate)?; + let crate_ = get_crate!(db, name; || -> Error::NoCrate); + ensure_has_crate_perm!(db, user, crate_, Permission::VISIBLE; || -> Error::NoCrate); - let perms = UserCratePermission::find(db.clone(), user.id, c.id) - .await? - .unwrap_or_default(); - - if !perms - .permissions - .contains(UserCratePermissionValue::VISIBLE) - { - return Err(Error::NoCrate); - } - - let version = Arc::new(c) - .version(db, version) - .await? - .ok_or(Error::NoVersion)?; + let version = crate_.version(db, version).await?.ok_or(Error::NoVersion)?; let file_ref = chartered_fs::FileReference::from_str(&version.filesystem_object).unwrap(); diff --git a/chartered-web/src/endpoints/cargo_api/mod.rs b/chartered-web/src/endpoints/cargo_api/mod.rs index 113c927..ae07c91 100644 --- a/chartered-web/src/endpoints/cargo_api/mod.rs +++ a/chartered-web/src/endpoints/cargo_api/mod.rs @@ -1,5 +1,27 @@ +macro_rules! get_crate { + ($db:expr, $name:expr; || -> $error:expr) => { + Crate::find_by_name($db.clone(), $name) + .await? + .ok_or($error) + .map(std::sync::Arc::new)? + }; +} + +macro_rules! ensure_has_crate_perm { + ($db:expr, $user:expr, $crate_expr:expr, $permissions:expr; || -> $error:expr) => {{ + if !$user + .has_crate_permission($db.clone(), $crate_expr.id, $permissions) + .await? + { + return Err($error); + } + }}; +} + mod download; +mod owners; mod publish; pub use download::handle as download; +pub use owners::handle_get as get_owners; pub use publish::handle as publish; diff --git a/chartered-web/src/endpoints/cargo_api/owners.rs b/chartered-web/src/endpoints/cargo_api/owners.rs new file mode 100644 index 0000000..5f1191f 100644 --- /dev/null +++ a/chartered-web/src/endpoints/cargo_api/owners.rs @@ -1,0 +1,47 @@ +use axum::{extract, Json}; +use chartered_db::{ + crates::Crate, + users::{User, UserCratePermissionValue as Permission}, + ConnectionPool, +}; +use serde::Serialize; +use std::sync::Arc; + +define_error!( + Database(_e: chartered_db::Error) => INTERNAL_SERVER_ERROR / "Failed to query database", + NoCrate => NOT_FOUND / "The requested crate does not exist", +); + +#[derive(Serialize)] +pub struct GetResponse { + users: Vec, +} + +#[derive(Serialize)] +pub struct GetResponseUser { + id: i32, + login: String, + name: Option, +} + +pub async fn handle_get( + extract::Path((_api_key, name)): extract::Path<(String, String)>, + extract::Extension(db): extract::Extension, + extract::Extension(user): extract::Extension>, +) -> Result, Error> { + let crate_ = get_crate!(db, name; || -> Error::NoCrate); + ensure_has_crate_perm!(db, user, crate_, Permission::VISIBLE; || -> Error::NoCrate); + + let users = crate_ + .owners(db) + .await? + .into_iter() + .map(|user| GetResponseUser { + id: user.id, + login: user.username, + name: None, + }) + .collect(); + + Ok(Json(GetResponse { users })) +} -- rgit 0.1.3