diff --git a/crates/app/src/auth/mod.rs b/crates/app/src/auth/mod.rs index 4b451d25..d72119a4 100644 --- a/crates/app/src/auth/mod.rs +++ b/crates/app/src/auth/mod.rs @@ -6,3 +6,35 @@ mod tls; pub use oidc::Claims as OidcClaims; pub use tls::{Config as TlsConfig, TrustedCertificate}; + +use super::{Repository, Store, User}; + +use drawbridge_type::RepositoryContext; + +use axum::body::Body; +use axum::extract::RequestParts; +use axum::http::Request; +use axum::response::IntoResponse; + +pub async fn assert_repository_read<'a>( + store: &'a Store, + cx: &'a RepositoryContext, + req: Request, +) -> Result<(Repository<'a>, Option>), impl IntoResponse> { + let repo = store.repository(cx); + if repo + .is_public() + .await + .map_err(IntoResponse::into_response)? + { + Ok((repo, None)) + } else { + RequestParts::new(req) + .extract::() + .await? + .assert_user(store, &cx.owner) + .await + .map_err(IntoResponse::into_response) + .map(|user| (repo, Some(user))) + } +} diff --git a/crates/app/src/auth/oidc.rs b/crates/app/src/auth/oidc.rs index fdedcea8..eacd2aa1 100644 --- a/crates/app/src/auth/oidc.rs +++ b/crates/app/src/auth/oidc.rs @@ -48,6 +48,26 @@ e.into_response() } }) } + + /// Assert that the client is the user identified by `cx`. + pub async fn assert_user<'a>( + &self, + store: &'a Store, + cx: &UserContext, + ) -> Result, impl IntoResponse> { + let (ref oidc_cx, user) = self + .get_user(store) + .await + .map_err(IntoResponse::into_response)?; + if oidc_cx != cx { + return Err(( + StatusCode::UNAUTHORIZED, + format!( "You are logged in as `{oidc_cx}`, please relogin as `{cx}` to access the resource"), + ) + .into_response()); + } + Ok(user) + } } #[async_trait] diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 84570a28..6328d439 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -14,7 +14,7 @@ pub mod tags; pub mod trees; pub mod users; -pub use auth::*; +pub use auth::{OidcClaims, TlsConfig, TrustedCertificate}; pub use builder::*; pub(crate) use handle::*; pub(crate) use store::*; diff --git a/crates/app/src/repos/get.rs b/crates/app/src/repos/get.rs index 5a0772bf..7cec5618 100644 --- a/crates/app/src/repos/get.rs +++ b/crates/app/src/repos/get.rs @@ -6,30 +6,21 @@ use super::super::{OidcClaims, Store}; use drawbridge_type::RepositoryContext; use async_std::sync::Arc; -use axum::http::StatusCode; use axum::response::IntoResponse; use axum::Extension; -use log::debug; +use log::{debug, trace}; pub async fn get( - Extension(store): Extension>, + Extension(ref store): Extension>, claims: OidcClaims, cx: RepositoryContext, ) -> impl IntoResponse { - let (oidc_cx, user) = claims - .get_user(&store) + trace!(target: "app::trees::get", "called for `{cx}`"); + + let user = claims + .assert_user(store, &cx.owner) .await .map_err(IntoResponse::into_response)?; - if oidc_cx != cx.owner { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx.owner - ), - ) - .into_response()); - } // TODO: Stream body // https://github.com/profianinc/drawbridge/issues/56 diff --git a/crates/app/src/repos/head.rs b/crates/app/src/repos/head.rs index e4f8cf1a..2d87c779 100644 --- a/crates/app/src/repos/head.rs +++ b/crates/app/src/repos/head.rs @@ -6,32 +6,22 @@ use super::super::{OidcClaims, Store}; use drawbridge_type::RepositoryContext; use async_std::sync::Arc; -use axum::http::StatusCode; use axum::response::IntoResponse; use axum::Extension; -use log::debug; +use log::{debug, trace}; pub async fn head( - Extension(store): Extension>, + Extension(ref store): Extension>, claims: OidcClaims, cx: RepositoryContext, ) -> impl IntoResponse { - let (oidc_cx, user) = claims - .get_user(&store) - .await - .map_err(IntoResponse::into_response)?; - if oidc_cx != cx.owner { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx.owner - ), - ) - .into_response()); - } + trace!(target: "app::trees::head", "called for `{cx}`"); - user.repository(&cx.name) + claims + .assert_user(store, &cx.owner) + .await + .map_err(IntoResponse::into_response)? + .repository(&cx.name) .get_meta() .await .map_err(|e| { diff --git a/crates/app/src/repos/put.rs b/crates/app/src/repos/put.rs index 35a520d3..7817ff70 100644 --- a/crates/app/src/repos/put.rs +++ b/crates/app/src/repos/put.rs @@ -9,31 +9,22 @@ use async_std::sync::Arc; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::{Extension, Json}; -use log::debug; +use log::{debug, trace}; pub async fn put( - Extension(store): Extension>, + Extension(ref store): Extension>, claims: OidcClaims, cx: RepositoryContext, meta: Meta, Json(config): Json, ) -> impl IntoResponse { - let (oidc_cx, user) = claims - .get_user(&store) - .await - .map_err(IntoResponse::into_response)?; - if oidc_cx != cx.owner { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx.owner - ), - ) - .into_response()); - } + trace!(target: "app::trees::put", "called for `{cx}`"); - user.create_repository(&cx.name, meta, &config) + claims + .assert_user(store, &cx.owner) + .await + .map_err(IntoResponse::into_response)? + .create_repository(&cx.name, meta, &config) .await .map_err(|e| { debug!(target: "app::repos::put", "failed for `{cx}`: {:?}", e); diff --git a/crates/app/src/store/entity.rs b/crates/app/src/store/entity.rs index 47e42c3a..14de7282 100644 --- a/crates/app/src/store/entity.rs +++ b/crates/app/src/store/entity.rs @@ -17,7 +17,7 @@ use futures::io::copy; use futures::try_join; use futures::{AsyncRead, AsyncWrite}; use log::{debug, trace}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; const STORAGE_FAILURE_RESPONSE: (StatusCode, &str) = (StatusCode::INTERNAL_SERVER_ERROR, "Storage backend failure"); @@ -301,7 +301,7 @@ impl<'a, P: AsRef> Entity<'a, P> { }) } - /// Returns metadata of an entity + /// Returns metadata of the entity. pub async fn get_meta(&self) -> Result> { let buf = self .root @@ -316,21 +316,49 @@ impl<'a, P: AsRef> Entity<'a, P> { .map_err(GetError::Internal) } - /// Returns metadata of an entity and a reader of its contents. + /// Returns contents of the entity as [AsyncRead]. + pub async fn get_content(&self) -> Result> { + self.root + .open(self.content_path()) + .map_err(|e| match e.kind() { + io::ErrorKind::NotFound => GetError::NotFound, + _ => { + GetError::Internal(anyhow::Error::new(e).context("failed to open content file")) + } + }) + .await + } + + /// Reads contents of the entity. + pub async fn read_content(&self) -> Result, GetError> { + self.root + .read(self.content_path()) + .map_err(|e| match e.kind() { + io::ErrorKind::NotFound => GetError::NotFound, + _ => { + GetError::Internal(anyhow::Error::new(e).context("failed to read content file")) + } + }) + .await + } + + /// Returns the contents of the entity as JSON. + pub async fn get_content_json(&self) -> Result> + where + for<'de> T: Deserialize<'de>, + { + let buf = self.read_content().await?; + serde_json::from_slice(&buf) + .context("failed to decode content as JSON") + .map_err(GetError::Internal) + } + + /// Returns metadata of the entity and a reader of its contents. pub async fn get(&self) -> Result<(Meta, impl '_ + AsyncRead), GetError> { - try_join!( - self.get_meta(), - self.root - .open(self.content_path()) - .map_err(|e| match e.kind() { - io::ErrorKind::NotFound => GetError::NotFound, - _ => GetError::Internal( - anyhow::Error::new(e).context("failed to open content file") - ), - }) - ) + try_join!(self.get_meta(), self.get_content()) } + /// Returns metadata of the entity and writes its contents into `dst`. pub async fn get_to_writer( &self, dst: &mut (impl Unpin + AsyncWrite), diff --git a/crates/app/src/store/repo.rs b/crates/app/src/store/repo.rs index a939931f..fe9175f8 100644 --- a/crates/app/src/store/repo.rs +++ b/crates/app/src/store/repo.rs @@ -6,7 +6,7 @@ use super::{CreateError, Entity, GetError, Tag}; use std::ops::Deref; use drawbridge_type::digest::{Algorithms, ContentDigest}; -use drawbridge_type::{Meta, TagEntry, TagName}; +use drawbridge_type::{Meta, RepositoryConfig, TagEntry, TagName}; use anyhow::{anyhow, Context}; use camino::{Utf8Path, Utf8PathBuf}; @@ -30,6 +30,15 @@ impl<'a, P> From> for Repository<'a, P> { } impl<'a, P: AsRef> Repository<'a, P> { + pub async fn get_json(&self) -> Result> { + self.get_content_json().await + } + + pub async fn is_public(&self) -> Result> { + let conf = self.get_json().await?; + Ok(conf.public) + } + pub async fn tags(&self) -> Result, GetError> { self.read_dir("tags") .await? diff --git a/crates/app/src/tags/get.rs b/crates/app/src/tags/get.rs index 53ad02b8..19298b51 100644 --- a/crates/app/src/tags/get.rs +++ b/crates/app/src/tags/get.rs @@ -1,41 +1,33 @@ // SPDX-FileCopyrightText: 2022 Profian Inc. // SPDX-License-Identifier: AGPL-3.0-only -use super::super::{OidcClaims, Store}; +use super::super::Store; +use crate::auth::assert_repository_read; use drawbridge_type::TagContext; use async_std::sync::Arc; -use axum::http::StatusCode; +use axum::body::Body; +use axum::http::Request; use axum::response::IntoResponse; use axum::Extension; -use log::debug; +use log::{debug, trace}; pub async fn get( - Extension(store): Extension>, - claims: OidcClaims, + Extension(ref store): Extension>, cx: TagContext, + req: Request, ) -> impl IntoResponse { - let (oidc_cx, user) = claims - .get_user(&store) + trace!(target: "app::tags::get", "called for `{cx}`"); + + let (repo, _) = assert_repository_read(store, &cx.repository, req) .await .map_err(IntoResponse::into_response)?; - if oidc_cx != cx.repository.owner { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx.repository.owner - ), - ) - .into_response()); - } // TODO: Stream body // https://github.com/profianinc/drawbridge/issues/56 let mut body = vec![]; - user.repository(&cx.repository.name) - .tag(&cx.name) + repo.tag(&cx.name) .get_to_writer(&mut body) .await .map_err(|e| { diff --git a/crates/app/src/tags/head.rs b/crates/app/src/tags/head.rs index cb183b84..49b2748e 100644 --- a/crates/app/src/tags/head.rs +++ b/crates/app/src/tags/head.rs @@ -1,37 +1,29 @@ // SPDX-FileCopyrightText: 2022 Profian Inc. // SPDX-License-Identifier: AGPL-3.0-only -use super::super::{OidcClaims, Store}; +use super::super::Store; +use crate::auth::assert_repository_read; use drawbridge_type::TagContext; use async_std::sync::Arc; -use axum::http::StatusCode; +use axum::body::Body; +use axum::http::Request; use axum::response::IntoResponse; use axum::Extension; -use log::debug; +use log::{debug, trace}; pub async fn head( - Extension(store): Extension>, - claims: OidcClaims, + Extension(ref store): Extension>, cx: TagContext, + req: Request, ) -> impl IntoResponse { - let (oidc_cx, user) = claims - .get_user(&store) - .await - .map_err(IntoResponse::into_response)?; - if oidc_cx != cx.repository.owner { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx.repository.owner - ), - ) - .into_response()); - } + trace!(target: "app::tags::head", "called for `{cx}`"); - user.repository(&cx.repository.name) + assert_repository_read(store, &cx.repository, req) + .await + .map_err(IntoResponse::into_response) + .map(|(repo, _)| repo)? .tag(&cx.name) .get_meta() .await diff --git a/crates/app/src/tags/put.rs b/crates/app/src/tags/put.rs index 1f54e8a6..c74501d4 100644 --- a/crates/app/src/tags/put.rs +++ b/crates/app/src/tags/put.rs @@ -13,7 +13,7 @@ use axum::extract::RequestParts; use axum::http::{Request, StatusCode}; use axum::response::IntoResponse; use axum::{Extension, Json}; -use log::debug; +use log::{debug, trace}; pub async fn put( Extension(store): Extension>, @@ -22,6 +22,8 @@ pub async fn put( meta: Meta, req: Request, ) -> impl IntoResponse { + trace!(target: "app::tags::put", "called for `{cx}`"); + if meta.hash.is_empty() { return Err(( StatusCode::BAD_REQUEST, @@ -30,20 +32,10 @@ pub async fn put( .into_response()); } - let (oidc_cx, user) = claims - .get_user(&store) + let user = claims + .assert_user(&store, &cx.repository.owner) .await .map_err(IntoResponse::into_response)?; - if oidc_cx != cx.repository.owner { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx.repository.owner - ), - ) - .into_response()); - } let mut req = RequestParts::new(req); let entry = match meta.mime.to_string().as_str() { diff --git a/crates/app/src/tags/query.rs b/crates/app/src/tags/query.rs index da8ec4e8..56c3499d 100644 --- a/crates/app/src/tags/query.rs +++ b/crates/app/src/tags/query.rs @@ -1,38 +1,30 @@ // SPDX-FileCopyrightText: 2022 Profian Inc. // SPDX-License-Identifier: AGPL-3.0-only -use super::super::{OidcClaims, Store}; +use super::super::Store; +use crate::auth::assert_repository_read; use drawbridge_type::{Meta, RepositoryContext}; use async_std::sync::Arc; -use axum::http::StatusCode; +use axum::body::Body; +use axum::http::Request; use axum::response::IntoResponse; use axum::Extension; -use log::debug; +use log::{debug, trace}; use mime::APPLICATION_JSON; pub async fn query( - Extension(store): Extension>, - claims: OidcClaims, - cx: RepositoryContext, + Extension(ref store): Extension>, + ref cx: RepositoryContext, + req: Request, ) -> impl IntoResponse { - let (oidc_cx, user) = claims - .get_user(&store) - .await - .map_err(IntoResponse::into_response)?; - if oidc_cx != cx.owner { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx.owner - ), - ) - .into_response()); - } + trace!(target: "app::tags::query", "called for `{cx}`"); - user.repository(&cx.name) + assert_repository_read(store, cx, req) + .await + .map_err(IntoResponse::into_response) + .map(|(repo, _)| repo)? .tags_json() .await .map(|(hash, buf)| { diff --git a/crates/app/src/trees/get.rs b/crates/app/src/trees/get.rs index 5827fc7f..c8a37b47 100644 --- a/crates/app/src/trees/get.rs +++ b/crates/app/src/trees/get.rs @@ -1,52 +1,39 @@ // SPDX-FileCopyrightText: 2022 Profian Inc. // SPDX-License-Identifier: AGPL-3.0-only -use super::super::{OidcClaims, Store, TrustedCertificate}; +use super::super::{Store, TrustedCertificate}; +use crate::auth::assert_repository_read; use drawbridge_type::TreeContext; use async_std::sync::Arc; use axum::body::Body; -use axum::extract::RequestParts; -use axum::http::{Request, StatusCode}; +use axum::http::Request; use axum::response::IntoResponse; use axum::Extension; -use log::debug; +use log::{debug, trace}; pub async fn get( - Extension(store): Extension>, + Extension(ref store): Extension>, cert: Option>, cx: TreeContext, req: Request, ) -> impl IntoResponse { - // TODO: Check if repo is public - let user = if cert.is_none() { - let (oidc_cx, user) = RequestParts::new(req) - .extract::() - .await? - .get_user(&store) + trace!(target: "app::trees::get", "called for `{cx}`"); + + let repo = if cert.is_none() { + assert_repository_read(store, &cx.tag.repository, req) .await - .map_err(IntoResponse::into_response)?; - if oidc_cx != cx.tag.repository.owner { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx.tag.repository.owner - ), - ) - .into_response()); - } - user + .map_err(IntoResponse::into_response) + .map(|(repo, _)| repo)? } else { - store.user(&cx.tag.repository.owner) + store.repository(&cx.tag.repository) }; // TODO: Stream body // https://github.com/profianinc/drawbridge/issues/56 let mut body = vec![]; - user.repository(&cx.tag.repository.name) - .tag(&cx.tag.name) + repo.tag(&cx.tag.name) .node(&cx.path) .get_to_writer(&mut body) .await diff --git a/crates/app/src/trees/head.rs b/crates/app/src/trees/head.rs index 8568516f..6c06cf7f 100644 --- a/crates/app/src/trees/head.rs +++ b/crates/app/src/trees/head.rs @@ -1,55 +1,41 @@ // SPDX-FileCopyrightText: 2022 Profian Inc. // SPDX-License-Identifier: AGPL-3.0-only -use super::super::{OidcClaims, Store, TrustedCertificate}; +use super::super::{Store, TrustedCertificate}; +use crate::auth::assert_repository_read; use drawbridge_type::TreeContext; use async_std::sync::Arc; use axum::body::Body; -use axum::extract::RequestParts; -use axum::http::{Request, StatusCode}; +use axum::http::Request; use axum::response::IntoResponse; use axum::Extension; -use log::debug; +use log::{debug, trace}; pub async fn head( - Extension(store): Extension>, + Extension(ref store): Extension>, cert: Option>, cx: TreeContext, req: Request, ) -> impl IntoResponse { - // TODO: Check if repo is public - let user = if cert.is_none() { - let (oidc_cx, user) = RequestParts::new(req) - .extract::() - .await? - .get_user(&store) + trace!(target: "app::trees::head", "called for `{cx}`"); + + if cert.is_none() { + assert_repository_read(store, &cx.tag.repository, req) .await - .map_err(IntoResponse::into_response)?; - if oidc_cx != cx.tag.repository.owner { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx.tag.repository.owner - ), - ) - .into_response()); - } - user + .map_err(IntoResponse::into_response) + .map(|(repo, _)| repo)? } else { - store.user(&cx.tag.repository.owner) - }; - - user.repository(&cx.tag.repository.name) - .tag(&cx.tag.name) - .node(&cx.path) - .get_meta() - .await - .map_err(|e| { - debug!(target: "app::trees::head", "failed for `{cx}`: {:?}", e); - e.into_response() - }) - .map(|meta| (meta, ())) + store.repository(&cx.tag.repository) + } + .tag(&cx.tag.name) + .node(&cx.path) + .get_meta() + .await + .map_err(|e| { + debug!(target: "app::trees::head", "failed for `{cx}`: {:?}", e); + e.into_response() + }) + .map(|meta| (meta, ())) } diff --git a/crates/app/src/trees/put.rs b/crates/app/src/trees/put.rs index f67a6d85..dd3e3d81 100644 --- a/crates/app/src/trees/put.rs +++ b/crates/app/src/trees/put.rs @@ -12,15 +12,17 @@ use axum::http::{Request, StatusCode}; use axum::response::IntoResponse; use axum::{Extension, Json}; use futures::{io, TryStreamExt}; -use log::debug; +use log::{debug, trace}; pub async fn put( - Extension(store): Extension>, + Extension(ref store): Extension>, claims: OidcClaims, cx: TreeContext, meta: Meta, req: Request, ) -> impl IntoResponse { + trace!(target: "app::trees::put", "called for `{cx}`"); + if meta.hash.is_empty() { return Err(( StatusCode::BAD_REQUEST, @@ -29,20 +31,10 @@ pub async fn put( .into_response()); } - let (oidc_cx, user) = claims - .get_user(&store) + let user = claims + .assert_user(store, &cx.tag.repository.owner) .await .map_err(IntoResponse::into_response)?; - if oidc_cx != cx.tag.repository.owner { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx.tag.repository.owner - ), - ) - .into_response()); - } let mut req = RequestParts::new(req); let tag = user.repository(&cx.tag.repository.name).tag(&cx.tag.name); diff --git a/crates/app/src/users/get.rs b/crates/app/src/users/get.rs index b1d1e2cf..b36b25b8 100644 --- a/crates/app/src/users/get.rs +++ b/crates/app/src/users/get.rs @@ -6,30 +6,21 @@ use super::super::{OidcClaims, Store}; use drawbridge_type::UserContext; use async_std::sync::Arc; -use axum::http::StatusCode; use axum::response::IntoResponse; use axum::Extension; -use log::debug; +use log::{debug, trace}; pub async fn get( - Extension(store): Extension>, + Extension(ref store): Extension>, claims: OidcClaims, - cx: UserContext, + ref cx: UserContext, ) -> impl IntoResponse { - let (oidc_cx, user) = claims - .get_user(&store) + trace!(target: "app::users::get", "called for `{cx}`"); + + let user = claims + .assert_user(store, cx) .await .map_err(IntoResponse::into_response)?; - if oidc_cx != cx { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx - ), - ) - .into_response()); - } // TODO: Stream body // https://github.com/profianinc/drawbridge/issues/56 diff --git a/crates/app/src/users/head.rs b/crates/app/src/users/head.rs index db89cf00..bc939890 100644 --- a/crates/app/src/users/head.rs +++ b/crates/app/src/users/head.rs @@ -6,32 +6,22 @@ use super::super::{OidcClaims, Store}; use drawbridge_type::UserContext; use async_std::sync::Arc; -use axum::http::StatusCode; use axum::response::IntoResponse; use axum::Extension; -use log::debug; +use log::{debug, trace}; pub async fn head( - Extension(store): Extension>, + Extension(ref store): Extension>, claims: OidcClaims, - cx: UserContext, + ref cx: UserContext, ) -> impl IntoResponse { - let (oidc_cx, user) = claims - .get_user(&store) - .await - .map_err(IntoResponse::into_response)?; - if oidc_cx != cx { - return Err(( - StatusCode::UNAUTHORIZED, - format!( - "You are logged in as `{oidc_cx}`, please relogin as `{}` to access `{cx}`", - cx - ), - ) - .into_response()); - } + trace!(target: "app::users::head", "called for `{cx}`"); - user.get_meta() + claims + .assert_user(store, cx) + .await + .map_err(IntoResponse::into_response)? + .get_meta() .await .map_err(|e| { debug!(target: "app::users::head", "failed for `{cx}`: {:?}", e); diff --git a/crates/app/src/users/put.rs b/crates/app/src/users/put.rs index bb06a320..1f71ef6d 100644 --- a/crates/app/src/users/put.rs +++ b/crates/app/src/users/put.rs @@ -9,15 +9,17 @@ use async_std::sync::Arc; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::{Extension, Json}; -use log::{debug, warn}; +use log::{debug, trace, warn}; pub async fn put( - Extension(store): Extension>, + Extension(ref store): Extension>, claims: OidcClaims, - cx: UserContext, + ref cx: UserContext, meta: Meta, - Json(record): Json, + Json(ref record): Json, ) -> impl IntoResponse { + trace!(target: "app::users::put", "called for `{cx}`"); + if record.subject != claims.subject().as_str() { return Err((StatusCode::UNAUTHORIZED, "OpenID Connect subject mismatch").into_response()); } @@ -44,7 +46,7 @@ pub async fn put( } store - .create_user(&cx, meta, &record) + .create_user(cx, meta, record) .await .map_err(|e| { debug!(target: "app::users::put", "failed for `{cx}`: {:?}", e); diff --git a/crates/type/src/repository/config.rs b/crates/type/src/repository/config.rs index d0a943dc..114a51b9 100644 --- a/crates/type/src/repository/config.rs +++ b/crates/type/src/repository/config.rs @@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize}; /// A repository config -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] -pub struct Config {} +pub struct Config { + pub public: bool, +} diff --git a/tests/mod.rs b/tests/mod.rs index 55f7dd1a..57f8c12d 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -202,45 +202,84 @@ async fn app() { .expect("failed to create user"), true ); + assert!(oidc_cl + .user(&format!("{user_name}other").parse().unwrap()) + .create(&user_record) + .is_err()); assert!(anon_user.get().is_err()); assert!(cert_user.get().is_err()); assert_eq!(oidc_user.get().expect("failed to get user"), user_record); - let repo_name = "test-repo-private".parse().unwrap(); - let repo_conf = RepositoryConfig {}; + let prv_repo_name = "test-repo-private".parse().unwrap(); + let prv_repo_conf = RepositoryConfig { public: false }; + + let pub_repo_name = "test-repo-public".parse().unwrap(); + let pub_repo_conf = RepositoryConfig { public: true }; + + let anon_prv_repo = anon_user.repository(&prv_repo_name); + let cert_prv_repo = cert_user.repository(&prv_repo_name); + let oidc_prv_repo = oidc_user.repository(&prv_repo_name); + + let anon_pub_repo = anon_user.repository(&pub_repo_name); + let cert_pub_repo = cert_user.repository(&pub_repo_name); + let oidc_pub_repo = oidc_user.repository(&pub_repo_name); + + assert!(anon_prv_repo.get().is_err()); + assert!(cert_prv_repo.get().is_err()); + assert!(oidc_prv_repo.get().is_err()); - let anon_repo = anon_user.repository(&repo_name); - let cert_repo = cert_user.repository(&repo_name); - let oidc_repo = oidc_user.repository(&repo_name); + assert!(anon_pub_repo.get().is_err()); + assert!(cert_pub_repo.get().is_err()); + assert!(oidc_pub_repo.get().is_err()); - assert!(anon_repo.get().is_err()); - assert!(cert_repo.get().is_err()); - assert!(oidc_repo.get().is_err()); + assert!(anon_prv_repo.tags().is_err()); + assert!(cert_prv_repo.tags().is_err()); + assert!(oidc_prv_repo.tags().is_err()); - assert!(anon_repo.tags().is_err()); - assert!(cert_repo.tags().is_err()); - assert!(oidc_repo.tags().is_err()); + assert!(anon_pub_repo.tags().is_err()); + assert!(cert_pub_repo.tags().is_err()); + assert!(oidc_pub_repo.tags().is_err()); + + assert!(anon_prv_repo.create(&prv_repo_conf).is_err()); + assert!(cert_prv_repo.create(&prv_repo_conf).is_err()); + assert_eq!( + oidc_prv_repo + .create(&prv_repo_conf) + .expect("failed to create repository"), + true + ); - assert!(anon_repo.create(&repo_conf).is_err()); - assert!(cert_repo.create(&repo_conf).is_err()); + assert!(anon_pub_repo.create(&pub_repo_conf).is_err()); + assert!(cert_pub_repo.create(&pub_repo_conf).is_err()); assert_eq!( - oidc_repo - .create(&repo_conf) + oidc_pub_repo + .create(&pub_repo_conf) .expect("failed to create repository"), true ); - assert!(anon_repo.get().is_err()); - assert!(cert_repo.get().is_err()); + assert!(anon_prv_repo.get().is_err()); + assert!(cert_prv_repo.get().is_err()); assert_eq!( - oidc_repo.get().expect("failed to get repository"), - repo_conf + oidc_prv_repo.get().expect("failed to get repository"), + prv_repo_conf ); - assert!(anon_repo.tags().is_err()); - assert!(cert_repo.tags().is_err()); - assert_eq!(oidc_repo.tags().expect("failed to get tags"), vec![]); + assert!(anon_pub_repo.get().is_err()); + assert!(cert_pub_repo.get().is_err()); + assert_eq!( + oidc_pub_repo.get().expect("failed to get repository"), + pub_repo_conf + ); + + assert!(anon_prv_repo.tags().is_err()); + assert!(cert_prv_repo.tags().is_err()); + assert_eq!(oidc_prv_repo.tags().expect("failed to get tags"), vec![]); + + assert_eq!(anon_pub_repo.tags().expect("failed to get tags"), vec![]); + assert_eq!(cert_pub_repo.tags().expect("failed to get tags"), vec![]); + assert_eq!(oidc_pub_repo.tags().expect("failed to get tags"), vec![]); let pkg = tempdir().expect("failed to create temporary package directory"); @@ -273,22 +312,30 @@ async fn app() { let tag_name = "0.1.0".parse().unwrap(); - let anon_tag = anon_repo.tag(&tag_name); - let cert_tag = cert_repo.tag(&tag_name); - let oidc_tag = oidc_repo.tag(&tag_name); + let anon_prv_tag = anon_prv_repo.tag(&tag_name); + let cert_prv_tag = cert_prv_repo.tag(&tag_name); + let oidc_prv_tag = oidc_prv_repo.tag(&tag_name); + + let anon_pub_tag = anon_pub_repo.tag(&tag_name); + let cert_pub_tag = cert_pub_repo.tag(&tag_name); + let oidc_pub_tag = oidc_pub_repo.tag(&tag_name); - assert!(anon_tag.get().is_err()); - assert!(cert_tag.get().is_err()); - assert!(oidc_tag.get().is_err()); + assert!(anon_prv_tag.get().is_err()); + assert!(cert_prv_tag.get().is_err()); + assert!(oidc_prv_tag.get().is_err()); - assert!(anon_tag.create_from_path_unsigned(pkg.path()).is_err()); - assert!(cert_tag.create_from_path_unsigned(pkg.path()).is_err()); - let (tag_created, tree_created) = oidc_tag + assert!(anon_pub_tag.get().is_err()); + assert!(cert_pub_tag.get().is_err()); + assert!(oidc_pub_tag.get().is_err()); + + assert!(anon_prv_tag.create_from_path_unsigned(pkg.path()).is_err()); + assert!(cert_prv_tag.create_from_path_unsigned(pkg.path()).is_err()); + let (prv_tag_created, prv_tree_created) = oidc_prv_tag .create_from_path_unsigned(pkg.path()) .expect("failed to create a tag and upload the tree"); - assert!(tag_created); + assert!(prv_tag_created); assert_eq!( - tree_created.clone().into_iter().collect::>(), + prv_tree_created.clone().into_iter().collect::>(), vec![ (TreePath::ROOT, true), ("tEst-file..__.foo.42.".parse().unwrap(), true), @@ -304,26 +351,65 @@ async fn app() { ] ); - assert!(anon_repo.tags().is_err()); - assert!(cert_repo.tags().is_err()); + assert!(anon_pub_tag.create_from_path_unsigned(pkg.path()).is_err()); + assert!(cert_pub_tag.create_from_path_unsigned(pkg.path()).is_err()); + assert_eq!( + oidc_pub_tag + .create_from_path_unsigned(pkg.path()) + .expect("failed to create a tag and upload the tree"), + (prv_tag_created, prv_tree_created) + ); + + assert!(anon_prv_repo.tags().is_err()); + assert!(cert_prv_repo.tags().is_err()); + assert_eq!( + oidc_prv_repo.tags().expect("failed to get tags"), + vec![tag_name.clone()] + ); + + assert_eq!( + anon_pub_repo.tags().expect("failed to get tags"), + vec![tag_name.clone()] + ); assert_eq!( - oidc_repo.tags().expect("failed to get tags"), + cert_pub_repo.tags().expect("failed to get tags"), + vec![tag_name.clone()] + ); + assert_eq!( + oidc_pub_repo.tags().expect("failed to get tags"), vec![tag_name.clone()] ); let file_name = "test-file.txt".parse().unwrap(); - let anon_file = anon_tag.path(&file_name); - let cert_file = cert_tag.path(&file_name); - let oidc_file = oidc_tag.path(&file_name); + let anon_prv_file = anon_prv_tag.path(&file_name); + let cert_prv_file = cert_prv_tag.path(&file_name); + let oidc_prv_file = oidc_prv_tag.path(&file_name); + + let anon_pub_file = anon_pub_tag.path(&file_name); + let cert_pub_file = cert_pub_tag.path(&file_name); + let oidc_pub_file = oidc_pub_tag.path(&file_name); - assert!(anon_file.get_string().is_err()); + assert!(anon_prv_file.get_string().is_err()); + assert_eq!( + cert_prv_file.get_string().expect("failed to get file"), + (APPLICATION_OCTET_STREAM, "text".into()) + ); + assert_eq!( + oidc_prv_file.get_string().expect("failed to get file"), + (APPLICATION_OCTET_STREAM, "text".into()) + ); + + assert_eq!( + anon_pub_file.get_string().expect("failed to get file"), + (APPLICATION_OCTET_STREAM, "text".into()) + ); assert_eq!( - cert_file.get_string().expect("failed to get file"), + cert_pub_file.get_string().expect("failed to get file"), (APPLICATION_OCTET_STREAM, "text".into()) ); assert_eq!( - oidc_file.get_string().expect("failed to get file"), + oidc_pub_file.get_string().expect("failed to get file"), (APPLICATION_OCTET_STREAM, "text".into()) ); });