Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(openldap): tls certs and ldif file #217

Merged
merged 3 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ kube = { version = "0.90.0", default-features = false, features = [
k8s-openapi = { version = "0.21.1", features = ["v1_29"] }
clickhouse = "0.11.6"
vaultrs = "0.7.2"
openssl-sys = { version = "0.9.103", features = ["vendored"] }
native-tls = { version = "0.2.12", features = ["vendored"] }

[[example]]
name = "postgres"
Expand Down
17 changes: 17 additions & 0 deletions src/openldap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# OpenLDAP Testcontainer

This modules provides the `bitnami/openldap` image as a testcontainer.


## Testing

For testing tls connections, you need to provide a certificate and key. All needed tls artifacts are generated by the `generate-certs.sh` script.

The used openssl version for the latests created certificate is:

```bash
> openssl version
OpenSSL 3.0.13 30 Jan 2024 (Library: OpenSSL 3.0.13 30 Jan 2024)
```

Please update the version here in the README if you generate new certificates for the tests.
53 changes: 53 additions & 0 deletions src/openldap/create_certs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/bash

## Helper Script for creating certificates for the LDAP server tls tests

# Set variables
ROOT_SUBJECT="/C=AT/O=Test/OU=Testcontainer/CN=rootCA"
SERVER_SUBJECT="/C=AT/O=Test/OU=Testcontainer/CN=ldap.example.org"
DAYS_UNTIL_EXPIRE=$((365 * 20))

# Create directory for certificates
mkdir -p certs
rm -fr certs/*
cd certs

# Create a config file for the server certificate
cat >server.cnf <<EOF
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = AT
O = Test
OU = Testcontainer
CN = ldap.example.org
[v3_req]
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = openldap
EOF

# Create a self-signed root certificate
openssl req -x509 -nodes -days $DAYS_UNTIL_EXPIRE -newkey rsa:2048 -keyout rootCA.key -out rootCA.crt -subj "$ROOT_SUBJECT"

# Create a key for the server certificate
openssl genrsa -out ldap.example.org.key 2048

# Generate Server CSR using the config file
openssl req -new -key ldap.example.org.key -out ldap.example.org.csr -subj "$SERVER_SUBJECT"

# Sign the server certificate with the Root CA
openssl x509 -req -in ldap.example.org.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out ldap.example.org.crt -days $DAYS_UNTIL_EXPIRE -extfile server.cnf -extensions v3_req

# Clean up
rm -r *.csr *.cnf *.srl rootCA.key

cd -
223 changes: 221 additions & 2 deletions src/openldap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ use std::{borrow::Cow, collections::HashMap};
use parse_display::{Display, FromStr};
use testcontainers::{
core::{ContainerPort, WaitFor},
Image,
CopyDataSource, CopyToContainer, Image,
};

const NAME: &str = "bitnami/openldap";
const TAG: &str = "2.6.8";
const OPENLDAP_PORT: ContainerPort = ContainerPort::Tcp(1389);
const OPENLDAPS_PORT: ContainerPort = ContainerPort::Tcp(1636);

/// Module to work with [`OpenLDAP`] inside of tests.
///
Expand Down Expand Up @@ -46,6 +47,7 @@ const OPENLDAP_PORT: ContainerPort = ContainerPort::Tcp(1389);
pub struct OpenLDAP {
env_vars: HashMap<String, String>,
users: Vec<User>,
copy_to_sources: Vec<CopyToContainer>,
}
#[derive(Debug, Clone)]
struct User {
Expand Down Expand Up @@ -231,6 +233,53 @@ impl OpenLDAP {
.insert("LDAP_PASSWORD_HASH".to_owned(), password_hash.to_string());
self
}

/// Sets a custom ldif file (content) which should be used.
/// Default: `[]`
pub fn with_ldif_file(mut self, source: impl Into<CopyDataSource>) -> Self {
let n = self.copy_to_sources.len() + 1;

let container_ldif_path = format!("/ldifs/custom{n}.ldif");

self.copy_to_sources
.push(CopyToContainer::new(source, container_ldif_path));
self
}

/// Set all necessary certificate artifacts to build up a secure communication.
/// Default: `[]`
pub fn with_tls(
mut self,
cert: impl Into<CopyDataSource>,
key: impl Into<CopyDataSource>,
) -> Self {
self.copy_to_sources
.push(CopyToContainer::new(cert, "/certs/cert.crt"));
self.copy_to_sources
.push(CopyToContainer::new(key, "/certs/key.key"));

self.env_vars.insert(
"LDAP_TLS_CERT_FILE".to_owned(),
"/certs/cert.crt".to_owned(),
);
self.env_vars
.insert("LDAP_TLS_KEY_FILE".to_owned(), "/certs/key.key".to_owned());

self.env_vars
.insert("LDAP_ENABLE_TLS".to_owned(), "yes".to_owned());

self
}

/// Sets the root certificate used for signing the tls certificate.
/// Default: `[]`
pub fn with_cert_ca(mut self, ca: impl Into<CopyDataSource>) -> Self {
self.copy_to_sources
.push(CopyToContainer::new(ca, "/certs/ca.crt"));
self.env_vars
.insert("LDAP_TLS_CA_FILE".to_owned(), "/certs/ca.crt".to_owned());
self
}
}

/// hash to be used in generation of user passwords.
Expand Down Expand Up @@ -350,6 +399,7 @@ impl Default for OpenLDAP {
Self {
users: vec![],
env_vars: HashMap::new(),
copy_to_sources: vec![],
}
}
}
Expand Down Expand Up @@ -394,7 +444,13 @@ impl Image for OpenLDAP {
}

fn expose_ports(&self) -> &[ContainerPort] {
&[OPENLDAP_PORT]
&[OPENLDAP_PORT, OPENLDAPS_PORT]
}

fn copy_to_sources(&self) -> impl IntoIterator<Item = &CopyToContainer> {
self.copy_to_sources
.iter()
.collect::<Vec<&CopyToContainer>>()
}
}

Expand Down Expand Up @@ -526,4 +582,167 @@ mod tests {
ldap.unbind().await?;
Ok(())
}

#[tokio::test]
async fn ldap_with_ldif() -> Result<(), Box<dyn std::error::Error + 'static>> {
let _ = pretty_env_logger::try_init();
let ldif = r#"
version: 1

# Entry 1: dc=example,dc=org
dn: dc=example,dc=org
objectClass: top
objectClass: domain
dc: example

# Entry 2: ou=users,dc=frauscher,dc=test
dn: ou=users,dc=example,dc=org
objectclass: organizationalUnit
objectclass: top
ou: users

# Entry 3: cn=maximiliane,dc=example,dc=org
dn: cn=maximiliane,ou=users,dc=example,dc=org
cn: maximiliane
gidnumber: 501
givenname: Florian
homedirectory: /home/users/maximiliane
objectclass: inetOrgPerson
objectclass: posixAccount
objectclass: top
sn: Maximiliane
uid: maximiliane
uidnumber: 1001
userpassword: {SSHA}1vtVpqX6Y77jAVs/1uTd/rHS8YRYEh/9
"#;

let openldap_image = OpenLDAP::default().with_ldif_file(ldif.to_string().into_bytes());

let node = openldap_image.start().await?;

let connection_string = format!(
"ldap://{}:{}",
node.get_host().await?,
node.get_host_port_ipv4(OPENLDAP_PORT).await?,
);
let (conn, mut ldap) = LdapConnAsync::new(&connection_string).await?;
ldap3::drive!(conn);
ldap.simple_bind("cn=maximiliane,ou=users,dc=example,dc=org", "pwd1")
.await?
.success()?;
let users = read_users(&mut ldap, "(cn=*)", &["cn"]).await?;
assert_eq!(users.len(), 1); // cn=maximiliane
ldap.unbind().await?;
Ok(())
}

#[tokio::test]
async fn ldap_secure() -> Result<(), Box<dyn std::error::Error + 'static>> {
let _ = pretty_env_logger::try_init();

let root_ca = r#"-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUacPFMoNlemFtn974hn0x4c+ssGowDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVQxDTALBgNVBAoMBFRlc3QxFjAUBgNVBAsMDVRlc3Rj
b250YWluZXIxDzANBgNVBAMMBnJvb3RDQTAeFw0yNDA5MjUxODI0MDFaFw00NDA5
MjAxODI0MDFaMEUxCzAJBgNVBAYTAkFUMQ0wCwYDVQQKDARUZXN0MRYwFAYDVQQL
DA1UZXN0Y29udGFpbmVyMQ8wDQYDVQQDDAZyb290Q0EwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQC4+4uW9PE5HQGWtn4/P1wPm11z/UevNGveDEUvwcDz
oEZlfRLTihoVfc7EZGUF8/6TS2rkp1qz2yrDDP4o7a7l0Om22vCshsWlhhqQ1mON
tiXnOWXqKjon6NZatvYnQVvUwm8BuSi4HR4xNM8N9BF81rhFj3tbam3lylJign/3
sFsxUg2C3mAaDoolTFslBPPnKHW2EmSw04kgC6XhWGXqpAbWbL5h2rlOE/WoLTkl
f+EpILsCyEApNbVLXh+B9+IrJvNqX6yuFL7zzJhefOa1L1sAG0bNc9nAwuwyDcDu
OzxULkv0JGK2Dob6NL74NSB96kPLbZi/FRIHwhBgmUlPAgMBAAGjUzBRMB0GA1Ud
DgQWBBQ2Bcju3oNHso17GQFoKd5/46OsnjAfBgNVHSMEGDAWgBQ2Bcju3oNHso17
GQFoKd5/46OsnjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAP
za/O0NzyLZjjOoBsDKyJzvgHv/Ls8yBRSmwAbJoXqT1F4F39px5ZVOdmruXeJsG1
PIaLb2/4oPoSgqLfiPUMAJelTjD/CVpfFthiorCgYPSIIhkjgc6jdhCez67gotFE
nBITxIRg47yAmCA+0+/YtTnul1deB9r2cuXeHVTPfUVphDsKSGVLJS0TK1iIL/PZ
k/bSZzdk/i80spmFS3W/fLHPWFUbio1r0CBpbibQNl19x8uHz+J7L1kmW7gofyd9
w4a3nYIMqapx5KGgGqI3lyc/ePet2JhUabbu7rQx6oEHJaaco3qip2t6pS0WtDcG
B5tEQNlAfsx/Rfi4JUNi
-----END CERTIFICATE-----
"#;
let key = r#"-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDB8KR+QtENXXI0
+BWZ/rD2nDfRi7v6dDLpD1yuVLFbBgVMDisf8CoqZP67x9lW5sRz/CXLjY+If1Vi
xFxcwURzVBWmrMBLbEgDnsyX0GhgCdZTJM3eBTXwCN77bH/xSkZ6Dg1AV/i/ltAI
TvMlE5Azrd/0IePWEQYtQYUQMlJHcxrDex3sKZ/oM6ohMl1sbkNDV+Ncatl7bIel
RDyHE1SgExFFL2qWg2CeHJHPliq/0osp1cU99SCtaalxwisnih0GMswIf++1s+r6
/CGRI+5xXOd1wtPwcjFxdVnSuhbFxniz9fPaPXw0NoVBksRcJNri2RHDZpW7nK+D
OMcvWtxxAgMBAAECggEANQPOQ4GKWgfwX1Btv0HjKBa+H3b+NNGs1Q7Q/ArEzKgR
rJ+25C0nqZ0gET7pR5sfmsETp9gTo3GDatNYmDZwusIChSR2EGgSK4MuVFWxIoet
4d6OtCFihDI4miwnsVLnfxf2QV+K7PyR86N5TepSIf5m2PqmqG7Q7HAbqrjGyybO
te4IIYUox2OirICfNMdy6ROj3c+oEiXu5eEpxbl01WzSU7rO4fMdfc0O/cocg2rr
njBXD0utCiUw0M6EaisYzAg5ughP02enmxGzGwBT+lr2AQHG3Mk++PICRxsiBIv0
T0o6Q3mIR4YMNDt7dZVz8mjL7lx1iuczM5CUbRnhzQKBgQDxTubdOmTidd1WxRJ5
4t19Ga604YVG8c97hZ7fpNL0fTY/JcxkEN02uhHgA9fmBhE2vRus82x+iLKjcNBY
ZKzM2PaRsjK2g9UNi829Zii/VjZJX1QwipO0EQt0Rruf+QtWXL47UHKTxpRfLrFN
zaXrVM15uq/pdhmovj+LQ4NplwKBgQDNv3JQVXBImy9/5h2xPwVlkjXzQdD0PTEs
emnLRfnctyqAzmtgTsbS1tDhoCwm5TuI6MMbrtnojSQKRr68BDfhvnm4TBHWYrS1
0BqY+P1XkJH4jmIQsRpS/ZnDlB50+aiAnuMahui0mUT71/upGS277Zqr+u7Gkt2b
fFeWzu3bNwKBgFiU+FbZ6tLfJaOGsKOhzmDwHpwz9XL3rYzQnmPG49HwbQt9WqyZ
LDu8zncHsie0rnkDrrcsnPVORRWOgk0QmAaS1uDhI5CwkHNqkNooOGkUwtToc8Vl
+Zaucx/6H0I4cBsB7KtlesoYqbrPLzM6fOAIv20iRRVUz1KMlFMRM5p9AoGBAJJr
Tv/Kfbi974S2j6Tms4GAFrLBwOE/dvIvP4CwkMs48p9txs5n4WiEBWy73w/jDIY3
FzppKZwsbVx+0hfdbKNTOS4lvH/0CKRmr7bzYt9g+/CF61Xzo0cyQK4Fh9M5JGg8
KmRjY9G6TXRoVSkWyQw3YF5JmoloVRrk1zR0mKLrAoGBAKPJmagtBVmTvKdYFhs0
X6au0mG5xN4HnkdvX1sGgeg+DgJ/dCa4LA3lZagUhDjupR0QgaaYVZhyFe6lDVck
lOB14QZNoFr+xwiSDg4oM4/TQXualO/4nZxzUynbZNdNuvhQ8mRdeu5jqs363deP
bCWwJaA6QQpNSitVrzg5XbRQ
-----END PRIVATE KEY-----
"#;
let cert = r#"-----BEGIN CERTIFICATE-----
MIIDqzCCApOgAwIBAgIUJArHsm2jdPlyOXWC7TWdFxYD19kwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVQxDTALBgNVBAoMBFRlc3QxFjAUBgNVBAsMDVRlc3Rj
b250YWluZXIxDzANBgNVBAMMBnJvb3RDQTAeFw0yNDA5MjUxODI0MDJaFw00NDA5
MjAxODI0MDJaME8xCzAJBgNVBAYTAkFUMQ0wCwYDVQQKDARUZXN0MRYwFAYDVQQL
DA1UZXN0Y29udGFpbmVyMRkwFwYDVQQDDBBsZGFwLmV4YW1wbGUub3JnMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwfCkfkLRDV1yNPgVmf6w9pw30Yu7
+nQy6Q9crlSxWwYFTA4rH/AqKmT+u8fZVubEc/wly42PiH9VYsRcXMFEc1QVpqzA
S2xIA57Ml9BoYAnWUyTN3gU18Aje+2x/8UpGeg4NQFf4v5bQCE7zJROQM63f9CHj
1hEGLUGFEDJSR3Maw3sd7Cmf6DOqITJdbG5DQ1fjXGrZe2yHpUQ8hxNUoBMRRS9q
loNgnhyRz5Yqv9KLKdXFPfUgrWmpccIrJ4odBjLMCH/vtbPq+vwhkSPucVzndcLT
8HIxcXVZ0roWxcZ4s/Xz2j18NDaFQZLEXCTa4tkRw2aVu5yvgzjHL1rccQIDAQAB
o4GIMIGFMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAeBgNV
HREEFzAVgglsb2NhbGhvc3SCCG9wZW5sZGFwMB0GA1UdDgQWBBTOko7NFMj7/fd/
KGvfbYezhjmH8zAfBgNVHSMEGDAWgBQ2Bcju3oNHso17GQFoKd5/46OsnjANBgkq
hkiG9w0BAQsFAAOCAQEAlud2ST43+Q2Sa+VHS8Tio1M76+AdNj1dmQHYsFN7Vm91
cAEOFOO8y/oSqTZrIuxenFCIsMeAAVOEZ7BjcpzX50ncHAYDu2szpmTscvujNoSs
1qvbfRC1aL8bky4XECct7Md1h7TTN/pY0E+6b1wI0gyHkCeuiaOfeq7I+lUogIzL
SpuBTQvi59BdeLyTXImg8WCSKoLrZljdaEjCZdM51FWFvY2WdW1NE/ahniJpkGv5
hcDj6qNPn8FHCLxzOs1HUucyncxbS9z6I91WaFWXWu0DH90lMA8gedyXZr6YOnkg
H32P9zbIKaSiPxFg5JVRW5hpQWUI1dYr3CpKP4i98w==
-----END CERTIFICATE-----
"#;

let openldap_image = OpenLDAP::default()
.with_allow_anon_binding(false)
.with_user("maximiliane", "pwd1")
.with_tls(cert.to_string().into_bytes(), key.to_string().into_bytes())
.with_cert_ca(root_ca.to_string().into_bytes());
let node = openldap_image.start().await?;

let connection_string = format!(
"ldaps://{}:{}",
node.get_host().await?,
node.get_host_port_ipv4(OPENLDAPS_PORT).await?,
);

let mut builder = native_tls::TlsConnector::builder();
let root_ca = native_tls::Certificate::from_pem(root_ca.as_bytes())?;
let connector = builder.add_root_certificate(root_ca).build()?;

let settings = ldap3::LdapConnSettings::new().set_connector(connector);
let (conn, mut ldap) =
ldap3::LdapConnAsync::with_settings(settings, &connection_string).await?;

ldap3::drive!(conn);
ldap.simple_bind("cn=maximiliane,ou=users,dc=example,dc=org", "pwd1")
.await?
.success()?;
let users = read_users(&mut ldap, "(cn=*)", &["cn"]).await?;
assert_eq!(users.len(), 2); // cn=maximiliane and cn=readers
ldap.unbind().await?;
Ok(())
}
}
Loading