mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
server: implement healthcheck
This commit is contained in:
parent
01d4b6e1fc
commit
3aaf53442b
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -2138,6 +2138,7 @@ dependencies = [
|
||||
"opaque-ke",
|
||||
"orion",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"rustls 0.20.6",
|
||||
"rustls-pemfile",
|
||||
"sea-query",
|
||||
@ -2162,6 +2163,7 @@ dependencies = [
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"webpki-roots 0.21.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3018,9 +3020,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.11"
|
||||
version = "0.11.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92"
|
||||
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
@ -3034,9 +3036,9 @@ dependencies = [
|
||||
"hyper-rustls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls 0.20.6",
|
||||
|
@ -47,6 +47,7 @@ tracing-attributes = "^0.1.21"
|
||||
tracing-log = "*"
|
||||
rustls-pemfile = "1.0.0"
|
||||
serde_bytes = "0.11.7"
|
||||
webpki-roots = "*"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
@ -124,5 +125,10 @@ features = ["jpeg"]
|
||||
default-features = false
|
||||
version = "0.24"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.11"
|
||||
default-features = false
|
||||
features = ["rustls-tls-webpki-roots"]
|
||||
|
||||
[dev-dependencies]
|
||||
mockall = "0.9.1"
|
||||
|
@ -20,6 +20,9 @@ pub enum Command {
|
||||
/// Run the LDAP and GraphQL server.
|
||||
#[clap(name = "run")]
|
||||
Run(RunOpts),
|
||||
/// Test whether the LDAP and GraphQL server are responsive.
|
||||
#[clap(name = "healthcheck")]
|
||||
HealthCheck(RunOpts),
|
||||
/// Send a test email.
|
||||
#[clap(name = "send_test_email")]
|
||||
SendTestEmail(TestEmailOpts),
|
||||
|
124
server/src/infra/healthcheck.rs
Normal file
124
server/src/infra/healthcheck.rs
Normal file
@ -0,0 +1,124 @@
|
||||
use crate::infra::configuration::LdapsOptions;
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use futures_util::SinkExt;
|
||||
use ldap3_proto::{
|
||||
proto::{
|
||||
LdapDerefAliases, LdapFilter, LdapMsg, LdapOp, LdapSearchRequest, LdapSearchResultEntry,
|
||||
LdapSearchScope,
|
||||
},
|
||||
LdapCodec,
|
||||
};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_rustls::TlsConnector as RustlsTlsConnector;
|
||||
use tokio_util::codec::{FramedRead, FramedWrite};
|
||||
use tracing::{debug, info, instrument};
|
||||
|
||||
async fn check_ldap_endpoint<Stream>(stream: Stream) -> Result<()>
|
||||
where
|
||||
Stream: tokio::io::AsyncRead + tokio::io::AsyncWrite,
|
||||
{
|
||||
use tokio_stream::StreamExt;
|
||||
let (r, w) = tokio::io::split(stream);
|
||||
let mut requests = FramedRead::new(r, LdapCodec);
|
||||
let mut resp = FramedWrite::new(w, LdapCodec);
|
||||
|
||||
resp.send(LdapMsg {
|
||||
msgid: 0,
|
||||
op: LdapOp::SearchRequest(LdapSearchRequest {
|
||||
base: "".to_string(),
|
||||
scope: LdapSearchScope::Base,
|
||||
aliases: LdapDerefAliases::Never,
|
||||
sizelimit: 0,
|
||||
timelimit: 0,
|
||||
typesonly: false,
|
||||
filter: LdapFilter::Present("objectClass".to_string()),
|
||||
attrs: vec!["supportedExtension".to_string()],
|
||||
}),
|
||||
ctrl: vec![],
|
||||
})
|
||||
.await?;
|
||||
resp.flush().await?;
|
||||
|
||||
let no_answer = || anyhow!("No answer from LDAP server");
|
||||
let invalid_answer = "Invalid answer from LDAP server";
|
||||
|
||||
let msg = requests
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(no_answer)?
|
||||
.context(invalid_answer)?;
|
||||
debug!("Received message: {:?}", &msg);
|
||||
match msg.op {
|
||||
LdapOp::SearchResultEntry(LdapSearchResultEntry { dn, attributes }) => ensure!(
|
||||
dn.is_empty()
|
||||
&& attributes
|
||||
.into_iter()
|
||||
.any(|a| a.atype == "objectClass" && a.vals == vec![b"top".to_vec()]),
|
||||
invalid_answer
|
||||
),
|
||||
_ => bail!(invalid_answer),
|
||||
}
|
||||
let msg = requests.next().await.ok_or_else(no_answer)??;
|
||||
debug!("Received message: {:?}", &msg);
|
||||
ensure!(
|
||||
matches!(msg.op, LdapOp::SearchResultDone(_)),
|
||||
invalid_answer
|
||||
);
|
||||
info!("Success");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = "info", err)]
|
||||
pub async fn check_ldap(port: u16) -> Result<()> {
|
||||
check_ldap_endpoint(TcpStream::connect(format!("localhost:{}", port)).await?).await
|
||||
}
|
||||
|
||||
fn get_root_certificates() -> rustls::RootCertStore {
|
||||
let mut root_store = rustls::RootCertStore::empty();
|
||||
root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
|
||||
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
|
||||
ta.subject,
|
||||
ta.spki,
|
||||
ta.name_constraints,
|
||||
)
|
||||
}));
|
||||
root_store
|
||||
}
|
||||
|
||||
fn get_tls_connector() -> Result<RustlsTlsConnector> {
|
||||
use rustls::ClientConfig;
|
||||
let client_config = std::sync::Arc::new(
|
||||
ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(get_root_certificates())
|
||||
.with_no_client_auth(),
|
||||
);
|
||||
Ok(client_config.into())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = "info", err)]
|
||||
pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> {
|
||||
if !ldaps_options.enabled {
|
||||
return Ok(());
|
||||
};
|
||||
let tls_connector = get_tls_connector()?;
|
||||
let url = format!("localhost:{}", ldaps_options.port);
|
||||
check_ldap_endpoint(
|
||||
tls_connector
|
||||
.connect(
|
||||
rustls::ServerName::try_from(url.as_str())?,
|
||||
TcpStream::connect(&url).await?,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = "info", err)]
|
||||
pub async fn check_api(port: u16) -> Result<()> {
|
||||
reqwest::get(format!("http://localhost:{}/health", port))
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
info!("Success");
|
||||
Ok(())
|
||||
}
|
@ -633,6 +633,13 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
}
|
||||
|
||||
pub async fn do_search_or_dse(&mut self, request: &LdapSearchRequest) -> Vec<LdapOp> {
|
||||
if request.base.is_empty()
|
||||
&& request.scope == LdapSearchScope::Base
|
||||
&& request.filter == LdapFilter::Present("objectClass".to_string())
|
||||
{
|
||||
debug!("rootDSE request");
|
||||
return vec![root_dse_response(&self.base_dn_str), make_search_success()];
|
||||
}
|
||||
let user_info = match &self.user_info {
|
||||
None => {
|
||||
return vec![make_search_error(
|
||||
@ -642,13 +649,6 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
}
|
||||
Some(u) => u,
|
||||
};
|
||||
if request.base.is_empty()
|
||||
&& request.scope == LdapSearchScope::Base
|
||||
&& request.filter == LdapFilter::Present("objectClass".to_string())
|
||||
{
|
||||
debug!("rootDSE request");
|
||||
return vec![root_dse_response(&self.base_dn_str), make_search_success()];
|
||||
}
|
||||
let user_filter = if user_info.is_admin_or_readonly() {
|
||||
None
|
||||
} else {
|
||||
|
@ -37,9 +37,9 @@ impl RootSpanBuilder for CustomRootSpanBuilder {
|
||||
pub fn init(config: &Configuration) -> anyhow::Result<()> {
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
EnvFilter::new(if config.verbose {
|
||||
"sqlx=warn,debug"
|
||||
"sqlx=warn,reqwest=warn,debug"
|
||||
} else {
|
||||
"sqlx=warn,info"
|
||||
"sqlx=warn,reqwest=warn,info"
|
||||
})
|
||||
});
|
||||
tracing_subscriber::registry()
|
||||
|
@ -3,6 +3,7 @@ pub mod cli;
|
||||
pub mod configuration;
|
||||
pub mod db_cleaner;
|
||||
pub mod graphql;
|
||||
pub mod healthcheck;
|
||||
pub mod jwt_sql_tables;
|
||||
pub mod ldap_handler;
|
||||
pub mod ldap_server;
|
||||
|
@ -82,6 +82,7 @@ fn http_config<Backend>(
|
||||
server_url,
|
||||
mail_options,
|
||||
}))
|
||||
.route("/health", web::get().to(|| HttpResponse::Ok().finish()))
|
||||
.service(web::scope("/auth").configure(auth_service::configure_server::<Backend>))
|
||||
// API endpoint.
|
||||
.service(
|
||||
|
@ -2,6 +2,8 @@
|
||||
#![forbid(non_ascii_idents)]
|
||||
#![allow(clippy::nonstandard_macro_braces)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
domain::{
|
||||
handler::{BackendHandler, CreateUserRequest, GroupRequestFilter},
|
||||
@ -9,7 +11,7 @@ use crate::{
|
||||
sql_opaque_handler::register_password,
|
||||
sql_tables::PoolOptions,
|
||||
},
|
||||
infra::{cli::*, configuration::Configuration, db_cleaner::Scheduler, mail},
|
||||
infra::{cli::*, configuration::Configuration, db_cleaner::Scheduler, healthcheck, mail},
|
||||
};
|
||||
use actix::Actor;
|
||||
use actix_server::ServerBuilder;
|
||||
@ -132,11 +134,42 @@ fn send_test_email_command(opts: TestEmailOpts) -> Result<()> {
|
||||
mail::send_test_email(to, &config.smtp_options)
|
||||
}
|
||||
|
||||
fn run_healthcheck(opts: RunOpts) -> Result<()> {
|
||||
debug!("CLI: {:#?}", &opts);
|
||||
let config = infra::configuration::init(opts)?;
|
||||
infra::logging::init(&config)?;
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
use tokio::time::timeout;
|
||||
let delay = Duration::from_millis(3000);
|
||||
let (ldap, ldaps, api) = runtime.block_on(async {
|
||||
tokio::join!(
|
||||
timeout(delay, healthcheck::check_ldap(config.ldap_port)),
|
||||
timeout(delay, healthcheck::check_ldaps(&config.ldaps_options)),
|
||||
timeout(delay, healthcheck::check_api(config.http_port)),
|
||||
)
|
||||
});
|
||||
|
||||
let mut failure = false;
|
||||
[ldap, ldaps, api]
|
||||
.into_iter()
|
||||
.filter_map(Result::err)
|
||||
.for_each(|e| {
|
||||
failure = true;
|
||||
error!("{:#}", e)
|
||||
});
|
||||
std::process::exit(if failure { 1 } else { 0 })
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli_opts = infra::cli::init();
|
||||
match cli_opts.command {
|
||||
Command::ExportGraphQLSchema(opts) => infra::graphql::api::export_schema(opts),
|
||||
Command::Run(opts) => run_server_command(opts),
|
||||
Command::HealthCheck(opts) => run_healthcheck(opts),
|
||||
Command::SendTestEmail(opts) => send_test_email_command(opts),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user