From 9a1df9f2a816baa4be364f3dc4720a2a116d74fc Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 6 Sep 2024 20:09:13 -0300 Subject: [PATCH] feat: implemented fetch secrets --- src/domain/project/cache.rs | 2 +- src/domain/project/command.rs | 72 +++++++++++++++++++++++++++-------- src/driven/cache/project.rs | 7 ++-- src/drivers/grpc/project.rs | 55 ++++++++++++++------------ 4 files changed, 93 insertions(+), 43 deletions(-) diff --git a/src/domain/project/cache.rs b/src/domain/project/cache.rs index 2661ad0..824d594 100644 --- a/src/domain/project/cache.rs +++ b/src/domain/project/cache.rs @@ -19,7 +19,7 @@ pub trait ProjectDrivenCache: Send + Sync { async fn update(&self, project: &ProjectUpdate) -> Result<()>; async fn delete(&self, id: &str, deleted_at: &DateTime) -> Result<()>; async fn create_secret(&self, secret: &ProjectSecret) -> Result<()>; - async fn find_secret_by_project_id(&self, project_id: &str) -> Result>; + async fn find_secrets(&self, project_id: &str) -> Result>; async fn find_users( &self, project_id: &str, diff --git a/src/domain/project/command.rs b/src/domain/project/command.rs index b1bdbb6..78b5f3e 100644 --- a/src/domain/project/command.rs +++ b/src/domain/project/command.rs @@ -41,7 +41,8 @@ pub async fn create( let user_id = assert_credential(&cmd.credential)?; if cache.find_by_namespace(&cmd.namespace).await?.is_some() { - return Err(Error::CommandMalformed("invalid project namespace".into())); } + return Err(Error::CommandMalformed("invalid project namespace".into())); + } let (name, email) = auth0.find_info(&user_id).await?; let billing_provider_id = stripe.create_customer(&name, &email).await?; @@ -115,6 +116,16 @@ pub async fn delete( Ok(()) } +pub async fn fetch_secret( + cache: Arc, + cmd: FetchSecretCmd, +) -> Result> { + assert_credential(&cmd.credential)?; + assert_permission(cache.clone(), &cmd.credential, &cmd.project_id).await?; + + cache.find_secrets(&cmd.project_id).await +} + pub async fn create_secret( cache: Arc, event: Arc, @@ -127,7 +138,7 @@ pub async fn create_secret( return Err(Error::CommandMalformed("invalid project id".into())); }; - let secrets = cache.find_secret_by_project_id(&cmd.project_id).await?; + let secrets = cache.find_secrets(&cmd.project_id).await?; if secrets.len() >= MAX_SECRET { return Err(Error::SecretExceeded(format!( "secrets exceeded the limit of {MAX_SECRET}" @@ -187,7 +198,7 @@ pub async fn verify_secret( return Err(Error::Unauthorized("invalid project secret".into())); } - let secrets = cache.find_secret_by_project_id(&cmd.project_id).await?; + let secrets = cache.find_secrets(&cmd.project_id).await?; let secret = secrets.into_iter().find(|project_secret| { let argon2 = Argon2::new_with_secret( @@ -428,6 +439,20 @@ impl DeleteCmd { } } +#[derive(Debug, Clone)] +pub struct FetchSecretCmd { + pub credential: Credential, + pub project_id: String, +} +impl FetchSecretCmd { + pub fn new(credential: Credential, project_id: String) -> Self { + Self { + credential, + project_id, + } + } +} + #[derive(Debug, Clone)] pub struct CreateSecretCmd { pub credential: Credential, @@ -610,6 +635,14 @@ mod tests { } } } + impl Default for FetchSecretCmd { + fn default() -> Self { + Self { + credential: Credential::Auth0("user id".into()), + project_id: Uuid::new_v4().to_string(), + } + } + } impl Default for CreateSecretCmd { fn default() -> Self { Self { @@ -773,9 +806,7 @@ mod tests { cache .expect_find_by_id() .return_once(|_| Ok(Some(Project::default()))); - cache - .expect_find_secret_by_project_id() - .return_once(|_| Ok(Vec::new())); + cache.expect_find_secrets().return_once(|_| Ok(Vec::new())); let mut event = MockEventDrivenBridge::new(); event.expect_dispatch().return_once(|_| Ok(())); @@ -786,6 +817,21 @@ mod tests { assert!(result.is_ok()); } + #[tokio::test] + async fn it_should_fetch_project_secrets() { + let mut cache = MockProjectDrivenCache::new(); + cache + .expect_find_user_permission() + .return_once(|_, _| Ok(Some(ProjectUser::default()))); + cache + .expect_find_secrets() + .return_once(|_| Ok(vec![ProjectSecret::default()])); + + let cmd = FetchSecretCmd::default(); + + let result = fetch_secret(Arc::new(cache), cmd).await; + assert!(result.is_ok()); + } #[tokio::test] async fn it_should_create_project_secret() { let mut cache = MockProjectDrivenCache::new(); @@ -795,9 +841,7 @@ mod tests { cache .expect_find_by_id() .return_once(|_| Ok(Some(Project::default()))); - cache - .expect_find_secret_by_project_id() - .return_once(|_| Ok(Vec::new())); + cache.expect_find_secrets().return_once(|_| Ok(Vec::new())); let mut event = MockEventDrivenBridge::new(); event.expect_dispatch().return_once(|_| Ok(())); @@ -859,7 +903,7 @@ mod tests { .expect_find_by_id() .return_once(|_| Ok(Some(Project::default()))); cache - .expect_find_secret_by_project_id() + .expect_find_secrets() .return_once(|_| Ok(vec![ProjectSecret::default(); 3])); let event = MockEventDrivenBridge::new(); @@ -874,7 +918,7 @@ mod tests { async fn it_should_verify_secret() { let mut cache = MockProjectDrivenCache::new(); cache - .expect_find_secret_by_project_id() + .expect_find_secrets() .return_once(|_| Ok(vec![ProjectSecret::default()])); let cmd = VerifySecretCmd::default(); @@ -886,7 +930,7 @@ mod tests { async fn it_should_fail_verify_secret_when_invalid_key() { let mut cache = MockProjectDrivenCache::new(); cache - .expect_find_secret_by_project_id() + .expect_find_secrets() .return_once(|_| Ok(vec![ProjectSecret::default()])); let cmd = VerifySecretCmd { @@ -924,9 +968,7 @@ mod tests { #[tokio::test] async fn it_should_fail_verify_secret_when_there_arent_secrets_storaged() { let mut cache = MockProjectDrivenCache::new(); - cache - .expect_find_secret_by_project_id() - .return_once(|_| Ok(vec![])); + cache.expect_find_secrets().return_once(|_| Ok(vec![])); let cmd = VerifySecretCmd::default(); diff --git a/src/driven/cache/project.rs b/src/driven/cache/project.rs index 596238f..20f2d76 100644 --- a/src/driven/cache/project.rs +++ b/src/driven/cache/project.rs @@ -284,7 +284,7 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { Ok(()) } - async fn find_secret_by_project_id(&self, project_id: &str) -> Result> { + async fn find_secrets(&self, project_id: &str) -> Result> { let secrets = sqlx::query_as::<_, ProjectSecret>( r#" SELECT @@ -295,7 +295,8 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { ps.secret, ps.created_at FROM project_secret ps - WHERE ps.project_id = $1; + WHERE ps.project_id = $1 + ORDER BY ps.created_at DESC; "#, ) .bind(project_id) @@ -691,7 +692,7 @@ mod tests { }; cache.create_secret(&secret).await.unwrap(); - let result = cache.find_secret_by_project_id(&project.id).await; + let result = cache.find_secrets(&project.id).await; assert!(result.is_ok()); assert!(result.unwrap().len() == 1); diff --git a/src/drivers/grpc/project.rs b/src/drivers/grpc/project.rs index 988fb4b..e568663 100644 --- a/src/drivers/grpc/project.rs +++ b/src/drivers/grpc/project.rs @@ -2,13 +2,12 @@ use dmtri::demeter::ops::v1alpha as proto; use std::{sync::Arc, time::Duration}; use tonic::{async_trait, Status}; use tracing::error; -use uuid::Uuid; use crate::domain::{ auth::{Auth0Driven, Credential}, event::EventDrivenBridge, project::{ - self, cache::ProjectDrivenCache, Project, ProjectEmailDriven, ProjectUser, + self, cache::ProjectDrivenCache, Project, ProjectEmailDriven, ProjectSecret, ProjectUser, ProjectUserInvite, StripeDriven, }, }; @@ -146,7 +145,26 @@ impl proto::project_service_server::ProjectService for ProjectServiceImpl { Ok(tonic::Response::new(message)) } + async fn fetch_project_secrets( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let credential = match request.extensions().get::() { + Some(credential) => credential.clone(), + None => return Err(Status::unauthenticated("invalid credential")), + }; + + let req = request.into_inner(); + let cmd = project::command::FetchSecretCmd::new(credential, req.project_id); + + let secrets = project::command::fetch_secret(self.cache.clone(), cmd.clone()).await?; + + let records = secrets.into_iter().map(|v| v.into()).collect(); + let message = proto::FetchProjectSecretsResponse { records }; + + Ok(tonic::Response::new(message)) + } async fn create_project_secret( &self, request: tonic::Request, @@ -177,28 +195,6 @@ impl proto::project_service_server::ProjectService for ProjectServiceImpl { Ok(tonic::Response::new(message)) } - async fn fetch_project_secrets( - &self, - request: tonic::Request, - ) -> Result, tonic::Status> { - let _credential = match request.extensions().get::() { - Some(credential) => credential.clone(), - None => return Err(Status::unauthenticated("invalid credential")), - }; - - let req = request.into_inner(); - - let message = proto::FetchProjectSecretsResponse { - records: vec![proto::ProjectSecret { - id: Uuid::new_v4().to_string(), - name: "Secret Name".into(), - project_id: req.project_id, - ..Default::default() - }], - }; - - Ok(tonic::Response::new(message)) - } async fn fetch_project_users( &self, request: tonic::Request, @@ -350,3 +346,14 @@ impl From for proto::ProjectUser { } } } + +impl From for proto::ProjectSecret { + fn from(value: ProjectSecret) -> Self { + Self { + id: value.id, + project_id: value.project_id, + name: value.name, + created_at: value.created_at.to_rfc3339(), + } + } +}