🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-09-09 19:35:58.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-09-09 19:35:58.0 +01:00:00
commit
b60dd4ea78d2ba2eac2ffd8492392eaf29056bf6 [patch]
tree
b5a4b9e16896302795923a070bf6293f902acd54
parent
f2c016701c7cd6803ae87e3e7e41a7b6f0e9d33c
download
b60dd4ea78d2ba2eac2ffd8492392eaf29056bf6.tar.gz

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(-)

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::<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?
    }
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<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 {}
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<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))
    }
}

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<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
            }
        }
    };
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<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();

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<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 }))
}