Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce public repositories #202

Merged
merged 1 commit into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions crates/app/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Body>,
) -> Result<(Repository<'a>, Option<User<'a>>), 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::<OidcClaims>()
.await?
.assert_user(store, &cx.owner)
.await
.map_err(IntoResponse::into_response)
.map(|user| (repo, Some(user)))
}
}
20 changes: 20 additions & 0 deletions crates/app/src/auth/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<User<'a>, 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]
Expand Down
2 changes: 1 addition & 1 deletion crates/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down
21 changes: 6 additions & 15 deletions crates/app/src/repos/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<Store>>,
Extension(ref store): Extension<Arc<Store>>,
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
Expand Down
26 changes: 8 additions & 18 deletions crates/app/src/repos/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<Store>>,
Extension(ref store): Extension<Arc<Store>>,
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| {
Expand Down
25 changes: 8 additions & 17 deletions crates/app/src/repos/put.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<Store>>,
Extension(ref store): Extension<Arc<Store>>,
claims: OidcClaims,
cx: RepositoryContext,
meta: Meta,
Json(config): Json<RepositoryConfig>,
) -> 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);
Expand Down
56 changes: 42 additions & 14 deletions crates/app/src/store/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -301,7 +301,7 @@ impl<'a, P: AsRef<Utf8Path>> Entity<'a, P> {
})
}

/// Returns metadata of an entity
/// Returns metadata of the entity.
pub async fn get_meta(&self) -> Result<Meta, GetError<anyhow::Error>> {
let buf = self
.root
Expand All @@ -316,21 +316,49 @@ impl<'a, P: AsRef<Utf8Path>> 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<impl '_ + AsyncRead, GetError<anyhow::Error>> {
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<Vec<u8>, GetError<anyhow::Error>> {
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<T>(&self) -> Result<T, GetError<anyhow::Error>>
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<anyhow::Error>> {
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),
Expand Down
11 changes: 10 additions & 1 deletion crates/app/src/store/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -30,6 +30,15 @@ impl<'a, P> From<Entity<'a, P>> for Repository<'a, P> {
}

impl<'a, P: AsRef<Utf8Path>> Repository<'a, P> {
pub async fn get_json(&self) -> Result<RepositoryConfig, GetError<anyhow::Error>> {
self.get_content_json().await
}

pub async fn is_public(&self) -> Result<bool, GetError<anyhow::Error>> {
let conf = self.get_json().await?;
Ok(conf.public)
}

pub async fn tags(&self) -> Result<Vec<TagName>, GetError<anyhow::Error>> {
self.read_dir("tags")
.await?
Expand Down
30 changes: 11 additions & 19 deletions crates/app/src/tags/get.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
// SPDX-FileCopyrightText: 2022 Profian Inc. <[email protected]>
// 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<Arc<Store>>,
claims: OidcClaims,
Extension(ref store): Extension<Arc<Store>>,
cx: TagContext,
req: Request<Body>,
) -> 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| {
Expand Down
32 changes: 12 additions & 20 deletions crates/app/src/tags/head.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,29 @@
// SPDX-FileCopyrightText: 2022 Profian Inc. <[email protected]>
// 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<Arc<Store>>,
claims: OidcClaims,
Extension(ref store): Extension<Arc<Store>>,
cx: TagContext,
req: Request<Body>,
) -> 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
Expand Down
Loading