From 4e407bef971a7fcbf4656f378124e321a4506790 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Thu, 26 Sep 2024 15:05:53 -0300 Subject: [PATCH] feat: implemented project secret deletion --- ...63c926ee390f8a93d6e203c7283cd97ba7e7a.json | 12 ++++ Cargo.lock | 2 +- src/domain/event/mod.rs | 22 ++++++ src/domain/project/cache.rs | 22 +++++- src/domain/project/command.rs | 68 ++++++++++++++++++- src/driven/cache/project.rs | 54 +++++++++++++++ src/drivers/cache/mod.rs | 3 + src/drivers/grpc/project.rs | 17 +++++ 8 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 .sqlx/query-ab3d742e1ccdeb23fed0f17695a63c926ee390f8a93d6e203c7283cd97ba7e7a.json diff --git a/.sqlx/query-ab3d742e1ccdeb23fed0f17695a63c926ee390f8a93d6e203c7283cd97ba7e7a.json b/.sqlx/query-ab3d742e1ccdeb23fed0f17695a63c926ee390f8a93d6e203c7283cd97ba7e7a.json new file mode 100644 index 0000000..8122ed1 --- /dev/null +++ b/.sqlx/query-ab3d742e1ccdeb23fed0f17695a63c926ee390f8a93d6e203c7283cd97ba7e7a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM\n project_secret\n WHERE \n id=$1;\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "ab3d742e1ccdeb23fed0f17695a63c926ee390f8a93d6e203c7283cd97ba7e7a" +} diff --git a/Cargo.lock b/Cargo.lock index b750af8..8f3be47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -965,7 +965,7 @@ dependencies = [ [[package]] name = "dmtri" version = "0.1.0" -source = "git+https://github.com/demeter-run/specs.git#70f4c78447d28f771456cdfeca968c65165a6dd8" +source = "git+https://github.com/demeter-run/specs.git#25e4853c635e1fab25305543e59a280fd8742ab6" dependencies = [ "bytes", "pbjson", diff --git a/src/domain/event/mod.rs b/src/domain/event/mod.rs index c21c7e7..74de19e 100644 --- a/src/domain/event/mod.rs +++ b/src/domain/event/mod.rs @@ -61,6 +61,14 @@ pub struct ProjectSecretCreated { } into_event!(ProjectSecretCreated); +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectSecretDeleted { + pub id: String, + pub deleted_by: String, + pub deleted_at: DateTime, +} +into_event!(ProjectSecretDeleted); + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProjectUserInviteCreated { pub id: String, @@ -156,6 +164,7 @@ pub enum Event { ProjectUpdated(ProjectUpdated), ProjectDeleted(ProjectDeleted), ProjectSecretCreated(ProjectSecretCreated), + ProjectSecretDeleted(ProjectSecretDeleted), ProjectUserInviteCreated(ProjectUserInviteCreated), ProjectUserInviteAccepted(ProjectUserInviteAccepted), ProjectUserDeleted(ProjectUserDeleted), @@ -171,6 +180,7 @@ impl Event { Event::ProjectUpdated(_) => "ProjectUpdated".into(), Event::ProjectDeleted(_) => "ProjectDeleted".into(), Event::ProjectSecretCreated(_) => "ProjectSecretCreated".into(), + Event::ProjectSecretDeleted(_) => "ProjectSecretDeleted".into(), Event::ProjectUserInviteCreated(_) => "ProjectUserInviteCreated".into(), Event::ProjectUserInviteAccepted(_) => "ProjectUserInviteAccepted".into(), Event::ProjectUserDeleted(_) => "ProjectUserDeleted".into(), @@ -188,6 +198,9 @@ impl Event { "ProjectSecretCreated" => { Ok(Self::ProjectSecretCreated(serde_json::from_slice(payload)?)) } + "ProjectSecretDeleted" => { + Ok(Self::ProjectSecretDeleted(serde_json::from_slice(payload)?)) + } "ProjectUserInviteCreated" => Ok(Self::ProjectUserInviteCreated( serde_json::from_slice(payload)?, )), @@ -256,6 +269,15 @@ mod tests { } } } + impl Default for ProjectSecretDeleted { + fn default() -> Self { + Self { + id: Uuid::new_v4().to_string(), + deleted_by: Uuid::new_v4().to_string(), + deleted_at: Utc::now(), + } + } + } impl Default for ProjectUserInviteCreated { fn default() -> Self { Self { diff --git a/src/domain/project/cache.rs b/src/domain/project/cache.rs index 112a0ff..4e14ca5 100644 --- a/src/domain/project/cache.rs +++ b/src/domain/project/cache.rs @@ -2,8 +2,8 @@ use chrono::{DateTime, Utc}; use std::sync::Arc; use crate::domain::event::{ - ProjectCreated, ProjectDeleted, ProjectSecretCreated, ProjectUpdated, ProjectUserDeleted, - ProjectUserInviteAccepted, ProjectUserInviteCreated, + ProjectCreated, ProjectDeleted, ProjectSecretCreated, ProjectSecretDeleted, ProjectUpdated, + ProjectUserDeleted, ProjectUserInviteAccepted, ProjectUserInviteCreated, }; use crate::domain::Result; @@ -20,6 +20,8 @@ pub trait ProjectDrivenCache: Send + Sync { async fn delete(&self, id: &str, deleted_at: &DateTime) -> Result<()>; async fn create_secret(&self, secret: &ProjectSecret) -> Result<()>; async fn find_secrets(&self, project_id: &str) -> Result>; + async fn find_secret_by_id(&self, id: &str) -> Result>; + async fn delete_secret(&self, id: &str) -> Result<()>; async fn find_users( &self, project_id: &str, @@ -62,6 +64,12 @@ pub async fn create_secret( ) -> Result<()> { cache.create_secret(&evt.into()).await } +pub async fn delete_secret( + cache: Arc, + evt: ProjectSecretDeleted, +) -> Result<()> { + cache.delete_secret(&evt.id).await +} pub async fn create_user_invite( cache: Arc, @@ -111,6 +119,16 @@ mod tests { let result = create_secret(Arc::new(cache), evt).await; assert!(result.is_ok()); } + #[tokio::test] + async fn it_should_delete_project_secret_cache() { + let mut cache = MockProjectDrivenCache::new(); + cache.expect_delete_secret().return_once(|_| Ok(())); + + let evt = ProjectSecretDeleted::default(); + + let result = delete_secret(Arc::new(cache), evt).await; + assert!(result.is_ok()); + } #[tokio::test] async fn it_should_create_user_invite_cache() { diff --git a/src/domain/project/command.rs b/src/domain/project/command.rs index fe4ea12..9601429 100644 --- a/src/domain/project/command.rs +++ b/src/domain/project/command.rs @@ -13,8 +13,9 @@ use crate::domain::{ auth::{Auth0Driven, Credential, UserId}, error::Error, event::{ - EventDrivenBridge, ProjectCreated, ProjectDeleted, ProjectSecretCreated, ProjectUpdated, - ProjectUserDeleted, ProjectUserInviteAccepted, ProjectUserInviteCreated, + EventDrivenBridge, ProjectCreated, ProjectDeleted, ProjectSecretCreated, + ProjectSecretDeleted, ProjectUpdated, ProjectUserDeleted, ProjectUserInviteAccepted, + ProjectUserInviteCreated, }, project::{ProjectStatus, ProjectUserInviteStatus}, utils, Result, MAX_SECRET, PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX, @@ -233,6 +234,30 @@ pub async fn verify_secret( Ok(secret) } +pub async fn delete_secret( + cache: Arc, + event: Arc, + cmd: DeleteSecretCmd, +) -> Result<()> { + let user_id = assert_credential(&cmd.credential)?; + + let Some(secret) = cache.find_secret_by_id(&cmd.id).await? else { + return Err(Error::CommandMalformed("invalid secret id".into())); + }; + + assert_permission(cache.clone(), &cmd.credential, &secret.project_id, None).await?; + + let evt = ProjectSecretDeleted { + id: secret.id, + deleted_by: user_id, + deleted_at: Utc::now(), + }; + + event.dispatch(evt.into()).await?; + info!(secret = &cmd.id, "project secret deleted"); + + Ok(()) +} pub async fn fetch_user( cache: Arc, @@ -577,6 +602,17 @@ pub struct VerifySecretCmd { pub key: String, } +#[derive(Debug, Clone)] +pub struct DeleteSecretCmd { + pub credential: Credential, + pub id: String, +} +impl DeleteSecretCmd { + pub fn new(credential: Credential, id: String) -> Self { + Self { credential, id } + } +} + #[derive(Debug, Clone)] pub struct FetchUserCmd { pub credential: Credential, @@ -786,6 +822,14 @@ mod tests { } } } + impl Default for DeleteSecretCmd { + fn default() -> Self { + Self { + credential: Credential::Auth0("user id".into()), + id: Uuid::new_v4().to_string(), + } + } + } impl Default for FetchUserInviteCmd { fn default() -> Self { Self { @@ -1125,6 +1169,26 @@ mod tests { assert!(result.is_err()); } + #[tokio::test] + async fn it_should_delete_secret() { + let mut cache = MockProjectDrivenCache::new(); + cache + .expect_find_user_permission() + .return_once(|_, _| Ok(Some(ProjectUser::default()))); + + cache + .expect_find_secret_by_id() + .return_once(|_| Ok(Some(ProjectSecret::default()))); + + let mut event = MockEventDrivenBridge::new(); + event.expect_dispatch().return_once(|_| Ok(())); + + let cmd = DeleteSecretCmd::default(); + + let result = delete_secret(Arc::new(cache), Arc::new(event), cmd).await; + assert!(result.is_ok()); + } + #[tokio::test] async fn it_should_fetch_project_user_invites() { let mut cache = MockProjectDrivenCache::new(); diff --git a/src/driven/cache/project.rs b/src/driven/cache/project.rs index 367147c..baaeb4c 100644 --- a/src/driven/cache/project.rs +++ b/src/driven/cache/project.rs @@ -305,6 +305,41 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { Ok(secrets) } + async fn find_secret_by_id(&self, id: &str) -> Result> { + let secret = sqlx::query_as::<_, ProjectSecret>( + r#" + SELECT + ps.id, + ps.project_id, + ps.name, + ps.phc, + ps.secret, + ps.created_at + FROM project_secret ps + WHERE ps.id = $1; + "#, + ) + .bind(id) + .fetch_optional(&self.sqlite.db) + .await?; + + Ok(secret) + } + async fn delete_secret(&self, id: &str) -> Result<()> { + sqlx::query!( + r#" + DELETE FROM + project_secret + WHERE + id=$1; + "#, + id, + ) + .execute(&self.sqlite.db) + .await?; + + Ok(()) + } async fn find_user_permission( &self, user_id: &str, @@ -737,6 +772,25 @@ mod tests { assert!(result.unwrap().len() == 1); } + #[tokio::test] + async fn it_should_find_secret_by_id() { + let cache = get_cache().await; + + let project = Project::default(); + cache.create(&project).await.unwrap(); + + let secret = ProjectSecret { + project_id: project.id.clone(), + ..Default::default() + }; + cache.create_secret(&secret).await.unwrap(); + + let result = cache.find_secret_by_id(&secret.id).await; + + assert!(result.is_ok()); + assert!(result.unwrap().is_some()); + } + #[tokio::test] async fn it_should_find_user_permission() { let cache = get_cache().await; diff --git a/src/drivers/cache/mod.rs b/src/drivers/cache/mod.rs index a29db46..3263b84 100644 --- a/src/drivers/cache/mod.rs +++ b/src/drivers/cache/mod.rs @@ -59,6 +59,9 @@ pub async fn subscribe(config: CacheConfig) -> Result<()> { Event::ProjectSecretCreated(evt) => { project::cache::create_secret(project_cache.clone(), evt.clone()).await } + Event::ProjectSecretDeleted(evt) => { + project::cache::delete_secret(project_cache.clone(), evt.clone()).await + } Event::ProjectUserInviteCreated(evt) => { project::cache::create_user_invite(project_cache.clone(), evt.clone()).await } diff --git a/src/drivers/grpc/project.rs b/src/drivers/grpc/project.rs index 1126592..630e7fb 100644 --- a/src/drivers/grpc/project.rs +++ b/src/drivers/grpc/project.rs @@ -195,6 +195,23 @@ impl proto::project_service_server::ProjectService for ProjectServiceImpl { Ok(tonic::Response::new(message)) } + async fn delete_project_secret( + &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::DeleteSecretCmd::new(credential, req.id); + project::command::delete_secret(self.cache.clone(), self.event.clone(), cmd.clone()) + .await?; + let message = proto::DeleteProjectSecretResponse {}; + + Ok(tonic::Response::new(message)) + } async fn fetch_project_users( &self, request: tonic::Request,