From 4ee74ebaa981f4bb0d7907eb8dd06cb79f285d74 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 25 Sep 2021 01:34:28 +0100 Subject: [PATCH] Organisations management in Web UI --- Cargo.lock | 11 +++++++++++ chartered-db/Cargo.toml | 1 + chartered-db/src/crates.rs | 25 ++++++++++++++----------- chartered-db/src/lib.rs | 16 ++++++++++++---- chartered-db/src/organisations.rs | 195 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-db/src/users.rs | 9 +-------- chartered-frontend/src/index.tsx | 22 ++++++++++++++++++++++ chartered-git/src/main.rs | 2 +- chartered-web/src/main.rs | 20 ++++++++++++++++++++ chartered-frontend/src/sections/Nav.tsx | 7 ++++++- chartered-frontend/src/pages/crate/CrateView.tsx | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- chartered-frontend/src/pages/crate/Members.tsx | 271 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- chartered-frontend/src/pages/crate/OrganisationView.tsx | 260 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-frontend/src/pages/organisations/ListOrganisations.tsx | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx | 5 +++++ chartered-web/src/endpoints/web_api/mod.rs | 1 + chartered-web/src/endpoints/web_api/organisations/info.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/organisations/members.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/organisations/mod.rs | 7 +++++++ 19 files changed, 1074 insertions(+), 218 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2a37e5..695139c 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -199,6 +199,7 @@ "chartered-types", "chrono", "diesel", + "diesel_logger", "displaydoc", "dotenv", "hex", @@ -407,6 +408,16 @@ "proc-macro2", "quote", "syn", +] + +[[package]] +name = "diesel_logger" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1793935ad14586bf2aa51574a7157932640c345205ccfb2db431846d846e3db7" +dependencies = [ + "diesel", + "log", ] [[package]] diff --git a/chartered-db/Cargo.toml b/chartered-db/Cargo.toml index 622ebc1..0c3880b 100644 --- a/chartered-db/Cargo.toml +++ a/chartered-db/Cargo.toml @@ -13,6 +13,7 @@ bitflags = "1" chrono = "0.4" diesel = { version = "1", features = ["sqlite", "r2d2", "chrono"] } +diesel_logger = "0.1" displaydoc = "0.2" hex = "0.4" http = "0.2" diff --git a/chartered-db/src/crates.rs b/chartered-db/src/crates.rs index fbd7729..0cad66b 100644 --- a/chartered-db/src/crates.rs +++ a/chartered-db/src/crates.rs @@ -1,4 +1,5 @@ -use crate::users::{Organisation, User, UserCratePermission}; +use crate::organisations::Organisation; +use crate::users::{User, UserCratePermission}; use super::{ coalesce, @@ -144,7 +145,7 @@ permissions, }) } else { - Err(Error::MissingPermission(Permissions::VISIBLE)) + Err(Error::MissingCratePermission(Permissions::VISIBLE)) } }) .await? @@ -173,10 +174,11 @@ .select((id, permissions)) .first::<(i32, Permissions)>(&conn)?; + #[allow(clippy::if_not_else)] if !perms.contains(Permissions::VISIBLE) { - Err(Error::MissingPermission(Permissions::VISIBLE)) + Err(Error::MissingCratePermission(Permissions::VISIBLE)) } else if !perms.contains(Permissions::CREATE_CRATE) { - Err(Error::MissingPermission(Permissions::CREATE_CRATE)) + Err(Error::MissingCratePermission(Permissions::CREATE_CRATE)) } else { use crate::schema::crates::dsl::{crates, name, organisation_id}; @@ -262,7 +264,7 @@ conn: ConnectionPool, ) -> Result> { if !self.permissions.contains(Permissions::MANAGE_USERS) { - return Err(Error::MissingPermission(Permissions::MANAGE_USERS)); + return Err(Error::MissingCratePermission(Permissions::MANAGE_USERS)); } tokio::task::spawn_blocking(move || { @@ -286,7 +288,7 @@ given_permissions: crate::users::UserCratePermissionValue, ) -> Result { if !self.permissions.contains(Permissions::MANAGE_USERS) { - return Err(Error::MissingPermission(Permissions::MANAGE_USERS)); + return Err(Error::MissingCratePermission(Permissions::MANAGE_USERS)); } tokio::task::spawn_blocking(move || { @@ -314,7 +316,7 @@ given_permissions: crate::users::UserCratePermissionValue, ) -> Result { if !self.permissions.contains(Permissions::MANAGE_USERS) { - return Err(Error::MissingPermission(Permissions::MANAGE_USERS)); + return Err(Error::MissingCratePermission(Permissions::MANAGE_USERS)); } tokio::task::spawn_blocking(move || { @@ -341,7 +343,7 @@ given_user_id: i32, ) -> Result<()> { if !self.permissions.contains(Permissions::MANAGE_USERS) { - return Err(Error::MissingPermission(Permissions::MANAGE_USERS)); + return Err(Error::MissingCratePermission(Permissions::MANAGE_USERS)); } tokio::task::spawn_blocking(move || { @@ -383,10 +385,12 @@ }; if !self.permissions.contains(Permissions::PUBLISH_VERSION) { - return Err(Error::MissingPermission(Permissions::PUBLISH_VERSION)); + return Err(Error::MissingCratePermission(Permissions::PUBLISH_VERSION)); } tokio::task::spawn_blocking(move || { + use diesel::result::{DatabaseErrorKind, Error as DieselError}; + let conn = conn.get()?; conn.transaction::<_, crate::Error, _>(|| { @@ -415,7 +419,6 @@ )) .execute(&conn); - use diesel::result::{DatabaseErrorKind, Error as DieselError}; match res { Ok(_) => Ok(()), Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => { @@ -439,7 +442,7 @@ 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)); + return Err(Error::MissingCratePermission(Permissions::YANK_VERSION)); } tokio::task::spawn_blocking(move || { diff --git a/chartered-db/src/lib.rs b/chartered-db/src/lib.rs index c497e19..c66eb8a 100644 --- a/chartered-db/src/lib.rs +++ a/chartered-db/src/lib.rs @@ -32,6 +32,7 @@ } pub mod crates; +pub mod organisations; pub mod schema; pub mod users; pub mod uuid; @@ -44,11 +45,12 @@ r2d2::{ConnectionManager, Pool}, sql_types::{Integer, Nullable}, }; +use diesel_logger::LoggingConnection; use displaydoc::Display; use std::sync::Arc; use thiserror::Error; -pub type ConnectionPool = Arc>>; +pub type ConnectionPool = Arc>>>; pub type Result = std::result::Result; pub fn init() -> Result { @@ -66,9 +68,13 @@ /// Key parse failure: `{0}` KeyParse(#[from] thrussh_keys::Error), /// You don't have the {0:?} permission for this crate - MissingPermission(crate::users::UserCratePermissionValue), + MissingCratePermission(crate::users::UserCratePermissionValue), + /// You don't have the {0:?} permission for this organisation + MissingOrganisationPermission(crate::users::UserCratePermissionValue), /// The requested crate does not exist MissingCrate, + /// The requested organisation does not exist + MissingOrganisation, /// Version {0} already exists for this crate VersionConflict(String), } @@ -78,12 +84,14 @@ pub fn status_code(&self) -> http::StatusCode { match self { Self::MissingCrate => http::StatusCode::NOT_FOUND, - Self::MissingPermission(v) + Self::MissingCratePermission(v) | Self::MissingOrganisationPermission(v) if v.contains(crate::users::UserCratePermissionValue::VISIBLE) => { http::StatusCode::NOT_FOUND } - Self::MissingPermission(_) => http::StatusCode::FORBIDDEN, + Self::MissingCratePermission(_) | Self::MissingOrganisationPermission(_) => { + http::StatusCode::FORBIDDEN + } Self::KeyParse(_) | Self::VersionConflict(_) => http::StatusCode::BAD_REQUEST, _ => http::StatusCode::INTERNAL_SERVER_ERROR, } diff --git a/chartered-db/src/organisations.rs b/chartered-db/src/organisations.rs new file mode 100644 index 0000000..5a0e735 100644 --- /dev/null +++ a/chartered-db/src/organisations.rs @@ -1,0 +1,195 @@ +use crate::{ + crates::Crate, + users::{User, UserCratePermissionValue as Permission}, + Error, +}; + +use super::{ + schema::{organisations, user_organisation_permissions, users}, + uuid::SqlUuid, + ConnectionPool, Result, +}; + +use diesel::{prelude::*, Associations, Identifiable, Queryable}; + +use std::sync::Arc; + +#[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)] +pub struct Organisation { + pub id: i32, + pub uuid: SqlUuid, + pub name: String, +} + +impl Organisation { + pub async fn find_by_name( + conn: ConnectionPool, + requesting_user_id: i32, + given_name: String, + ) -> Result { + use organisations::dsl::name as organisation_name; + + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + let (permissions, organisation) = organisations::table + .left_join( + user_organisation_permissions::table.on(user_organisation_permissions::user_id + .eq(requesting_user_id) + .and( + user_organisation_permissions::organisation_id + .eq(organisations::dsl::id), + )), + ) + .filter(organisation_name.eq(given_name)) + .select(( + user_organisation_permissions::dsl::permissions.nullable(), + organisations::all_columns, + )) + .get_result::<(Option, _)>(&conn) + .optional()? + .ok_or(Error::MissingOrganisation)?; + + let permissions = + permissions.ok_or(Error::MissingOrganisationPermission(Permission::VISIBLE))?; + + Ok(OrganisationWithPermissions { + organisation, + permissions, + }) + }) + .await? + } +} + +pub struct OrganisationWithPermissions { + organisation: Organisation, + permissions: Permission, +} + +impl OrganisationWithPermissions { + #[must_use] + pub fn permissions(&self) -> Permission { + self.permissions + } + + pub async fn crates(self: Arc, conn: ConnectionPool) -> Result> { + if !self.permissions.contains(Permission::VISIBLE) { + return Err(Error::MissingOrganisationPermission(Permission::VISIBLE)); + } + + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + Crate::belonging_to(&self.organisation) + .load(&conn) + .map_err(Into::into) + }) + .await? + } + + pub async fn members(self: Arc, conn: ConnectionPool) -> Result> { + if !self.permissions.contains(Permission::VISIBLE) { + return Err(Error::MissingOrganisationPermission(Permission::VISIBLE)); + } + + tokio::task::spawn_blocking(move || { + use crate::schema::user_organisation_permissions::dsl::organisation_id; + + let conn = conn.get()?; + user_organisation_permissions::table + .filter(organisation_id.eq(self.organisation.id)) + .inner_join(users::table) + .select(( + users::all_columns, + user_organisation_permissions::columns::permissions, + )) + .load(&conn) + .map_err(Into::into) + }) + .await? + } + + pub async fn update_permissions( + self: Arc, + conn: ConnectionPool, + given_user_id: i32, + given_permissions: crate::users::UserCratePermissionValue, + ) -> Result { + if !self.permissions.contains(Permission::MANAGE_USERS) { + return Err(Error::MissingCratePermission(Permission::MANAGE_USERS)); + } + + tokio::task::spawn_blocking(move || { + use crate::schema::user_organisation_permissions::dsl::{ + organisation_id, permissions, user_id, user_organisation_permissions, + }; + + let conn = conn.get()?; + + Ok(diesel::update( + user_organisation_permissions + .filter(user_id.eq(given_user_id)) + .filter(organisation_id.eq(self.organisation.id)), + ) + .set(permissions.eq(given_permissions.bits())) + .execute(&conn)?) + }) + .await? + } + + pub async fn insert_permissions( + self: Arc, + conn: ConnectionPool, + given_user_id: i32, + given_permissions: crate::users::UserCratePermissionValue, + ) -> Result { + if !self.permissions.contains(Permission::MANAGE_USERS) { + return Err(Error::MissingCratePermission(Permission::MANAGE_USERS)); + } + + tokio::task::spawn_blocking(move || { + use crate::schema::user_organisation_permissions::dsl::{ + organisation_id, permissions, user_id, user_organisation_permissions, + }; + + let conn = conn.get()?; + + Ok(diesel::insert_into(user_organisation_permissions) + .values(( + user_id.eq(given_user_id), + organisation_id.eq(self.organisation.id), + permissions.eq(given_permissions.bits()), + )) + .execute(&conn)?) + }) + .await? + } + + pub async fn delete_member( + self: Arc, + conn: ConnectionPool, + given_user_id: i32, + ) -> Result<()> { + if !self.permissions.contains(Permission::MANAGE_USERS) { + return Err(Error::MissingCratePermission(Permission::MANAGE_USERS)); + } + + tokio::task::spawn_blocking(move || { + use crate::schema::user_organisation_permissions::dsl::{ + organisation_id, user_id, user_organisation_permissions, + }; + + let conn = conn.get()?; + + diesel::delete( + user_organisation_permissions + .filter(user_id.eq(given_user_id)) + .filter(organisation_id.eq(self.organisation.id)), + ) + .execute(&conn)?; + + Ok(()) + }) + .await? + } +} diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index 6743bbb..00d6011 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -1,5 +1,5 @@ use super::{ - schema::{organisations, user_crate_permissions, user_sessions, user_ssh_keys, users}, + schema::{user_crate_permissions, user_sessions, user_ssh_keys, users}, uuid::SqlUuid, ConnectionPool, Result, }; @@ -9,13 +9,6 @@ use rand::{thread_rng, Rng}; use std::sync::Arc; 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 { diff --git a/chartered-frontend/src/index.tsx b/chartered-frontend/src/index.tsx index 09f1e20..aa3b7f3 100644 --- a/chartered-frontend/src/index.tsx +++ a/chartered-frontend/src/index.tsx @@ -19,6 +19,8 @@ import CrateView from "./pages/crate/CrateView"; import ListSshKeys from "./pages/ssh-keys/ListSshKeys"; import AddSshKeys from "./pages/ssh-keys/AddSshKeys"; +import ListOrganisations from "./pages/organisations/ListOrganisations"; +import OrganisationView from "./pages/crate/OrganisationView"; function App() { return ( @@ -44,8 +46,18 @@ /> } + /> + } + /> + } /> } + /> + } + /> + } /> diff --git a/chartered-git/src/main.rs b/chartered-git/src/main.rs index 26d2aaa..982c545 100644 --- a/chartered-git/src/main.rs +++ a/chartered-git/src/main.rs @@ -152,7 +152,7 @@ let args = shlex::split(data); Box::pin(async move { - let mut args = args.into_iter().map(|v| v.into_iter()).flatten(); + let mut args = args.into_iter().flat_map(Vec::into_iter); if args.next().as_deref() != Some("git-upload-pack") { anyhow::bail!("not git-upload-pack"); diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs index 7e656b7..1b46c75 100644 --- a/chartered-web/src/main.rs +++ a/chartered-web/src/main.rs @@ -33,6 +33,7 @@ #[tokio::main] #[allow(clippy::semicolon_if_nothing_returned)] // lint breaks with tokio::main +#[allow(clippy::too_many_lines)] // todo: refactor async fn main() { env_logger::init(); @@ -69,6 +70,24 @@ axum_box_after_every_route!(Router::new().route("/login", post(endpoints::web_api::login))); let web_authenticated = axum_box_after_every_route!(Router::new() + // organisations endpoints + .route( + "/organisations/:org", + get(endpoints::web_api::organisations::info) + ) + .route( + "/organisations/:org/members", + patch(endpoints::web_api::organisations::update_member) + ) + .route( + "/organisations/:org/members", + put(endpoints::web_api::organisations::insert_member) + ) + .route( + "/organisations/:org/members", + delete(endpoints::web_api::organisations::delete_member) + ) + // crate endpoints .route("/crates/:org/:crate", get(endpoints::web_api::crates::info)) .route( "/crates/:org/:crate/members", @@ -90,6 +109,7 @@ "/crates/recently-updated", get(endpoints::web_api::crates::list_recently_updated) ) + // users endpoints .route("/users/search", get(endpoints::web_api::search_users)) .route("/ssh-key", get(endpoints::web_api::get_ssh_keys)) .route("/ssh-key", put(endpoints::web_api::add_ssh_key)) diff --git a/chartered-frontend/src/sections/Nav.tsx b/chartered-frontend/src/sections/Nav.tsx index a46927e..fd4782b 100644 --- a/chartered-frontend/src/sections/Nav.tsx +++ a/chartered-frontend/src/sections/Nav.tsx @@ -38,8 +38,13 @@
  • - + SSH Keys + +
  • +
  • + + Organisations
  • diff --git a/chartered-frontend/src/pages/crate/CrateView.tsx b/chartered-frontend/src/pages/crate/CrateView.tsx index dc7bcfa..7c1488f 100644 --- a/chartered-frontend/src/pages/crate/CrateView.tsx +++ a/chartered-frontend/src/pages/crate/CrateView.tsx @@ -19,12 +19,12 @@ Square, } from "react-bootstrap-icons"; import { useParams, NavLink, Redirect } from "react-router-dom"; -import { useAuthenticatedRequest } from "../../util"; +import { authenticatedEndpoint, useAuthenticatedRequest } from "../../util"; import Prism from "react-syntax-highlighter/dist/cjs/prism"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; -import Members from "./Members"; +import CommonMembers from "./Members"; import { OverlayTrigger, Tooltip } from "react-bootstrap"; import HumanTime from "react-human-time"; @@ -76,13 +76,16 @@ const crateVersion = crateInfo.versions[crateInfo.versions.length - 1]; + const showLinks = + crateInfo.homepage || crateInfo.documentation || crateInfo.repository; + return (