From d27847cbb9f25dde54def0b480e60bee2761da1d Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Tue, 21 Sep 2021 02:51:52 +0100 Subject: [PATCH] Implement organisations functionality Lot of polishing off needs to be done here but makes a solid start on #18 permissions from organisations, but crate permissions can override these permissions (and include people from outside the organisation). Crates are also now auto-created on publish if the user has the CREATE_CRATE permission on the organisation 🎉 --- Cargo.lock | 24 ++++++++++++++++++++++++ chartered-db/Cargo.toml | 1 + chartered-git/Cargo.toml | 2 ++ chartered-db/src/crates.rs | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- chartered-db/src/lib.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++-------- chartered-db/src/schema.rs | 23 +++++++++++++++++++++++ chartered-db/src/users.rs | 11 ++++++++++- chartered-db/src/uuid.rs | 1 + chartered-frontend/src/index.tsx | 2 +- chartered-git/src/main.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- chartered-web/src/main.rs | 13 ++++++------- migrations/2021-08-31-214501_create_crates_table/down.sql | 4 +++- migrations/2021-08-31-214501_create_crates_table/up.sql | 45 ++++++++++++++++++++++++++++++++++----------- chartered-frontend/src/pages/Dashboard.tsx | 28 +++++++++++++++++++--------- chartered-web/src/models/crates.rs | 60 ------------------------------------------------------------ chartered-web/src/models/mod.rs | 1 - chartered-frontend/src/pages/crate/CrateView.tsx | 30 +++++++++++++++++++++++------- chartered-frontend/src/pages/crate/Members.tsx | 18 ++++++++++++++---- chartered-web/src/endpoints/cargo_api/download.rs | 29 ++++++++++++++++------------- chartered-web/src/endpoints/cargo_api/owners.rs | 22 +++++++--------------- chartered-web/src/endpoints/cargo_api/publish.rs | 39 +++++++++++++++++++++++++-------------- chartered-web/src/endpoints/cargo_api/yank.rs | 59 ++++++++++++++++++++++++++++------------------------------- chartered-web/src/endpoints/web_api/crates/info.rs | 31 +++++++++++++------------------ chartered-web/src/endpoints/web_api/crates/members.rs | 65 +++++++++++++++++++++++------------------------------------------ chartered-web/src/endpoints/web_api/crates/recently_updated.rs | 13 +++++-------- 25 files changed, 563 insertions(+), 321 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 344bbaa..e2a37e5 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -202,6 +202,7 @@ "displaydoc", "dotenv", "hex", + "http", "itertools", "option_set", "rand", @@ -240,11 +241,13 @@ "format-bytes", "futures", "hex", + "indoc", "itoa", "log", "serde", "serde_json", "sha-1", + "shlex", "thrussh", "thrussh-keys", "tokio", @@ -756,6 +759,15 @@ "tower-service", "tracing", "want", +] + +[[package]] +name = "indoc" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136" +dependencies = [ + "unindent", ] [[package]] @@ -1292,6 +1304,12 @@ "digest", "opaque-debug", ] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "signal-hook-registry" @@ -1625,6 +1643,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "unindent" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" [[package]] name = "uuid" diff --git a/chartered-db/Cargo.toml b/chartered-db/Cargo.toml index 62e8f9b..622ebc1 100644 --- a/chartered-db/Cargo.toml +++ a/chartered-db/Cargo.toml @@ -15,6 +15,7 @@ diesel = { version = "1", features = ["sqlite", "r2d2", "chrono"] } displaydoc = "0.2" hex = "0.4" +http = "0.2" itertools = "0.10" option_set = "0.1" rand = "0.8" diff --git a/chartered-git/Cargo.toml b/chartered-git/Cargo.toml index 041b4bd..0099cb6 100644 --- a/chartered-git/Cargo.toml +++ a/chartered-git/Cargo.toml @@ -20,10 +20,12 @@ format-bytes = "0.1" futures = "0.3" hex = "0.4" +indoc = "1.0" itoa = "0.4" log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" +shlex = "1" sha-1 = "0.9" thrussh = "0.33" thrussh-keys = "0.21" diff --git a/chartered-db/src/crates.rs b/chartered-db/src/crates.rs index 1e5de80..6157a79 100644 --- a/chartered-db/src/crates.rs +++ a/chartered-db/src/crates.rs @@ -1,18 +1,22 @@ -use crate::users::{User, UserCratePermission}; +use crate::users::{Organisation, User, UserCratePermission}; use super::{ - schema::{crate_versions, crates, users}, - BitwiseExpressionMethods, ConnectionPool, Result, + coalesce, + schema::{crate_versions, crates, organisations, users}, + users::UserCratePermissionValue as Permissions, + BitwiseExpressionMethods, ConnectionPool, Error, Result, }; use diesel::{insert_into, prelude::*, Associations, Identifiable, Queryable}; use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Arc}; -#[derive(Identifiable, Queryable, PartialEq, Eq, Hash, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)] +#[belongs_to(Organisation)] pub struct Crate { pub id: i32, pub name: String, + pub organisation_id: i32, pub readme: Option, pub description: Option, pub repository: Option, @@ -20,22 +24,61 @@ pub documentation: Option, } +macro_rules! crate_with_permissions { + ($user_id:ident) => { + crates::table + .left_join( + crate::schema::user_crate_permissions::table.on( + crate::schema::user_crate_permissions::dsl::user_id + .eq($user_id) + .and(crate::schema::user_crate_permissions::crate_id.eq(crates::id)), + ), + ) + .left_join( + crate::schema::user_organisation_permissions::table.on( + crate::schema::user_organisation_permissions::user_id + .eq($user_id) + .and( + crate::schema::user_organisation_permissions::organisation_id + .eq(crates::organisation_id), + ), + ), + ) + }; +} + +macro_rules! select_permissions { + () => { + coalesce( + crate::schema::user_crate_permissions::permissions.nullable(), + 0, + ) + .bitwise_or(coalesce( + crate::schema::user_organisation_permissions::permissions.nullable(), + 0, + )) + }; +} + impl Crate { - pub async fn all_visible_with_versions( + pub async fn list_with_versions( conn: ConnectionPool, - given_user_id: i32, + requesting_user_id: i32, + given_org_name: String, ) -> Result>>> { + use crate::schema::organisations::dsl::{name as org_name, organisations}; + tokio::task::spawn_blocking(move || { let conn = conn.get()?; - let crate_versions = crates::table - .inner_join(crate::schema::user_crate_permissions::table) + let crate_versions = crate_with_permissions!(requesting_user_id) + .inner_join(organisations) + .filter(org_name.eq(given_org_name)) .filter( - crate::schema::user_crate_permissions::permissions - .bitwise_and(crate::users::UserCratePermissionValue::VISIBLE.bits()) - .ne(0), + select_permissions!() + .bitwise_and(Permissions::VISIBLE.bits()) + .eq(Permissions::VISIBLE.bits()), ) - .filter(crate::schema::user_crate_permissions::dsl::user_id.eq(given_user_id)) .inner_join(crate_versions::table) .select((crates::all_columns, crate_versions::all_columns)) .load(&conn)?; @@ -47,23 +90,26 @@ pub async fn list_recently_updated( conn: ConnectionPool, - given_user_id: i32, - ) -> Result)>> { + requesting_user_id: i32, + ) -> Result, Organisation)>> { tokio::task::spawn_blocking(move || { let conn = conn.get()?; - let crates = crates::table - .inner_join(crate::schema::user_crate_permissions::table) + let crates = crate_with_permissions!(requesting_user_id) .filter( - crate::schema::user_crate_permissions::permissions - .bitwise_and(crate::users::UserCratePermissionValue::VISIBLE.bits()) - .ne(0), + select_permissions!() + .bitwise_and(Permissions::VISIBLE.bits()) + .eq(Permissions::VISIBLE.bits()), ) - .filter(crate::schema::user_crate_permissions::dsl::user_id.eq(given_user_id)) + .inner_join(organisations::table) .inner_join(crate_versions::table) - .order_by(crate::schema::crate_versions::dsl::id.desc()) - .select((crates::all_columns, crate_versions::all_columns)) + .select(( + crates::all_columns, + crate_versions::all_columns, + organisations::all_columns, + )) .limit(10) + .order_by(crate::schema::crate_versions::dsl::id.desc()) .load(&conn)?; Ok(crates) @@ -71,34 +117,95 @@ .await? } - pub async fn find_by_name(conn: ConnectionPool, crate_name: String) -> Result> { - use crate::schema::crates::dsl::{crates, name}; + pub async fn find_by_name( + conn: ConnectionPool, + requesting_user_id: i32, + given_org_name: String, + given_crate_name: String, + ) -> Result { + use crate::schema::crates::dsl::name as crate_name; + use crate::schema::organisations::dsl::{name as org_name, organisations}; tokio::task::spawn_blocking(move || { let conn = conn.get()?; - Ok(crates - .filter(name.eq(crate_name)) - .first::(&conn) - .optional()?) + let (crate_, permissions) = crate_with_permissions!(requesting_user_id) + .inner_join(organisations) + .filter(org_name.eq(given_org_name)) + .filter(crate_name.eq(given_crate_name)) + .select((crate::schema::crates::all_columns, select_permissions!())) + .first::<(Crate, Permissions)>(&conn) + .optional()? + .ok_or(Error::MissingCrate)?; + + if permissions.contains(Permissions::VISIBLE) { + Ok(CrateWithPermissions { + crate_, + permissions, + }) + } else { + Err(Error::MissingPermission(Permissions::VISIBLE)) + } }) .await? } - pub async fn versions_with_uploader( - self: Arc, + pub async fn create( conn: ConnectionPool, - ) -> Result, User)>> { + requesting_user_id: i32, + given_org_name: String, + given_crate_name: String, + ) -> Result { + use crate::schema::organisations::dsl::{id, name as org_name, organisations}; + use crate::schema::user_organisation_permissions::dsl::{ + organisation_id, permissions, user_id, + }; + tokio::task::spawn_blocking(move || { let conn = conn.get()?; - Ok(CrateVersion::belonging_to(&*self) - .inner_join(users::table) - .load::<(CrateVersion, User)>(&conn)?) + let (org_id, perms) = organisations + .filter(org_name.eq(given_org_name)) + .inner_join( + crate::schema::user_organisation_permissions::table + .on(organisation_id.eq(id).and(user_id.eq(requesting_user_id))), + ) + .select((id, permissions)) + .first::<(i32, Permissions)>(&conn)?; + + if !perms.contains(Permissions::VISIBLE) { + Err(Error::MissingPermission(Permissions::VISIBLE)) + } else if !perms.contains(Permissions::CREATE_CRATE) { + Err(Error::MissingPermission(Permissions::CREATE_CRATE)) + } else { + use crate::schema::crates::dsl::{crates, name, organisation_id}; + + insert_into(crates) + .values((name.eq(&given_crate_name), organisation_id.eq(org_id))) + .execute(&conn)?; + + let crate_ = crates + .filter(name.eq(given_crate_name).and(organisation_id.eq(org_id))) + .select(crate::schema::crates::all_columns) + .first::(&conn)?; + + Ok(CrateWithPermissions { + crate_, + permissions: perms, + }) + } }) .await? } +} + +#[derive(Debug)] +pub struct CrateWithPermissions { + pub crate_: Crate, + pub permissions: Permissions, +} +impl CrateWithPermissions { pub async fn version( self: Arc, conn: ConnectionPool, @@ -109,10 +216,24 @@ tokio::task::spawn_blocking(move || { let conn = conn.get()?; - Ok(CrateVersion::belonging_to(&*self) + Ok(CrateVersion::belonging_to(&self.crate_) .filter(version.eq(crate_version)) .get_result::(&conn) .optional()?) + }) + .await? + } + + pub async fn versions_with_uploader( + self: Arc, + conn: ConnectionPool, + ) -> Result, User)>> { + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + Ok(CrateVersion::belonging_to(&self.crate_) + .inner_join(users::table) + .load::<(CrateVersion, User)>(&conn)?) }) .await? } @@ -123,7 +244,7 @@ let conn = conn.get()?; - Ok(UserCratePermission::belonging_to(&*self) + Ok(UserCratePermission::belonging_to(&self.crate_) .filter( permissions .bitwise_and(crate::users::UserCratePermissionValue::MANAGE_USERS.bits()) @@ -140,10 +261,14 @@ self: Arc, conn: ConnectionPool, ) -> Result> { + if !self.permissions.contains(Permissions::MANAGE_USERS) { + return Err(Error::MissingPermission(Permissions::MANAGE_USERS)); + } + tokio::task::spawn_blocking(move || { let conn = conn.get()?; - Ok(UserCratePermission::belonging_to(&*self) + Ok(UserCratePermission::belonging_to(&self.crate_) .inner_join(crate::schema::users::dsl::users) .select(( crate::schema::users::all_columns, @@ -160,6 +285,10 @@ given_user_id: i32, given_permissions: crate::users::UserCratePermissionValue, ) -> Result { + if !self.permissions.contains(Permissions::MANAGE_USERS) { + return Err(Error::MissingPermission(Permissions::MANAGE_USERS)); + } + tokio::task::spawn_blocking(move || { use crate::schema::user_crate_permissions::dsl::{ crate_id, permissions, user_crate_permissions, user_id, @@ -170,7 +299,7 @@ Ok(diesel::update( user_crate_permissions .filter(user_id.eq(given_user_id)) - .filter(crate_id.eq(self.id)), + .filter(crate_id.eq(self.crate_.id)), ) .set(permissions.eq(given_permissions.bits())) .execute(&conn)?) @@ -184,6 +313,10 @@ given_user_id: i32, given_permissions: crate::users::UserCratePermissionValue, ) -> Result { + if !self.permissions.contains(Permissions::MANAGE_USERS) { + return Err(Error::MissingPermission(Permissions::MANAGE_USERS)); + } + tokio::task::spawn_blocking(move || { use crate::schema::user_crate_permissions::dsl::{ crate_id, permissions, user_crate_permissions, user_id, @@ -194,7 +327,7 @@ Ok(diesel::insert_into(user_crate_permissions) .values(( user_id.eq(given_user_id), - crate_id.eq(self.id), + crate_id.eq(self.crate_.id), permissions.eq(given_permissions.bits()), )) .execute(&conn)?) @@ -207,6 +340,10 @@ conn: ConnectionPool, given_user_id: i32, ) -> Result<()> { + if !self.permissions.contains(Permissions::MANAGE_USERS) { + return Err(Error::MissingPermission(Permissions::MANAGE_USERS)); + } + tokio::task::spawn_blocking(move || { use crate::schema::user_crate_permissions::dsl::{ crate_id, user_crate_permissions, user_id, @@ -217,7 +354,7 @@ diesel::delete( user_crate_permissions .filter(user_id.eq(given_user_id)) - .filter(crate_id.eq(self.id)), + .filter(crate_id.eq(self.crate_.id)), ) .execute(&conn)?; @@ -226,6 +363,7 @@ .await? } + #[allow(clippy::too_many_arguments)] pub async fn publish_version( self: Arc, conn: ConnectionPool, @@ -241,15 +379,20 @@ size, user_id, version, }; use crate::schema::crates::dsl::{ - crates, description, documentation, homepage, id, readme, repository, + crates, description, documentation, homepage, id, name, readme, repository, }; + if !self.permissions.contains(Permissions::PUBLISH_VERSION) { + return Err(Error::MissingPermission(Permissions::PUBLISH_VERSION)); + } + tokio::task::spawn_blocking(move || { let conn = conn.get()?; conn.transaction::<_, crate::Error, _>(|| { - diesel::update(crates.filter(id.eq(self.id))) + diesel::update(crates.filter(id.eq(self.crate_.id))) .set(( + name.eq(given.name), description.eq(metadata.description), readme.eq(metadata.readme), repository.eq(metadata.repository), @@ -260,7 +403,7 @@ insert_into(crate_versions) .values(( - crate_id.eq(self.id), + crate_id.eq(self.crate_.id), filesystem_object.eq(file_identifier.to_string()), size.eq(file_size), checksum.eq(file_checksum), @@ -287,13 +430,17 @@ yank: bool, ) -> Result<()> { use crate::schema::crate_versions::dsl::{crate_id, crate_versions, version, yanked}; + + if !self.permissions.contains(Permissions::YANK_VERSION) { + return Err(Error::MissingPermission(Permissions::YANK_VERSION)); + } tokio::task::spawn_blocking(move || { let conn = conn.get()?; diesel::update( crate_versions - .filter(crate_id.eq(self.id)) + .filter(crate_id.eq(self.crate_.id)) .filter(version.eq(given_version)), ) .set(yanked.eq(yank)) diff --git a/chartered-db/src/lib.rs b/chartered-db/src/lib.rs index 45aeffb..8648b14 100644 --- a/chartered-db/src/lib.rs +++ a/chartered-db/src/lib.rs @@ -1,5 +1,7 @@ #![deny(clippy::pedantic)] #![allow(clippy::missing_errors_doc)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::doc_markdown)] // `sql_function` fails this check macro_rules! derive_diesel_json { ($typ:ident$(<$lt:lifetime>)?) => { @@ -38,8 +40,9 @@ extern crate diesel; use diesel::{ - expression::{AsExpression, Expression}, + expression::{grouped::Grouped, AsExpression, Expression}, r2d2::{ConnectionManager, Pool}, + sql_types::{Integer, Nullable}, }; use displaydoc::Display; use std::sync::Arc; @@ -54,25 +57,56 @@ #[derive(Error, Display, Debug)] pub enum Error { - /// Failed to initialise to database connection pool: `{0}` + /// Failed to initialise to database connection pool Connection(#[from] diesel::r2d2::PoolError), - /// Failed to run query: `{0}` + /// Failed to run query Query(#[from] diesel::result::Error), - /// Failed to complete query task: `{0}` + /// Failed to complete query task TaskJoin(#[from] tokio::task::JoinError), /// Key parse failure: `{0}` KeyParse(#[from] thrussh_keys::Error), + /// You don't have the {0:?} permission for this crate + MissingPermission(crate::users::UserCratePermissionValue), + /// The requested crate does not exist + MissingCrate, } -diesel_infix_operator!(BitwiseAnd, " & ", diesel::sql_types::Integer); +impl Error { + #[must_use] + pub fn status_code(&self) -> http::StatusCode { + match self { + Self::MissingCrate => http::StatusCode::NOT_FOUND, + Self::MissingPermission(v) + if v.contains(crate::users::UserCratePermissionValue::VISIBLE) => + { + http::StatusCode::NOT_FOUND + } + Self::MissingPermission(_) => http::StatusCode::FORBIDDEN, + Self::KeyParse(_) => http::StatusCode::BAD_REQUEST, + _ => http::StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +sql_function!(fn coalesce(x: Nullable, y: Integer) -> Integer); + +diesel_infix_operator!(BitwiseAnd, " & ", Integer); +diesel_infix_operator!(BitwiseOr, " | ", Integer); + +trait BitwiseExpressionMethods: Expression + Sized { + fn bitwise_and>( + self, + other: T, + ) -> Grouped> { + Grouped(BitwiseAnd::new(self.as_expression(), other.as_expression())) + } -trait BitwiseExpressionMethods: Expression + Sized { - fn bitwise_and>( + fn bitwise_or>( self, other: T, - ) -> BitwiseAnd { - BitwiseAnd::new(self.as_expression(), other.as_expression()) + ) -> Grouped> { + Grouped(BitwiseOr::new(self.as_expression(), other.as_expression())) } } -impl> BitwiseExpressionMethods for T {} +impl> BitwiseExpressionMethods for T {} diff --git a/chartered-db/src/schema.rs b/chartered-db/src/schema.rs index 1b3e311..35ae527 100644 --- a/chartered-db/src/schema.rs +++ a/chartered-db/src/schema.rs @@ -19,6 +19,7 @@ crates (id) { id -> Integer, name -> Text, + organisation_id -> Integer, readme -> Nullable, description -> Nullable, repository -> Nullable, @@ -28,10 +29,27 @@ } table! { + organisations (id) { + id -> Integer, + uuid -> Binary, + name -> Text, + } +} + +table! { user_crate_permissions (id) { id -> Integer, user_id -> Integer, crate_id -> Integer, + permissions -> Integer, + } +} + +table! { + user_organisation_permissions (id) { + id -> Integer, + user_id -> Integer, + organisation_id -> Integer, permissions -> Integer, } } @@ -70,8 +88,11 @@ joinable!(crate_versions -> crates (crate_id)); joinable!(crate_versions -> users (user_id)); +joinable!(crates -> organisations (organisation_id)); joinable!(user_crate_permissions -> crates (crate_id)); joinable!(user_crate_permissions -> users (user_id)); +joinable!(user_organisation_permissions -> organisations (organisation_id)); +joinable!(user_organisation_permissions -> users (user_id)); joinable!(user_sessions -> user_ssh_keys (user_ssh_key_id)); joinable!(user_sessions -> users (user_id)); joinable!(user_ssh_keys -> users (user_id)); @@ -79,7 +100,9 @@ allow_tables_to_appear_in_same_query!( crate_versions, crates, + organisations, user_crate_permissions, + user_organisation_permissions, user_sessions, user_ssh_keys, users, diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index 2fc1cef..6743bbb 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -1,5 +1,5 @@ use super::{ - schema::{user_crate_permissions, user_sessions, user_ssh_keys, users}, + schema::{organisations, user_crate_permissions, user_sessions, user_ssh_keys, users}, uuid::SqlUuid, ConnectionPool, Result, }; @@ -11,6 +11,13 @@ use thrussh_keys::PublicKeyBase64; #[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)] +pub struct Organisation { + pub id: i32, + pub uuid: SqlUuid, + pub name: String, +} + +#[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)] pub struct User { pub id: i32, pub uuid: SqlUuid, @@ -279,10 +286,12 @@ const PUBLISH_VERSION = 0b0000_0000_0000_0000_0000_0000_0000_0010; const YANK_VERSION = 0b0000_0000_0000_0000_0000_0000_0000_0100; const MANAGE_USERS = 0b0000_0000_0000_0000_0000_0000_0000_1000; + const CREATE_CRATE = 0b0000_0000_0000_0000_0000_0000_0001_0000; } } impl UserCratePermissionValue { + #[must_use] pub fn names() -> &'static [&'static str] { Self::NAMES } diff --git a/chartered-db/src/uuid.rs b/chartered-db/src/uuid.rs index bf3771a..19d8c2e 100644 --- a/chartered-db/src/uuid.rs +++ a/chartered-db/src/uuid.rs @@ -9,6 +9,7 @@ pub struct SqlUuid(pub uuid::Uuid); impl SqlUuid { + #[must_use] pub fn random() -> Self { Self(uuid::Uuid::new_v4()) } diff --git a/chartered-frontend/src/index.tsx b/chartered-frontend/src/index.tsx index 4d2d157..09f1e20 100644 --- a/chartered-frontend/src/index.tsx +++ a/chartered-frontend/src/index.tsx @@ -44,7 +44,7 @@ /> } /> , user_ssh_key: Option>, + organisation: Option, } impl Handler { @@ -91,6 +93,13 @@ } } + fn org_name(&self) -> Result<&str, anyhow::Error> { + match self.organisation { + Some(ref org) => Ok(org.as_str()), + None => anyhow::bail!("org not set after auth"), + } + } + fn user_ssh_key(&self) -> Result<&Arc, anyhow::Error> { match self.user_ssh_key { Some(ref ssh_key) => Ok(ssh_key), @@ -136,28 +145,43 @@ data: &[u8], mut session: Session, ) -> Self::FutureUnit { - eprintln!("exec {:x?}", data); + let data = match std::str::from_utf8(data) { + Ok(data) => data, + Err(e) => return Box::pin(futures::future::err(e.into())), + }; + let args = shlex::split(data); - let git_upload_pack = data.starts_with(b"git-upload-pack "); - Box::pin(async move { - if git_upload_pack { - // TODO: check GIT_PROTOCOL=version=2 set - self.write(PktLine::Data(b"version 2\n"))?; - self.write(PktLine::Data(b"agent=chartered/0.1.0\n"))?; - self.write(PktLine::Data(b"ls-refs=unborn\n"))?; - self.write(PktLine::Data(b"fetch=shallow wait-for-done\n"))?; - self.write(PktLine::Data(b"server-option\n"))?; - self.write(PktLine::Data(b"object-info\n"))?; - self.write(PktLine::Flush)?; - self.flush(&mut session, channel); + let mut args = args.into_iter().map(|v| v.into_iter()).flatten(); + + if args.next().as_deref() != Some("git-upload-pack") { + anyhow::bail!("not git-upload-pack"); + } + + if let Some(org) = args.next().filter(|v| v.as_str() != "/") { + let org = org + .trim_start_matches('/') + .trim_end_matches('/') + .to_string(); + self.organisation = Some(org); } else { - session.data( - channel, - CryptoVec::from_slice(b"Sorry, I have no clue who you are\r\n"), - ); + session.extended_data(channel, 1, CryptoVec::from_slice(indoc::indoc! {b" + \r\nNo organisation was given in the path part of the SSH URI. A chartered registry should be defined in your .cargo/config.toml as follows: + [registries] + chartered = {{ index = \"ssh://domain.to.registry.com/my-organisation\" }}\r\n + "})); session.close(channel); } + + // TODO: check GIT_PROTOCOL=version=2 set + self.write(PktLine::Data(b"version 2\n"))?; + self.write(PktLine::Data(b"agent=chartered/0.1.0\n"))?; + self.write(PktLine::Data(b"ls-refs=unborn\n"))?; + self.write(PktLine::Data(b"fetch=shallow wait-for-done\n"))?; + self.write(PktLine::Data(b"server-option\n"))?; + self.write(PktLine::Data(b"object-info\n"))?; + self.write(PktLine::Flush)?; + self.flush(&mut session, channel); Ok((self, session)) }) @@ -256,13 +280,14 @@ // TODO: key should be cached let config = format!( - r#"{{"dl":"http://127.0.0.1:8888/a/{key}/api/v1/crates","api":"http://127.0.0.1:8888/a/{key}"}}"#, + r#"{{"dl":"http://127.0.0.1:8888/a/{key}/o/{organisation}/api/v1/crates","api":"http://127.0.0.1:8888/a/{key}/o/{organisation}"}}"#, key = self .user_ssh_key()? .clone() .get_or_insert_session(self.db.clone(), self.ip.map(|v| v.to_string())) .await? .session_key, + organisation = self.org_name()?, ); let config_file = PackFileEntry::Blob(config.as_bytes()); @@ -275,7 +300,12 @@ // todo: the whole tree needs caching and then we can filter in code rather than at // the database - let tree = fetch_tree(self.db.clone(), self.user()?.id).await; + let tree = fetch_tree( + self.db.clone(), + self.user()?.id, + self.org_name()?.to_string(), + ) + .await; build_tree(&mut root_tree, &mut pack_file_entries, &tree)?; let root_tree = PackFileEntry::Tree(root_tree); @@ -356,13 +386,17 @@ async fn fetch_tree( db: chartered_db::ConnectionPool, user_id: i32, + org_name: String, ) -> TwoCharTree>> { use chartered_db::crates::Crate; let mut tree: TwoCharTree>> = BTreeMap::new(); // todo: handle files with 1/2/3 characters - for (crate_def, versions) in Crate::all_visible_with_versions(db, user_id).await.unwrap() { + for (crate_def, versions) in Crate::list_with_versions(db, user_id, org_name) + .await + .unwrap() + { let mut name_chars = crate_def.name.as_bytes().iter(); let first_dir = [*name_chars.next().unwrap(), *name_chars.next().unwrap()]; let second_dir = [*name_chars.next().unwrap(), *name_chars.next().unwrap()]; diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs index 1cfe97a..7e656b7 100644 --- a/chartered-web/src/main.rs +++ a/chartered-web/src/main.rs @@ -1,9 +1,8 @@ #![deny(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] mod endpoints; mod middleware; -mod models; use axum::{ handler::{delete, get, patch, post, put}, @@ -70,21 +69,21 @@ axum_box_after_every_route!(Router::new().route("/login", post(endpoints::web_api::login))); let web_authenticated = axum_box_after_every_route!(Router::new() - .route("/crates/:crate", get(endpoints::web_api::crates::info)) + .route("/crates/:org/:crate", get(endpoints::web_api::crates::info)) .route( - "/crates/:crate/members", + "/crates/:org/:crate/members", get(endpoints::web_api::crates::get_members) ) .route( - "/crates/:crate/members", + "/crates/:org/:crate/members", patch(endpoints::web_api::crates::update_member) ) .route( - "/crates/:crate/members", + "/crates/:org/:crate/members", put(endpoints::web_api::crates::insert_member) ) .route( - "/crates/:crate/members", + "/crates/:org/:crate/members", delete(endpoints::web_api::crates::delete_member) ) .route( @@ -109,7 +108,7 @@ .route("/", get(hello_world)) .nest("/a/:key/web/v1", web_authenticated) .nest("/a/-/web/v1", web_unauthenticated) - .nest("/a/:key/api/v1", api_authenticated) + .nest("/a/:key/o/:organisation/api/v1", api_authenticated) .layer(middleware_stack) // TODO!!! .layer( diff --git a/migrations/2021-08-31-214501_create_crates_table/down.sql b/migrations/2021-08-31-214501_create_crates_table/down.sql index 632c629..183c37f 100644 --- a/migrations/2021-08-31-214501_create_crates_table/down.sql +++ a/migrations/2021-08-31-214501_create_crates_table/down.sql @@ -1,6 +1,8 @@ +DROP TABLE organisations; DROP TABLE crates; DROP TABLE crate_versions; DROP TABLE users; +DROP TABLE user_organisation_permissions; +DROP TABLE user_crate_permissions; DROP TABLE user_ssh_keys; DROP TABLE user_sessions; -DROP TABLE user_crate_permissions; diff --git a/migrations/2021-08-31-214501_create_crates_table/up.sql b/migrations/2021-08-31-214501_create_crates_table/up.sql index 5277879..0b4d1e0 100644 --- a/migrations/2021-08-31-214501_create_crates_table/up.sql +++ a/migrations/2021-08-31-214501_create_crates_table/up.sql @@ -1,11 +1,22 @@ +CREATE TABLE organisations ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + uuid BINARY(128) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL UNIQUE +); + +INSERT INTO organisations (id, uuid, name) VALUES (1, X'936DA01F9ABD4D9D80C702AF85C822A8', "core"); + CREATE TABLE crates ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - name VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + organisation_id INTEGER NOT NULL, readme TEXT, description VARCHAR(255), repository VARCHAR(255), homepage VARCHAR(255), - documentation VARCHAR(255) + documentation VARCHAR(255), + UNIQUE (name, organisation_id), + FOREIGN KEY (organisation_id) REFERENCES organisations (id) ); CREATE TABLE crate_versions ( @@ -33,6 +44,26 @@ ); INSERT INTO users (id, uuid, username) VALUES (1, X'936DA01F9ABD4D9D80C702AF85C822A8', "admin"); + +CREATE TABLE user_organisation_permissions ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + organisation_id INTEGER NOT NULL, + permissions INTEGER NOT NULL, + UNIQUE (user_id, organisation_id), + FOREIGN KEY (user_id) REFERENCES users (id) + FOREIGN KEY (organisation_id) REFERENCES organisations (id) +); + +CREATE TABLE user_crate_permissions ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + crate_id INTEGER NOT NULL, + permissions INTEGER NOT NULL, + UNIQUE (user_id, crate_id), + FOREIGN KEY (user_id) REFERENCES users (id) + FOREIGN KEY (crate_id) REFERENCES crates (id) +); CREATE TABLE user_ssh_keys ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -55,14 +86,4 @@ ip VARCHAR(255), FOREIGN KEY (user_id) REFERENCES users (id) FOREIGN KEY (user_ssh_key_id) REFERENCES user_ssh_keys (id) -); - -CREATE TABLE user_crate_permissions ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - crate_id INTEGER NOT NULL, - permissions INTEGER NOT NULL, - UNIQUE (user_id, crate_id), - FOREIGN KEY (user_id) REFERENCES users (id) - FOREIGN KEY (crate_id) REFERENCES crates (id) ); diff --git a/chartered-frontend/src/pages/Dashboard.tsx b/chartered-frontend/src/pages/Dashboard.tsx index e8aae92..bc5deff 100644 --- a/chartered-frontend/src/pages/Dashboard.tsx +++ a/chartered-frontend/src/pages/Dashboard.tsx @@ -7,20 +7,23 @@ import { useAuthenticatedRequest } from "../util"; interface RecentlyUpdatedResponse { - versions: RecentlyUpdatedResponseVersions, + versions: RecentlyUpdatedResponseVersion[]; } -interface RecentlyUpdatedResponseVersions { - [i: number]: { name: string, version: string, }, +interface RecentlyUpdatedResponseVersion { + name: string; + version: string; + organisation: string; } export default function Dashboard() { const auth = useAuth(); - const { response: recentlyUpdated, error } = useAuthenticatedRequest({ - auth, - endpoint: "crates/recently-updated", - }); + const { response: recentlyUpdated, error } = + useAuthenticatedRequest({ + auth, + endpoint: "crates/recently-updated", + }); return (
@@ -67,15 +70,22 @@ interface Crate { name: string; version: string; + organisation: string; } function CrateCard({ crate }: { crate: Crate }) { return ( - +
-
{crate.name}
+
+ {crate.organisation}/ + {crate.name} +
v{crate.version}
diff --git a/chartered-web/src/models/crates.rs b/chartered-web/src/models/crates.rs deleted file mode 100644 index 60f8b61..0000000 100644 --- a/chartered-web/src/models/crates.rs +++ /dev/null @@ -1,60 +1,0 @@ -use axum::http::StatusCode; -use chartered_db::{ - crates::Crate, - users::{User, UserCratePermissionValue as Permission}, - ConnectionPool, -}; -use std::sync::Arc; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum CrateFetchError { - NotFound, - MissingPermission(chartered_db::users::UserCratePermissionValue), - Database(#[from] chartered_db::Error), -} - -impl CrateFetchError { - pub fn status_code(&self) -> StatusCode { - match self { - Self::NotFound | Self::MissingPermission(Permission::VISIBLE) => StatusCode::NOT_FOUND, - Self::MissingPermission(_) => StatusCode::FORBIDDEN, - Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl std::fmt::Display for CrateFetchError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::NotFound | Self::MissingPermission(Permission::VISIBLE) => { - write!(f, "The requested crate does not exist") - } - Self::MissingPermission(v) => { - write!(f, "You don't have the {:?} permission for this crate", v) - } - Self::Database(_) => write!(f, "An error occurred while fetching the crate."), - } - } -} - -pub async fn get_crate_with_permissions( - db: ConnectionPool, - user: Arc, - crate_name: String, - required_permissions: &[chartered_db::users::UserCratePermissionValue], -) -> Result, CrateFetchError> { - let crate_ = Crate::find_by_name(db.clone(), crate_name) - .await? - .ok_or(CrateFetchError::NotFound) - .map(std::sync::Arc::new)?; - let has_permissions = user.get_crate_permissions(db, crate_.id).await?; - - for required_permission in required_permissions { - if !has_permissions.contains(*required_permission) { - return Err(CrateFetchError::MissingPermission(*required_permission)); - } - } - - Ok(crate_) -} diff --git a/chartered-web/src/models/mod.rs b/chartered-web/src/models/mod.rs deleted file mode 100644 index 30e78d1..0000000 100644 --- a/chartered-web/src/models/mod.rs +++ /dev/null @@ -1,1 +1,0 @@ -pub mod crates; diff --git a/chartered-frontend/src/pages/crate/CrateView.tsx b/chartered-frontend/src/pages/crate/CrateView.tsx index edff414..f5e299d 100644 --- a/chartered-frontend/src/pages/crate/CrateView.tsx +++ a/chartered-frontend/src/pages/crate/CrateView.tsx @@ -56,16 +56,16 @@ export default function SingleCrate() { const auth = useAuth(); - const { crate, subview } = useParams(); + const { organisation, crate, subview } = useParams(); const currentTab: Tab | undefined = subview; if (!currentTab) { - return + return ; } const { response: crateInfo, error } = useAuthenticatedRequest({ auth, - endpoint: `crates/${crate}`, + endpoint: `crates/${organisation}/${crate}`, }); if (error) { @@ -141,12 +141,20 @@
  • - + Readme
  • - + Versions {crateInfo.versions.length} @@ -154,7 +162,11 @@
  • - + Members
  • @@ -168,7 +180,11 @@ ) : ( <> )} - {currentTab == "members" ? : <>} + {currentTab == "members" ? ( + + ) : ( + <> + )}
diff --git a/chartered-frontend/src/pages/crate/Members.tsx b/chartered-frontend/src/pages/crate/Members.tsx index e9742de..6e0fa3d 100644 --- a/chartered-frontend/src/pages/crate/Members.tsx +++ a/chartered-frontend/src/pages/crate/Members.tsx @@ -25,13 +25,19 @@ permissions: string[]; } -export default function Members({ crate }: { crate: string }) { +export default function Members({ + organisation, + crate, +}: { + organisation: string; + crate: string; +}) { const auth = useAuth(); const [reload, setReload] = useState(0); const { response, error } = useAuthenticatedRequest( { auth, - endpoint: `crates/${crate}/members`, + endpoint: `crates/${organisation}/${crate}/members`, }, [reload] ); @@ -72,6 +78,7 @@ {response.members.map((member, index) => ( ( StatusCode::INTERNAL_SERVER_ERROR, + Self::Database(e) => e.status_code(), + Self::File(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NoVersion => StatusCode::NOT_FOUND, - Self::CrateFetch(e) => e.status_code(), } } } @@ -35,13 +29,22 @@ define_error_response!(Error); pub async fn handle( - extract::Path((_session_key, name, version)): extract::Path<(String, String, String)>, + extract::Path((_session_key, name, organisation, version)): extract::Path<( + String, + String, + String, + String, + )>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, ) -> Result, Error> { - let crate_ = get_crate_with_permissions(db.clone(), user, name, &[Permission::VISIBLE]).await?; + let crate_with_permissions = + Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); - let version = crate_.version(db, version).await?.ok_or(Error::NoVersion)?; + let version = crate_with_permissions + .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/owners.rs b/chartered-web/src/endpoints/cargo_api/owners.rs index 5fe7e32..35a51e4 100644 --- a/chartered-web/src/endpoints/cargo_api/owners.rs +++ a/chartered-web/src/endpoints/cargo_api/owners.rs @@ -1,28 +1,19 @@ -use crate::models::crates::get_crate_with_permissions; use axum::{extract, Json}; -use chartered_db::{ - users::{User, UserCratePermissionValue as Permission}, - ConnectionPool, -}; +use chartered_db::{crates::Crate, users::User, ConnectionPool}; use serde::Serialize; use std::sync::Arc; use thiserror::Error; #[derive(Error, Debug)] pub enum Error { - #[error("Failed to query database")] - Database(#[from] chartered_db::Error), #[error("{0}")] - CrateFetch(#[from] crate::models::crates::CrateFetchError), + Database(#[from] chartered_db::Error), } impl Error { pub fn status_code(&self) -> axum::http::StatusCode { - use axum::http::StatusCode; - match self { - Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::CrateFetch(e) => e.status_code(), + Self::Database(e) => e.status_code(), } } } @@ -43,13 +34,14 @@ } pub async fn handle_get( - extract::Path((_session_key, name)): extract::Path<(String, String)>, + extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, ) -> Result, Error> { - let crate_ = get_crate_with_permissions(db.clone(), user, name, &[Permission::VISIBLE]).await?; + let crate_with_permissions = + Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); - let users = crate_ + let users = crate_with_permissions .owners(db) .await? .into_iter() diff --git a/chartered-web/src/endpoints/cargo_api/publish.rs b/chartered-web/src/endpoints/cargo_api/publish.rs index 8adc936..72eebb0 100644 --- a/chartered-web/src/endpoints/cargo_api/publish.rs +++ a/chartered-web/src/endpoints/cargo_api/publish.rs @@ -1,10 +1,6 @@ -use crate::models::crates::get_crate_with_permissions; use axum::extract; use bytes::Bytes; -use chartered_db::{ - users::{User, UserCratePermissionValue as Permission}, - ConnectionPool, -}; +use chartered_db::{crates::Crate, users::User, ConnectionPool}; use chartered_fs::FileSystem; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -13,10 +9,8 @@ #[derive(Error, Debug)] pub enum Error { - #[error("Failed to query database")] - Database(#[from] chartered_db::Error), #[error("{0}")] - CrateFetch(#[from] crate::models::crates::CrateFetchError), + Database(#[from] chartered_db::Error), #[error("Invalid JSON from client: {0}")] JsonParse(#[from] serde_json::Error), #[error("Invalid body")] @@ -28,8 +22,7 @@ use axum::http::StatusCode; match self { - Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::CrateFetch(e) => e.status_code(), + Self::Database(e) => e.status_code(), Self::JsonParse(_) | Self::MetadataParse => StatusCode::BAD_REQUEST, } } @@ -50,6 +43,7 @@ } pub async fn handle( + extract::Path((_session_key, organisation)): extract::Path<(String, String)>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, body: Bytes, @@ -58,17 +52,32 @@ parse(body.as_ref()).map_err(|_| Error::MetadataParse)?; let metadata: Metadata = serde_json::from_slice(metadata_bytes)?; - let crate_ = get_crate_with_permissions( + let crate_with_permissions = Crate::find_by_name( db.clone(), - user.clone(), + user.id, + organisation.clone(), metadata.inner.name.to_string(), - &[Permission::VISIBLE, Permission::PUBLISH_VERSION], ) - .await?; + .await; + + let crate_with_permissions = match crate_with_permissions { + Ok(v) => Arc::new(v), + Err(chartered_db::Error::MissingCrate) => { + let new_crate = Crate::create( + db.clone(), + user.id, + organisation, + metadata.inner.name.to_string(), + ) + .await?; + Arc::new(new_crate) + } + Err(e) => return Err(e.into()), + }; let file_ref = chartered_fs::Local.write(crate_bytes).await.unwrap(); - crate_ + crate_with_permissions .publish_version( db, user, diff --git a/chartered-web/src/endpoints/cargo_api/yank.rs b/chartered-web/src/endpoints/cargo_api/yank.rs index 7346aa9..58175b8 100644 --- a/chartered-web/src/endpoints/cargo_api/yank.rs +++ a/chartered-web/src/endpoints/cargo_api/yank.rs @@ -1,28 +1,19 @@ -use crate::models::crates::get_crate_with_permissions; use axum::{extract, Json}; -use chartered_db::{ - users::{User, UserCratePermissionValue as Permission}, - ConnectionPool, -}; +use chartered_db::{crates::Crate, users::User, ConnectionPool}; use serde::Serialize; use std::sync::Arc; use thiserror::Error; #[derive(Error, Debug)] pub enum Error { - #[error("Failed to query database")] - Database(#[from] chartered_db::Error), #[error("{0}")] - CrateFetch(#[from] crate::models::crates::CrateFetchError), + Database(#[from] chartered_db::Error), } impl Error { pub fn status_code(&self) -> axum::http::StatusCode { - use axum::http::StatusCode; - match self { - Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::CrateFetch(e) => e.status_code(), + Self::Database(e) => e.status_code(), } } } @@ -35,37 +26,41 @@ } pub async fn handle_yank( - extract::Path((_session_key, name, version)): extract::Path<(String, String, String)>, + extract::Path((_session_key, name, organisation, version)): extract::Path<( + String, + String, + String, + String, + )>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, ) -> Result, Error> { - let crate_ = get_crate_with_permissions( - db.clone(), - user, - name, - &[Permission::VISIBLE, Permission::YANK_VERSION], - ) - .await?; - - crate_.yank_version(db, version, true).await?; + let crate_with_permissions = + Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); + + crate_with_permissions + .yank_version(db, version, true) + .await?; Ok(Json(Response { ok: true })) } pub async fn handle_unyank( - extract::Path((_session_key, name, version)): extract::Path<(String, String, String)>, + extract::Path((_session_key, name, organisation, version)): extract::Path<( + String, + String, + String, + String, + )>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, ) -> Result, Error> { - let crate_ = get_crate_with_permissions( - db.clone(), - user, - name, - &[Permission::VISIBLE, Permission::YANK_VERSION], - ) - .await?; - - crate_.yank_version(db, version, false).await?; + let crate_with_permissions = + Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); + + crate_with_permissions + .yank_version(db, version, false) + .await?; Ok(Json(Response { ok: true })) } diff --git a/chartered-web/src/endpoints/web_api/crates/info.rs b/chartered-web/src/endpoints/web_api/crates/info.rs index 8b85db6..8e3455b 100644 --- a/chartered-web/src/endpoints/web_api/crates/info.rs +++ a/chartered-web/src/endpoints/web_api/crates/info.rs @@ -1,11 +1,6 @@ -use crate::models::crates::get_crate_with_permissions; use axum::{body::Full, extract, response::IntoResponse, Json}; use bytes::Bytes; -use chartered_db::{ - crates::Crate, - users::{User, UserCratePermissionValue as Permission}, - ConnectionPool, -}; +use chartered_db::{crates::Crate, users::User, ConnectionPool}; use chartered_types::cargo::CrateVersion; use chrono::TimeZone; use serde::Serialize; @@ -14,19 +9,14 @@ #[derive(Error, Debug)] pub enum Error { - #[error("Failed to query database")] - Database(#[from] chartered_db::Error), #[error("{0}")] - CrateFetch(#[from] crate::models::crates::CrateFetchError), + Database(#[from] chartered_db::Error), } impl Error { pub fn status_code(&self) -> axum::http::StatusCode { - use axum::http::StatusCode; - match self { - Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::CrateFetch(e) => e.status_code(), + Self::Database(e) => e.status_code(), } } } @@ -34,12 +24,17 @@ define_error_response!(Error); pub async fn handle( - extract::Path((_session_key, name)): extract::Path<(String, String)>, + extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, ) -> Result>, Error> { - let crate_ = get_crate_with_permissions(db.clone(), user, name, &[Permission::VISIBLE]).await?; - let versions = crate_.clone().versions_with_uploader(db).await?; + let crate_with_permissions = + Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); + + let versions = crate_with_permissions + .clone() + .versions_with_uploader(db) + .await?; // returning a Response instead of Json here so we don't have to close // every Crate/CrateVersion etc, would be easier if we just had an owned @@ -47,13 +42,13 @@ // diesel which requires `'static' which basically forces us to use Arc // if we want to keep a reference to anything ourselves. Ok(Json(Response { - info: crate_.as_ref().into(), + info: (&crate_with_permissions.crate_).into(), versions: versions .into_iter() .map(|(v, user)| ResponseVersion { size: v.size, created_at: chrono::Utc.from_local_datetime(&v.created_at).unwrap(), - inner: v.into_cargo_format(&crate_), + inner: v.into_cargo_format(&crate_with_permissions.crate_), uploader: user.username, }) .collect(), diff --git a/chartered-web/src/endpoints/web_api/crates/members.rs b/chartered-web/src/endpoints/web_api/crates/members.rs index a2fbc06..4059533 100644 --- a/chartered-web/src/endpoints/web_api/crates/members.rs +++ a/chartered-web/src/endpoints/web_api/crates/members.rs @@ -1,6 +1,6 @@ -use crate::models::crates::get_crate_with_permissions; use axum::{extract, Json}; use chartered_db::{ + crates::Crate, users::{User, UserCratePermissionValue as Permission}, uuid::Uuid, ConnectionPool, @@ -25,19 +25,14 @@ } pub async fn handle_get( - extract::Path((_session_key, name)): extract::Path<(String, String)>, + extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, ) -> Result, Error> { - let crate_ = get_crate_with_permissions( - db.clone(), - user, - name, - &[Permission::VISIBLE, Permission::MANAGE_USERS], - ) - .await?; - - let members = crate_ + let crate_with_permissions = + Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); + + let members = crate_with_permissions .members(db) .await? .into_iter() @@ -61,24 +56,19 @@ } pub async fn handle_patch( - extract::Path((_session_key, name)): extract::Path<(String, String)>, + extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, extract::Json(req): extract::Json, ) -> Result, Error> { - let crate_ = get_crate_with_permissions( - db.clone(), - user, - name, - &[Permission::VISIBLE, Permission::MANAGE_USERS], - ) - .await?; + let crate_with_permissions = + Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); let action_user = User::find_by_uuid(db.clone(), req.user_uuid) .await? .ok_or(Error::InvalidUserId)?; - let affected_rows = crate_ + let affected_rows = crate_with_permissions .update_permissions(db, action_user.id, req.permissions) .await?; if affected_rows == 0 { @@ -89,24 +79,19 @@ } pub async fn handle_put( - extract::Path((_session_key, name)): extract::Path<(String, String)>, + extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, extract::Json(req): extract::Json, ) -> Result, Error> { - let crate_ = get_crate_with_permissions( - db.clone(), - user, - name, - &[Permission::VISIBLE, Permission::MANAGE_USERS], - ) - .await?; + let crate_with_permissions = + Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); let action_user = User::find_by_uuid(db.clone(), req.user_uuid) .await? .ok_or(Error::InvalidUserId)?; - crate_ + crate_with_permissions .insert_permissions(db, action_user.id, req.permissions) .await?; @@ -119,34 +104,29 @@ } pub async fn handle_delete( - extract::Path((_session_key, name)): extract::Path<(String, String)>, + extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, extract::Extension(user): extract::Extension>, extract::Json(req): extract::Json, ) -> Result, Error> { - let crate_ = get_crate_with_permissions( - db.clone(), - user, - name, - &[Permission::VISIBLE, Permission::MANAGE_USERS], - ) - .await?; + let crate_with_permissions = + Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); let action_user = User::find_by_uuid(db.clone(), req.user_uuid) .await? .ok_or(Error::InvalidUserId)?; - crate_.delete_member(db, action_user.id).await?; + crate_with_permissions + .delete_member(db, action_user.id) + .await?; Ok(Json(ErrorResponse { error: None })) } #[derive(Error, Debug)] pub enum Error { - #[error("Failed to query database")] - Database(#[from] chartered_db::Error), #[error("{0}")] - CrateFetch(#[from] crate::models::crates::CrateFetchError), + Database(#[from] chartered_db::Error), #[error("Permissions update conflict, user was removed as a member of the crate")] UpdateConflictRemoved, #[error("An invalid user id was given")] @@ -158,8 +138,7 @@ use axum::http::StatusCode; match self { - Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::CrateFetch(e) => e.status_code(), + Self::Database(e) => e.status_code(), Self::UpdateConflictRemoved => StatusCode::CONFLICT, Self::InvalidUserId => StatusCode::BAD_REQUEST, } diff --git a/chartered-web/src/endpoints/web_api/crates/recently_updated.rs b/chartered-web/src/endpoints/web_api/crates/recently_updated.rs index 0b2f725..c2cec7c 100644 --- a/chartered-web/src/endpoints/web_api/crates/recently_updated.rs +++ a/chartered-web/src/endpoints/web_api/crates/recently_updated.rs @@ -6,19 +6,14 @@ #[derive(Error, Debug)] pub enum Error { - #[error("Failed to query database")] - Database(#[from] chartered_db::Error), #[error("{0}")] - CrateFetch(#[from] crate::models::crates::CrateFetchError), + Database(#[from] chartered_db::Error), } impl Error { pub fn status_code(&self) -> axum::http::StatusCode { - use axum::http::StatusCode; - match self { - Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::CrateFetch(e) => e.status_code(), + Self::Database(e) => e.status_code(), } } } @@ -34,9 +29,10 @@ Ok(Json(Response { versions: crates_with_versions .into_iter() - .map(|(crate_, version)| ResponseVersion { + .map(|(crate_, version, organisation)| ResponseVersion { name: crate_.name, version: version.version, + organisation: organisation.name, }) .collect(), })) @@ -51,4 +47,5 @@ pub struct ResponseVersion { name: String, version: String, + organisation: String, } -- rgit 0.1.3