Skip to content

Commit

Permalink
feat: Added API for adding credentials (#35)
Browse files Browse the repository at this point in the history
Implemented POST and DELETE methods to allow credentials to be modified
via HTTP. The GET method intentionally does not respond with the
credential as there's never a reason inserted credentials should be
readable after the initial insert. So as a additional security measure,
the GET only determines if the credential exists or not.
  • Loading branch information
Isawan authored Aug 16, 2023
2 parents f66ee2f + c17aa36 commit af2869f
Show file tree
Hide file tree
Showing 16 changed files with 401 additions and 30 deletions.
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ axum-prometheus = "0.4.0"
async-trait = "0.1.72"

[dev-dependencies]
axum-macros = "0.3.8"
tempfile = "3.7.0"
tracing-test = { version = "0.2.4", features = ["no-env-filter"] }
uuid = { version = "1.4.1", features = ["v4"] }
Expand Down
4 changes: 3 additions & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
ignore:
- integration
- integration
- src/credhelper/memory.rs
- src/credhelper/faulty.rs
49 changes: 37 additions & 12 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use axum::{routing::get, Router};
use axum::{extract::FromRef, routing::get, Router};
use axum_prometheus::{
metrics_exporter_prometheus::PrometheusHandle, PrometheusMetricLayerBuilder,
};
Expand All @@ -10,31 +10,39 @@ use tower_http::{
};
use tracing::Level;

use crate::http::api::APIState;
use crate::{
config::Args, credhelper::database::DatabaseCredentials, http::artifacts::artifacts_handler,
http::healthcheck::healthcheck_handler, http::index::index_handler,
http::version::version_handler, refresh::RefreshRequest, registry::RegistryClient,
config::Args,
credhelper::{database::DatabaseCredentials, CredentialHelper},
http::artifacts::artifacts_handler,
http::healthcheck::healthcheck_handler,
http::index::index_handler,
http::version::version_handler,
refresh::RefreshRequest,
registry::RegistryClient,
};

#[derive(Clone)]
pub(crate) struct AppState {
pub(crate) struct AppState<C> {
pub(crate) s3_client: aws_sdk_s3::Client,
pub(crate) http_client: reqwest::Client,
pub(crate) db_client: Pool<Postgres>,
pub(crate) registry_client: RegistryClient<DatabaseCredentials>,
pub(crate) config: Args,
pub(crate) refresher_tx: mpsc::Sender<RefreshRequest>,
pub(crate) credentials: C,
}

impl AppState {
impl<C> AppState<C> {
pub(crate) fn new(
config: Args,
s3: aws_sdk_s3::Client,
db: Pool<Postgres>,
http: reqwest::Client,
refresher_tx: mpsc::Sender<RefreshRequest>,
) -> AppState {
AppState {
credentials: C,
) -> Self {
Self {
s3_client: s3,
http_client: http.clone(),
db_client: db.clone(),
Expand All @@ -45,15 +53,28 @@ impl AppState {
),
config,
refresher_tx,
credentials,
}
}
}

pub(crate) fn provider_mirror_app(
state: AppState,
impl<C: Clone> FromRef<AppState<C>> for APIState<C> {
fn from_ref(state: &AppState<C>) -> Self {
Self {
credentials: state.credentials.clone(),
}
}
}

pub(crate) fn provider_mirror_app<C: Clone + Send + Sync + CredentialHelper + 'static>(
state: AppState<C>,
metric_handle: Option<PrometheusHandle>,
) -> Router {
let metric_layer = PrometheusMetricLayerBuilder::new()
.with_group_patterns_as(
"/api/v1/credentials/:hostname",
&["/api/v1/credentials/:hostname"],
)
.with_group_patterns_as(
"/:hostname/:namespace/:provider_type/index.json",
&["/:hostname/:namespace/:provider_type/index.json"],
Expand All @@ -65,7 +86,8 @@ pub(crate) fn provider_mirror_app(
.with_group_patterns_as("/artifacts/:version_id", &["/artifacts/:version_id"])
.build();

Router::new()
let api = crate::http::api::routes(APIState::from_ref(&state));
let mirror = Router::new()
.route(
"/:hostname/:namespace/:provider_type/index.json",
get(index_handler),
Expand All @@ -79,7 +101,10 @@ pub(crate) fn provider_mirror_app(
.route(
"/metrics",
get(|| async move { metric_handle.map_or("".to_string(), |x| x.render()) }),
)
);
Router::new()
.merge(api)
.merge(mirror)
.layer(
TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().level(Level::INFO))
Expand Down
31 changes: 31 additions & 0 deletions src/credhelper/faulty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// Implementation of the memory credential helper that always throws an error
///
/// This is useful for testing the error handling of the credential helper
use async_trait::async_trait;

use super::{Credential, CredentialHelper};

#[derive(Clone, Debug)]
pub(crate) struct FaultyCredentials;

impl FaultyCredentials {
#[allow(dead_code)]
pub(crate) fn new() -> Self {
Self
}
}

#[async_trait]
impl CredentialHelper for FaultyCredentials {
async fn get(&self, _hostname: impl AsRef<str> + Send) -> Result<Credential, anyhow::Error> {
Err(anyhow::anyhow!("Error occurred"))
}

async fn store(&mut self, _hostname: String, _cred: String) -> Result<(), anyhow::Error> {
Err(anyhow::anyhow!("Error occurred"))
}

async fn forget(&mut self, _hostname: impl AsRef<str> + Send) -> Result<(), anyhow::Error> {
Err(anyhow::anyhow!("Error occurred"))
}
}
32 changes: 24 additions & 8 deletions src/credhelper/memory.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
use std::{collections::HashMap, marker::Send};
use std::{
collections::HashMap,
marker::Send,
sync::{Arc, RwLock},
};

use async_trait::async_trait;

use super::{types::Credential, CredentialHelper};

// Credential helper implementation by storing in the database
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct MemoryCredentials {
map: HashMap<String, Option<String>>,
map: Arc<RwLock<HashMap<String, Option<String>>>>,
}

impl MemoryCredentials {
pub fn new() -> Self {
Self {
map: HashMap::new(),
map: Arc::new(RwLock::new(HashMap::new())),
}
}
}
Expand All @@ -27,19 +31,31 @@ impl Default for MemoryCredentials {
#[async_trait]
impl CredentialHelper for MemoryCredentials {
async fn get(&self, hostname: impl AsRef<str> + Send) -> Result<Credential, anyhow::Error> {
Ok(self
.map
let map = self.map.try_read().map_err(|_| {
anyhow::anyhow!("Could not acquire read lock on in memory credential store")
})?;
Ok(map
.get(hostname.as_ref())
.map_or(Credential::NotFound, |v| Credential::Entry(v.clone())))
}

async fn store(&mut self, hostname: String, cred: String) -> Result<(), anyhow::Error> {
self.map.insert(hostname, Some(cred));
self.map
.try_write()
.map_err(|_| {
anyhow::anyhow!("Could not acquire write lock on in memory credential store")
})?
.insert(hostname, Some(cred));
Ok(())
}

async fn forget(&mut self, hostname: impl AsRef<str> + Send) -> Result<(), anyhow::Error> {
self.map.remove(hostname.as_ref());
self.map
.try_write()
.map_err(|_| {
anyhow::anyhow!("Could not acquire write lock on in memory credential store")
})?
.remove(hostname.as_ref());
Ok(())
}
}
1 change: 1 addition & 0 deletions src/credhelper/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod database;
pub mod faulty;
pub mod memory;
mod types;

Expand Down
2 changes: 1 addition & 1 deletion src/credhelper/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use async_trait::async_trait;
use reqwest::RequestBuilder;
use std::marker::Send;

#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Debug)]
pub enum Credential {
NotFound,
Entry(Option<String>),
Expand Down
23 changes: 23 additions & 0 deletions src/http/api/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use axum::{routing::post, Router};

use crate::credhelper::CredentialHelper;

use self::v1::credential::{delete, exists, update};

pub(crate) mod v1;

#[derive(Clone)]
pub(crate) struct APIState<C> {
pub(crate) credentials: C,
}

pub(crate) fn routes<S, C: Clone + Send + Sync + 'static + CredentialHelper>(
state: APIState<C>,
) -> Router<S> {
Router::new()
.route(
"/api/v1/credentials/:hostname",
post(update).delete(delete).get(exists),
)
.with_state(state)
}
Loading

0 comments on commit af2869f

Please sign in to comment.