From e15d2ae2d0e1b6ce8b0e898c111fe7af2266dbb0 Mon Sep 17 00:00:00 2001 From: henry0715-dev Date: Thu, 22 Aug 2024 16:27:44 +0900 Subject: [PATCH] Add `updateTrustedDomain` GraphQL API Closes #2 --- CHANGELOG.md | 8 ++ Cargo.toml | 1 + src/graphql.rs | 1 + src/graphql/trusted_domain.rs | 142 +++++++++++++++++++++++++++++++++- 4 files changed, 150 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e632f..dd8c9db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ file is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Added the `updateTrustedDomain` GraphQL API to modify the list of trusted + domains. + ## [0.24.0] - 2024-11-19 ### Added @@ -732,6 +739,7 @@ Versioning](https://semver.org/spec/v2.0.0.html). - An initial version. +[Unreleased] https://github.com/aicers/review-web/compare/0.24.0...main [0.24.0]: https://github.com/aicers/review-web/compare/0.23.0...0.24.0 [0.23.0]: https://github.com/aicers/review-web/compare/0.22.0...0.23.0 [0.22.0]: https://github.com/aicers/review-web/compare/0.21.0...0.22.0 diff --git a/Cargo.toml b/Cargo.toml index e8a4d4c..7af76e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ tokio = "1" tower-http = { version = "0.6", features = ["fs", "trace"] } tracing = "0.1" vinum = { git = "https://github.com/vinesystems/vinum.git", tag = "1.0.3" } +regex = "1.11.1" [dev-dependencies] assert-json-diff = "2" diff --git a/src/graphql.rs b/src/graphql.rs index 9cb3717..da6fce6 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -728,6 +728,7 @@ mod tests { #[tokio::test] async fn unimplemented_agent_manager() { let agent_manager = super::MockAgentManager {}; + assert!(agent_manager.broadcast_trusted_domains().await.is_ok()); assert!(agent_manager .broadcast_trusted_user_agent_list(&[]) .await diff --git a/src/graphql/trusted_domain.rs b/src/graphql/trusted_domain.rs index 429b7c5..73cc35c 100644 --- a/src/graphql/trusted_domain.rs +++ b/src/graphql/trusted_domain.rs @@ -1,7 +1,8 @@ use async_graphql::{ connection::{Connection, EmptyFields}, - Context, Object, Result, SimpleObject, + Context, InputObject, Object, Result, SimpleObject, }; +use regex::Regex; use super::{AgentManager, BoxedAgentManager, Role, RoleGuard}; use crate::graphql::query_with_constraints; @@ -62,6 +63,33 @@ impl TrustedDomainMutation { Ok(name) } + /// Update a trusted domain, returning the new value if it passes domain validation. + #[graphql(guard = "RoleGuard::new(Role::SystemAdministrator) + .or(RoleGuard::new(Role::SecurityAdministrator))")] + async fn update_trusted_domain( + &self, + ctx: &Context<'_>, + old: TrustedDomainInput, + new: TrustedDomainInput, + ) -> Result { + if !is_valid_domain(&new.name) { + return Err(TrustedDomainError::InvalidDomainName(String::from(&new.name)).into()); + } + + let name = { + let store = crate::graphql::get_store(ctx).await?; + let map = store.trusted_domain_map(); + let old = review_database::TrustedDomain::from(old); + let new = review_database::TrustedDomain::from(new); + map.update(&old, &new)?; + new.name + }; + + let agent_manager = ctx.data::()?; + agent_manager.broadcast_trusted_domains().await?; + Ok(name) + } + /// Removes a trusted domain, returning the old value if it existed. #[graphql( guard = "RoleGuard::new(Role::SystemAdministrator).or(RoleGuard::new(Role::SecurityAdministrator))" @@ -94,6 +122,21 @@ impl From for TrustedDomain { } } +#[derive(InputObject)] +pub(super) struct TrustedDomainInput { + name: String, + remarks: String, +} + +impl From for review_database::TrustedDomain { + fn from(input: TrustedDomainInput) -> Self { + Self { + name: input.name, + remarks: input.remarks, + } + } +} + async fn load( ctx: &Context<'_>, after: Option, @@ -106,9 +149,25 @@ async fn load( super::load_edges(&map, after, before, first, last, EmptyFields) } +fn is_valid_domain(domain: &str) -> bool { + let domain_regex = + Regex::new(r"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$").unwrap(); + domain_regex.is_match(domain) +} + +#[derive(Debug, thiserror::Error)] +#[allow(clippy::module_name_repetitions)] +pub enum TrustedDomainError { + #[error("Invalid domain name: {0}")] + InvalidDomainName(String), +} + #[cfg(test)] mod tests { - use crate::graphql::TestSchema; + use std::net::SocketAddr; + + use crate::graphql::trusted_domain::is_valid_domain; + use crate::graphql::{BoxedAgentManager, MockAgentManager, TestSchema}; #[tokio::test] async fn trusted_domain_list() { @@ -157,4 +216,83 @@ mod tests { r#"{trustedDomainList: {edges: [{node: {name: "example2.org"}}]}}"# ); } + + #[tokio::test] + async fn update_trusted_domain() { + let agent_manager: BoxedAgentManager = Box::new(MockAgentManager {}); + let test_addr: SocketAddr = "127.0.0.1:8080".parse().unwrap(); + let schema = TestSchema::new_with(agent_manager, Some(test_addr)).await; + let insert_query = r#" + mutation { + insertTrustedDomain( + name: "test.com" + remarks: "origin_remarks" + ) + } + "#; + let update_query = r#" + mutation { + updateTrustedDomain( + old: { + name: "test.com" + remarks: "origin_remarks" + } + new: { + name: "test2.com" + remarks: "updated_remarks" + } + ) + } + "#; + + let update_error_query = r#" + mutation { + updateTrustedDomain( + old: { + name: "test2.com" + remarks: "origin_remarks" + } + new: { + name: "test" + remarks: "updated_remarks" + } + ) + } + "#; + let res = schema.execute(update_query).await; + assert_eq!( + res.errors.first().unwrap().message, + "no such entry".to_string() + ); + + let res = schema.execute(insert_query).await; + assert_eq!(res.data.to_string(), r#"{insertTrustedDomain: "test.com"}"#); + + let res = schema.execute(update_query).await; + assert_eq!( + res.data.to_string(), + r#"{updateTrustedDomain: "test2.com"}"# + ); + + let res = schema.execute(update_error_query).await; + assert_eq!( + res.errors.first().unwrap().message, + "Invalid domain name: test".to_string() + ); + } + + #[test] + fn valid_domain() { + let test_domains = vec![ + "ex.com", + "test.domain.co.kr", + "test.or.org", + "test-1.sample", + "error", + ]; + + let res: Vec<_> = test_domains.iter().map(|&x| is_valid_domain(x)).collect(); + let expect = vec![true, true, true, true, false]; + assert_eq!(res, expect); + } }