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 🎉
Diff
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(-)
@@ -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"
@@ -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"
@@ -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"
@@ -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<String>,
pub description: Option<String>,
pub repository: Option<String>,
@@ -20,22 +24,61 @@
pub documentation: Option<String>,
}
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<HashMap<Crate, Vec<CrateVersion<'static>>>> {
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<Vec<(Crate, CrateVersion<'static>)>> {
requesting_user_id: i32,
) -> Result<Vec<(Crate, CrateVersion<'static>, 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<Option<Self>> {
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<CrateWithPermissions> {
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::<Crate>(&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<Self>,
pub async fn create(
conn: ConnectionPool,
) -> Result<Vec<(CrateVersion<'static>, User)>> {
requesting_user_id: i32,
given_org_name: String,
given_crate_name: String,
) -> Result<CrateWithPermissions> {
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::<Crate>(&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<Self>,
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::<CrateVersion>(&conn)
.optional()?)
})
.await?
}
pub async fn versions_with_uploader(
self: Arc<Self>,
conn: ConnectionPool,
) -> Result<Vec<(CrateVersion<'static>, 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<Self>,
conn: ConnectionPool,
) -> Result<Vec<(crate::users::User, crate::users::UserCratePermissionValue)>> {
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<usize> {
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<usize> {
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<Self>,
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))
@@ -1,5 +1,7 @@
#![deny(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::doc_markdown)]
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 {
Connection(#[from] diesel::r2d2::PoolError),
Query(#[from] diesel::result::Error),
TaskJoin(#[from] tokio::task::JoinError),
KeyParse(#[from] thrussh_keys::Error),
MissingPermission(crate::users::UserCratePermissionValue),
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<Integer>, y: Integer) -> Integer);
diesel_infix_operator!(BitwiseAnd, " & ", Integer);
diesel_infix_operator!(BitwiseOr, " | ", Integer);
trait BitwiseExpressionMethods: Expression<SqlType = Integer> + Sized {
fn bitwise_and<T: AsExpression<Integer>>(
self,
other: T,
) -> Grouped<BitwiseAnd<Self, T::Expression>> {
Grouped(BitwiseAnd::new(self.as_expression(), other.as_expression()))
}
trait BitwiseExpressionMethods: Expression<SqlType = diesel::sql_types::Integer> + Sized {
fn bitwise_and<T: AsExpression<diesel::sql_types::Integer>>(
fn bitwise_or<T: AsExpression<Integer>>(
self,
other: T,
) -> BitwiseAnd<Self, T::Expression> {
BitwiseAnd::new(self.as_expression(), other.as_expression())
) -> Grouped<BitwiseOr<Self, T::Expression>> {
Grouped(BitwiseOr::new(self.as_expression(), other.as_expression()))
}
}
impl<T: Expression<SqlType = diesel::sql_types::Integer>> BitwiseExpressionMethods for T {}
impl<T: Expression<SqlType = Integer>> BitwiseExpressionMethods for T {}
@@ -19,6 +19,7 @@
crates (id) {
id -> Integer,
name -> Text,
organisation_id -> Integer,
readme -> Nullable<Text>,
description -> Nullable<Text>,
repository -> Nullable<Text>,
@@ -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,
@@ -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
}
@@ -9,6 +9,7 @@
pub struct SqlUuid(pub uuid::Uuid);
impl SqlUuid {
#[must_use]
pub fn random() -> Self {
Self(uuid::Uuid::new_v4())
}
@@ -44,7 +44,7 @@
/>
<PrivateRoute
exact
path="/crates/:crate/:subview?"
path="/crates/:organisation/:crate/:subview?"
component={() => <CrateView />}
/>
<PrivateRoute
@@ -58,6 +58,7 @@
db: self.db.clone(),
user: None,
user_ssh_key: None,
organisation: None,
}
}
}
@@ -70,6 +71,7 @@
db: chartered_db::ConnectionPool,
user: Option<chartered_db::users::User>,
user_ssh_key: Option<Arc<chartered_db::users::UserSshKey>>,
organisation: Option<String>,
}
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<chartered_db::users::UserSshKey>, 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 {
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);
}
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 @@
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 @@
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<TwoCharTree<BTreeMap<String, String>>> {
use chartered_db::crates::Crate;
let mut tree: TwoCharTree<TwoCharTree<BTreeMap<String, String>>> = BTreeMap::new();
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()];
@@ -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)
.layer(
@@ -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;
@@ -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)
);
@@ -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<RecentlyUpdatedResponse>({
auth,
endpoint: "crates/recently-updated",
});
const { response: recentlyUpdated, error } =
useAuthenticatedRequest<RecentlyUpdatedResponse>({
auth,
endpoint: "crates/recently-updated",
});
return (
<div className="text-white">
@@ -67,15 +70,22 @@
interface Crate {
name: string;
version: string;
organisation: string;
}
function CrateCard({ crate }: { crate: Crate }) {
return (
<Link to={`/crates/${crate.name}`} className="text-decoration-none">
<Link
to={`/crates/${crate.organisation}/${crate.name}`}
className="text-decoration-none"
>
<div className="card border-0 mb-2 shadow-sm">
<div className="card-body text-black d-flex flex-row">
<div className="flex-grow-1 align-self-center">
<h6 className="text-primary my-0">{crate.name}</h6>
<h6 className="text-primary my-0">
<span className="text-secondary">{crate.organisation}/</span>
{crate.name}
</h6>
<small className="text-secondary">v{crate.version}</small>
</div>
@@ -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<User>,
crate_name: String,
required_permissions: &[chartered_db::users::UserCratePermissionValue],
) -> Result<Arc<Crate>, 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_)
}
@@ -1,1 +1,0 @@
pub mod crates;
@@ -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 <Redirect to={`/crates/${crate}/readme`} />
return <Redirect to={`/crates/${organisation}/${crate}/readme`} />;
}
const { response: crateInfo, error } = useAuthenticatedRequest<CrateInfo>({
auth,
endpoint: `crates/${crate}`,
endpoint: `crates/${organisation}/${crate}`,
});
if (error) {
@@ -141,12 +141,20 @@
<div className="card-header">
<ul className="nav nav-pills card-header-pills">
<li className="nav-item">
<NavLink to={`/crates/${crate}/readme`} className="nav-link" activeClassName="bg-primary bg-gradient active">
<NavLink
to={`/crates/${organisation}/${crate}/readme`}
className="nav-link"
activeClassName="bg-primary bg-gradient active"
>
Readme
</NavLink>
</li>
<li className="nav-item">
<NavLink to={`/crates/${crate}/versions`} className="nav-link" activeClassName="bg-primary bg-gradient active">
<NavLink
to={`/crates/${organisation}/${crate}/versions`}
className="nav-link"
activeClassName="bg-primary bg-gradient active"
>
Versions
<span className={`badge rounded-pill bg-danger ms-1`}>
{crateInfo.versions.length}
@@ -154,7 +162,11 @@
</NavLink>
</li>
<li className="nav-item">
<NavLink to={`/crates/${crate}/members`} className="nav-link" activeClassName="bg-primary bg-gradient active">
<NavLink
to={`/crates/${organisation}/${crate}/members`}
className="nav-link"
activeClassName="bg-primary bg-gradient active"
>
Members
</NavLink>
</li>
@@ -168,7 +180,11 @@
) : (
<></>
)}
{currentTab == "members" ? <Members crate={crate} /> : <></>}
{currentTab == "members" ? (
<Members crate={crate} organisation={organisation} />
) : (
<></>
)}
</div>
</div>
</div>
@@ -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<CratesMembersResponse>(
{
auth,
endpoint: `crates/${crate}/members`,
endpoint: `crates/${organisation}/${crate}/members`,
},
[reload]
);
@@ -72,6 +78,7 @@
{response.members.map((member, index) => (
<MemberListItem
key={index}
organisation={organisation}
crate={crate}
member={member}
prospectiveMember={false}
@@ -83,6 +90,7 @@
{prospectiveMembers.map((member, index) => (
<MemberListItem
key={index}
organisation={organisation}
crate={crate}
member={member}
prospectiveMember={true}
@@ -112,12 +120,14 @@
}
function MemberListItem({
organisation,
crate,
member,
prospectiveMember,
allowedPermissions,
onUpdateComplete,
}: {
organisation: string;
crate: string;
member: Member;
prospectiveMember: boolean;
@@ -139,7 +149,7 @@
try {
let res = await fetch(
authenticatedEndpoint(auth, `crates/${crate}/members`),
authenticatedEndpoint(auth, `crates/${organisation}/${crate}/members`),
{
method: prospectiveMember ? "PUT" : "PATCH",
headers: {
@@ -171,7 +181,7 @@
try {
let res = await fetch(
authenticatedEndpoint(auth, `crates/${crate}/members`),
authenticatedEndpoint(auth, `crates/${organisation}/${crate}/members`),
{
method: "DELETE",
headers: {
@@ -1,23 +1,17 @@
use crate::models::crates::get_crate_with_permissions;
use axum::extract;
use chartered_db::{
users::{User, UserCratePermissionValue as Permission},
ConnectionPool,
};
use chartered_db::{crates::Crate, users::User, ConnectionPool};
use chartered_fs::FileSystem;
use std::{str::FromStr, sync::Arc};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Failed to query database")]
#[error("{0}")]
Database(#[from] chartered_db::Error),
#[error("Failed to fetch crate file")]
File(#[from] std::io::Error),
#[error("The requested version does not exist for the crate")]
NoVersion,
#[error("{0}")]
CrateFetch(#[from] crate::models::crates::CrateFetchError),
}
impl Error {
@@ -25,9 +19,9 @@
use axum::http::StatusCode;
match self {
Self::Database(_) | Self::File(_) => 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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Vec<u8>, 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();
@@ -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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Json<GetResponse>, 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()
@@ -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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
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,
@@ -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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Json<Response>, 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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Json<Response>, 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 }))
}
@@ -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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<axum::http::Response<Full<Bytes>>, 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?;
@@ -47,13 +42,13 @@
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(),
@@ -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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Json<GetResponse>, 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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Json(req): extract::Json<PutOrPatchRequest>,
) -> Result<Json<ErrorResponse>, 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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Json(req): extract::Json<PutOrPatchRequest>,
) -> Result<Json<ErrorResponse>, 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<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
extract::Json(req): extract::Json<DeleteRequest>,
) -> Result<Json<ErrorResponse>, 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,
}
@@ -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,
}