implement /api/v1/crate/[crate]/owners endpoint (returning members with the MANAGE_USERS perm)
Diff
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(-)
@@ -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::<CrateVersion>(&conn)
.optional()?)
})
.await?
}
pub async fn owners(self: Arc<Self>, conn: ConnectionPool) -> Result<Vec<crate::users::User>> {
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::<crate::users::User>(&conn)?)
})
.await?
}
@@ -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<SqlType = diesel::sql_types::Integer> + Sized {
fn bitwise_and<T: AsExpression<diesel::sql_types::Integer>>(
self,
other: T,
) -> BitwiseAnd<Self, T::Expression> {
BitwiseAnd::new(self.as_expression(), other.as_expression())
}
}
impl<T: Expression<SqlType = diesel::sql_types::Integer>> BitwiseExpressionMethods for T {}
@@ -37,6 +37,8 @@
) -> Result<Option<User>> {
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<Self>,
conn: ConnectionPool,
crate_id: i32,
requested_permissions: UserCratePermissionValue,
) -> Result<bool> {
let perms = UserCratePermission::find(conn, self.id, crate_id)
.await?
.unwrap_or_default();
Ok(perms.permissions.contains(requested_permissions))
}
}
@@ -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))
@@ -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<axum::body::Bytes>;
type BodyError = <Self::Body as axum::body::HttpBody>::Error;
fn into_response(self) -> axum::http::Response<Self::Body> {
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
}
}
};
@@ -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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Vec<u8>, 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();
@@ -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;
@@ -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<GetResponseUser>,
}
#[derive(Serialize)]
pub struct GetResponseUser {
id: i32,
login: String,
name: Option<String>,
}
pub async fn handle_get(
extract::Path((_api_key, name)): extract::Path<(String, String)>,
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Json<GetResponse>, 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 }))
}