From d38a2cd08b08e78b26e69dc2ced0994804406023 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Sat, 5 Apr 2025 00:24:24 -0500 Subject: [PATCH] server: extract graphql crate --- Cargo.lock | 27 +++++ crates/graphql-server/Cargo.toml | 75 ++++++++++++++ crates/graphql-server/src/api.rs | 91 +++++++++++++++++ .../graphql-server/src/lib.rs | 0 .../graphql-server/src}/mutation.rs | 10 +- .../graphql-server/src}/query.rs | 8 +- server/Cargo.toml | 3 + .../{graphql/api.rs => graphql_server.rs} | 99 +------------------ server/src/infra/mod.rs | 2 +- server/src/infra/tcp_server.rs | 2 +- server/src/main.rs | 4 +- 11 files changed, 220 insertions(+), 101 deletions(-) create mode 100644 crates/graphql-server/Cargo.toml create mode 100644 crates/graphql-server/src/api.rs rename server/src/infra/graphql/mod.rs => crates/graphql-server/src/lib.rs (100%) rename {server/src/infra/graphql => crates/graphql-server/src}/mutation.rs (99%) rename {server/src/infra/graphql => crates/graphql-server/src}/query.rs (99%) rename server/src/infra/{graphql/api.rs => graphql_server.rs} (65%) diff --git a/Cargo.lock b/Cargo.lock index 42f6787..8f1bd1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2544,6 +2544,7 @@ dependencies = [ "lldap_domain_handlers", "lldap_domain_model", "lldap_frontend_options", + "lldap_graphql_server", "lldap_ldap", "lldap_opaque_handler", "lldap_sql_backend_handler", @@ -2712,6 +2713,32 @@ dependencies = [ "serde", ] +[[package]] +name = "lldap_graphql_server" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "juniper", + "lldap_access_control", + "lldap_auth", + "lldap_domain", + "lldap_domain_handlers", + "lldap_domain_model", + "lldap_ldap", + "lldap_sql_backend_handler", + "lldap_test_utils", + "lldap_validation", + "mockall", + "pretty_assertions", + "serde", + "serde_json", + "tokio", + "tracing", + "urlencoding", + "uuid 1.11.0", +] + [[package]] name = "lldap_ldap" version = "0.1.0" diff --git a/crates/graphql-server/Cargo.toml b/crates/graphql-server/Cargo.toml new file mode 100644 index 0000000..c88f30c --- /dev/null +++ b/crates/graphql-server/Cargo.toml @@ -0,0 +1,75 @@ +[package] +name = "lldap_graphql_server" +version = "0.1.0" +description = "GraphQL server for LLDAP" +edition.workspace = true +authors.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +anyhow = "*" +juniper = "0.15" +serde_json = "1" +tracing = "*" +urlencoding = "2" + +[dependencies.chrono] +features = ["serde"] +version = "*" + +[dependencies.lldap_access_control] +path = "../access-control" + +[dependencies.lldap_auth] +path = "../auth" +features = ["opaque_server", "opaque_client", "sea_orm"] + +[dependencies.lldap_domain] +path = "../domain" + +[dependencies.lldap_domain_model] +path = "../domain-model" + +[dependencies.lldap_domain_handlers] +path = "../domain-handlers" + +[dependencies.lldap_ldap] +path = "../ldap" + +[dependencies.lldap_sql_backend_handler] +path = "../sql-backend-handler" + +[dependencies.lldap_validation] +path = "../validation" + +[dependencies.serde] +workspace = true + +[dependencies.uuid] +features = ["v1", "v3"] +version = "1" + +[dev-dependencies] +mockall = "0.11.4" +pretty_assertions = "1" + +#[dev-dependencies.lldap_auth] +#path = "../auth" +#features = ["test"] +# +#[dev-dependencies.lldap_opaque_handler] +#path = "../opaque-handler" +#features = ["test"] + +[dev-dependencies.lldap_test_utils] +path = "../test-utils" +# +#[dev-dependencies.lldap_sql_backend_handler] +#path = "../sql-backend-handler" +#features = ["test"] + +[dev-dependencies.tokio] +features = ["full"] +version = "1.25" diff --git a/crates/graphql-server/src/api.rs b/crates/graphql-server/src/api.rs new file mode 100644 index 0000000..cff9d16 --- /dev/null +++ b/crates/graphql-server/src/api.rs @@ -0,0 +1,91 @@ +use crate::{mutation::Mutation, query::Query}; +use juniper::{EmptySubscription, FieldError, RootNode}; +use lldap_access_control::{ + AccessControlledBackendHandler, AdminBackendHandler, ReadonlyBackendHandler, + UserReadableBackendHandler, UserWriteableBackendHandler, +}; +use lldap_auth::{access_control::ValidationResults, types::UserId}; +use lldap_domain_handlers::handler::BackendHandler; +use tracing::debug; + +pub struct Context { + pub handler: AccessControlledBackendHandler, + pub validation_result: ValidationResults, +} + +pub fn field_error_callback<'a>( + span: &'a tracing::Span, + error_message: &'a str, +) -> impl 'a + FnOnce() -> FieldError { + move || { + span.in_scope(|| debug!("Unauthorized")); + FieldError::from(error_message) + } +} + +impl Context { + #[cfg(test)] + pub fn new_for_tests(handler: Handler, validation_result: ValidationResults) -> Self { + Self { + handler: AccessControlledBackendHandler::new(handler), + validation_result, + } + } + + pub fn get_admin_handler(&self) -> Option<&(impl AdminBackendHandler + use)> { + self.handler.get_admin_handler(&self.validation_result) + } + + pub fn get_readonly_handler(&self) -> Option<&(impl ReadonlyBackendHandler + use)> { + self.handler.get_readonly_handler(&self.validation_result) + } + + pub fn get_writeable_handler( + &self, + user_id: &UserId, + ) -> Option<&(impl UserWriteableBackendHandler + use)> { + self.handler + .get_writeable_handler(&self.validation_result, user_id) + } + + pub fn get_readable_handler( + &self, + user_id: &UserId, + ) -> Option<&(impl UserReadableBackendHandler + use)> { + self.handler + .get_readable_handler(&self.validation_result, user_id) + } +} + +impl juniper::Context for Context {} + +type Schema = + RootNode<'static, Query, Mutation, EmptySubscription>>; + +pub fn schema() -> Schema { + Schema::new( + Query::::new(), + Mutation::::new(), + EmptySubscription::>::new(), + ) +} + +pub fn export_schema(output_file: Option) -> anyhow::Result<()> { + use anyhow::Context; + use lldap_sql_backend_handler::SqlBackendHandler; + let output = schema::().as_schema_language(); + match output_file { + None => println!("{}", output), + Some(path) => { + use std::fs::File; + use std::io::prelude::*; + use std::path::Path; + let path = Path::new(&path); + let mut file = + File::create(path).context(format!("unable to open '{}'", path.display()))?; + file.write_all(output.as_bytes()) + .context(format!("unable to write in '{}'", path.display()))?; + } + } + Ok(()) +} diff --git a/server/src/infra/graphql/mod.rs b/crates/graphql-server/src/lib.rs similarity index 100% rename from server/src/infra/graphql/mod.rs rename to crates/graphql-server/src/lib.rs diff --git a/server/src/infra/graphql/mutation.rs b/crates/graphql-server/src/mutation.rs similarity index 99% rename from server/src/infra/graphql/mutation.rs rename to crates/graphql-server/src/mutation.rs index 913d522..c333869 100644 --- a/server/src/infra/graphql/mutation.rs +++ b/crates/graphql-server/src/mutation.rs @@ -1,4 +1,4 @@ -use crate::infra::graphql::api::{Context, field_error_callback}; +use crate::api::{Context, field_error_callback}; use anyhow::{Context as AnyhowContext, anyhow}; use juniper::{FieldError, FieldResult, GraphQLInputObject, GraphQLObject, graphql_object}; use lldap_access_control::{ @@ -29,6 +29,12 @@ pub struct Mutation { _phantom: std::marker::PhantomData>, } +impl Default for Mutation { + fn default() -> Self { + Self::new() + } +} + impl Mutation { pub fn new() -> Self { Self { @@ -778,7 +784,7 @@ fn deserialize_attribute( #[cfg(test)] mod tests { use super::*; - use crate::infra::graphql::query::Query; + use crate::query::Query; use juniper::{ DefaultScalarValue, EmptySubscription, GraphQLType, InputValue, RootNode, Variables, execute, graphql_value, diff --git a/server/src/infra/graphql/query.rs b/crates/graphql-server/src/query.rs similarity index 99% rename from server/src/infra/graphql/query.rs rename to crates/graphql-server/src/query.rs index e96e22e..55002ed 100644 --- a/server/src/infra/graphql/query.rs +++ b/crates/graphql-server/src/query.rs @@ -1,4 +1,4 @@ -use crate::infra::graphql::api::{Context, field_error_callback}; +use crate::api::{Context, field_error_callback}; use anyhow::Context as AnyhowContext; use chrono::TimeZone; use juniper::{FieldResult, GraphQLInputObject, graphql_object}; @@ -110,6 +110,12 @@ pub struct Query { _phantom: std::marker::PhantomData>, } +impl Default for Query { + fn default() -> Self { + Self::new() + } +} + impl Query { pub fn new() -> Self { Self { diff --git a/server/Cargo.toml b/server/Cargo.toml index c72554d..d42440f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -98,6 +98,9 @@ path = "../crates/domain-handlers" [dependencies.lldap_frontend_options] path = "../crates/frontend-options" +[dependencies.lldap_graphql_server] +path = "../crates/graphql-server" + [dependencies.lldap_ldap] path = "../crates/ldap" diff --git a/server/src/infra/graphql/api.rs b/server/src/infra/graphql_server.rs similarity index 65% rename from server/src/infra/graphql/api.rs rename to server/src/infra/graphql_server.rs index a8b8571..c2701eb 100644 --- a/server/src/infra/graphql/api.rs +++ b/server/src/infra/graphql_server.rs @@ -1,109 +1,18 @@ -use crate::infra::{ - auth_service::check_if_token_is_valid, - cli::ExportGraphQLSchemaOpts, - graphql::{mutation::Mutation, query::Query}, - tcp_server::AppState, -}; +use crate::infra::{auth_service::check_if_token_is_valid, tcp_server::AppState}; use actix_web::FromRequest; use actix_web::HttpMessage; use actix_web::{Error, HttpRequest, HttpResponse, error::JsonPayloadError, web}; use actix_web_httpauth::extractors::bearer::BearerAuth; use juniper::{ - EmptySubscription, FieldError, RootNode, ScalarValue, + ScalarValue, http::{ GraphQLBatchRequest, GraphQLRequest, graphiql::graphiql_source, playground::playground_source, }, }; -use lldap_access_control::{ - AccessControlledBackendHandler, AdminBackendHandler, ReadonlyBackendHandler, - UserReadableBackendHandler, UserWriteableBackendHandler, -}; -use lldap_auth::{access_control::ValidationResults, types::UserId}; use lldap_domain_handlers::handler::BackendHandler; -use tracing::debug; - -pub struct Context { - pub handler: AccessControlledBackendHandler, - pub validation_result: ValidationResults, -} - -pub fn field_error_callback<'a>( - span: &'a tracing::Span, - error_message: &'a str, -) -> impl 'a + FnOnce() -> FieldError { - move || { - span.in_scope(|| debug!("Unauthorized")); - FieldError::from(error_message) - } -} - -impl Context { - #[cfg(test)] - pub fn new_for_tests(handler: Handler, validation_result: ValidationResults) -> Self { - Self { - handler: AccessControlledBackendHandler::new(handler), - validation_result, - } - } - - pub fn get_admin_handler(&self) -> Option<&(impl AdminBackendHandler + use)> { - self.handler.get_admin_handler(&self.validation_result) - } - - pub fn get_readonly_handler(&self) -> Option<&(impl ReadonlyBackendHandler + use)> { - self.handler.get_readonly_handler(&self.validation_result) - } - - pub fn get_writeable_handler( - &self, - user_id: &UserId, - ) -> Option<&(impl UserWriteableBackendHandler + use)> { - self.handler - .get_writeable_handler(&self.validation_result, user_id) - } - - pub fn get_readable_handler( - &self, - user_id: &UserId, - ) -> Option<&(impl UserReadableBackendHandler + use)> { - self.handler - .get_readable_handler(&self.validation_result, user_id) - } -} - -impl juniper::Context for Context {} - -type Schema = - RootNode<'static, Query, Mutation, EmptySubscription>>; - -fn schema() -> Schema { - Schema::new( - Query::::new(), - Mutation::::new(), - EmptySubscription::>::new(), - ) -} - -pub fn export_schema(opts: ExportGraphQLSchemaOpts) -> anyhow::Result<()> { - use anyhow::Context; - use lldap_sql_backend_handler::SqlBackendHandler; - let output = schema::().as_schema_language(); - match opts.output_file { - None => println!("{}", output), - Some(path) => { - use std::fs::File; - use std::io::prelude::*; - use std::path::Path; - let path = Path::new(&path); - let mut file = - File::create(path).context(format!("unable to open '{}'", path.display()))?; - file.write_all(output.as_bytes()) - .context(format!("unable to write in '{}'", path.display()))?; - } - } - Ok(()) -} +use lldap_graphql_server::api::Context; +use lldap_graphql_server::api::schema; async fn graphiql_route() -> Result { let html = graphiql_source("/api/graphql", None); diff --git a/server/src/infra/mod.rs b/server/src/infra/mod.rs index 48abaf8..0e0dd70 100644 --- a/server/src/infra/mod.rs +++ b/server/src/infra/mod.rs @@ -3,7 +3,7 @@ pub mod cli; pub mod configuration; pub mod database_string; pub mod db_cleaner; -pub mod graphql; +pub mod graphql_server; pub mod healthcheck; pub mod jwt_sql_tables; pub mod ldap_server; diff --git a/server/src/infra/tcp_server.rs b/server/src/infra/tcp_server.rs index 50db573..8259ad7 100644 --- a/server/src/infra/tcp_server.rs +++ b/server/src/infra/tcp_server.rs @@ -145,7 +145,7 @@ fn http_config( .service( web::scope("/api") .wrap(auth_service::CookieToHeaderTranslatorFactory) - .configure(super::graphql::api::configure_endpoint::), + .configure(crate::infra::graphql_server::configure_endpoint::), ) .service( web::resource("/pkg/lldap_app_bg.wasm.gz") diff --git a/server/src/main.rs b/server/src/main.rs index e143106..ec506ba 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -274,7 +274,9 @@ async fn create_schema_command(opts: RunOpts) -> Result<()> { async fn main() -> Result<()> { let cli_opts = infra::cli::init(); match cli_opts.command { - Command::ExportGraphQLSchema(opts) => infra::graphql::api::export_schema(opts), + Command::ExportGraphQLSchema(opts) => { + lldap_graphql_server::api::export_schema(opts.output_file) + } Command::Run(opts) => run_server_command(opts).await, Command::HealthCheck(opts) => run_healthcheck(opts).await, Command::SendTestEmail(opts) => send_test_email_command(opts).await,