From 807d48435d0cf1e2e840a0814edd7efe9a273ced Mon Sep 17 00:00:00 2001 From: Bear Giles Date: Mon, 16 Sep 2024 00:58:19 -0600 Subject: [PATCH] Initial commit Minimal viable product - can successfully connect to LDAP server as both anonymous user and authenticated admin user. This also includes java classes corresponding to several standard LDAP schemas. These classes do not contain the Active Directory or Kerberos schemas. --- modules/openldap/README.md | 183 ++++ modules/openldap/build.gradle | 7 + .../containers/ObjectClassInformation.java | 184 +++++ .../containers/OpenLdapConfiguration.java | 36 + .../containers/OpenLdapContainer.java | 782 ++++++++++++++++++ .../containers/schema/ApplicationEntity.java | 40 + .../containers/schema/ApplicationProcess.java | 28 + .../schema/CertificationAuthority.java | 30 + .../containers/schema/Country.java | 26 + .../testcontainers/containers/schema/DSA.java | 18 + .../containers/schema/Device.java | 40 + .../containers/schema/GroupOfNames.java | 41 + .../containers/schema/GroupOfUniqueNames.java | 41 + .../containers/schema/InetOrgPerson.java | 72 ++ .../containers/schema/Locality.java | 34 + .../containers/schema/Organization.java | 63 ++ .../schema/OrganizationalPerson.java | 53 ++ .../containers/schema/OrganizationalRole.java | 61 ++ .../containers/schema/OrganizationalUnit.java | 63 ++ .../containers/schema/Person.java | 35 + .../containers/schema/PkiCA.java | 28 + .../containers/schema/PkiUser.java | 18 + .../containers/schema/ResidentialPerson.java | 52 ++ .../schema/StrongAuthenticationUser.java | 18 + .../testcontainers/containers/schema/Top.java | 13 + .../schema/UserSecurityInformation.java | 18 + .../containers/schema/annotations/May.java | 15 + .../containers/schema/annotations/Must.java | 17 + .../containers/schema/annotations/Oid.java | 16 + .../containers/schema/annotations/Rfc.java | 15 + .../schema/annotations/RfcValue.java | 7 + .../ObjectClassInformationTest.java | 47 ++ .../containers/OpenLdapContainerTest.java | 81 ++ .../src/test/resources/logback-test.xml | 16 + .../test/resources/objectClassDefinitions.txt | 73 ++ 35 files changed, 2271 insertions(+) create mode 100644 modules/openldap/README.md create mode 100644 modules/openldap/build.gradle create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/ObjectClassInformation.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/OpenLdapConfiguration.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/OpenLdapContainer.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/ApplicationEntity.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/ApplicationProcess.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/CertificationAuthority.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/Country.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/DSA.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/Device.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/GroupOfNames.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/GroupOfUniqueNames.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/InetOrgPerson.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/Locality.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/Organization.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalPerson.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalRole.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalUnit.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/Person.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/PkiCA.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/PkiUser.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/ResidentialPerson.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/StrongAuthenticationUser.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/Top.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/UserSecurityInformation.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/May.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Must.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Oid.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Rfc.java create mode 100644 modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/RfcValue.java create mode 100644 modules/openldap/src/test/java/org/testcontainers/containers/ObjectClassInformationTest.java create mode 100644 modules/openldap/src/test/java/org/testcontainers/containers/OpenLdapContainerTest.java create mode 100644 modules/openldap/src/test/resources/logback-test.xml create mode 100644 modules/openldap/src/test/resources/objectClassDefinitions.txt diff --git a/modules/openldap/README.md b/modules/openldap/README.md new file mode 100644 index 00000000000..220e3b9438c --- /dev/null +++ b/modules/openldap/README.md @@ -0,0 +1,183 @@ +# IdP - OpenLDAP Module + +## Identity Providers (IdP) + +Identity Providers provide externalized user authentication (AuthN) and +authorization (AuthZ), and are increasingly available as authentication +mechanisms used by the type of hosted services available as TestContainers. + +### Providers + +- Microsoft Active Directory +- IPA/[FreeIPA](https://freeipa.org/) +- Samba (limited) +- All cloud providers +- All OAuth providers such as Google, Facebook, GitHub, etc. (typically limited to AuthN) +- [Okta](https://okta.com/) +- [Authentik](https://goauthentik.io/) + +Individual services are often available: + +- Kerberos: [MIT KDC](https://web.mit.edu/kerberos/), [Heimdal](https://github.com/heimdal/heimdal), [Apache Kerby](https://directory.apache.org/kerby/) +- LDAP: [OpenLDAP](https://openldap.org/) ([bitnami/openldap](https://hub.docker.com/r/bitnami/openldap)), [389 Directory Service](https://www.port389.org/), [Apache Directory](https://directory.apache.org/) +- CA/PKI [DogTag](https://www.dogtagpki.org/) +- CA/PKI (ACME protocol): Let's Encrypt ([letsencrypt/pebble](https://hub.docker.com/r/letsencrypt/pebble)) +- CA/PKI (EJBCA protocol): ([bitnami/ejbca](https://hub.docker.com/r/bitnami/ejbca)), ([keyfactor/ejbca-ce](https://hub.docker.com/r/keyfactor/ejbca-ce)) + +(Reminder: the 'bitnami' docker images are published by VMware.) + +### Services + +A good _de facto_ definition of an Enterprise IdP provider is +the services provided by Microsoft's Active Directory. It is not +limited to this - we'll often see additional support for OAuth, +J, and more. + +The minimal services are + +- DNS (for service discovery) +- Kerberos (for Authentication) +- LDAP (for storage of AuthN and AuthZ data) +- A Certificate Authority (for managing digital certificates used by servers) +- A Key Manager (for managing encryption keys) + +In modern terms LDAP can be viewed as a column database with a few +unusual constraints. The biggest limiting factor is that you should +only use properly registered OIDs. This is not a huge burden - there's +a well-documented process to get a new OID root and you're free to +extend however you like - think of the requirement to register a domain +name and then having freedom to manage your subdomains however you like - +but this is step that's not required by any other database. + +### Active Directory + +Active Directory makes extensive use of service discovery and requires +the LDAP server include an additional schema. + +In addition Active Directory requires the use of DNS +[SRV records](https://en.wikipedia.org/wiki/SRV_record) in order to find the +required services. This is not difficult to do when you manage your own +DNS servers and it can solve a lot of problems. However it does require +a bit of prep work in both setting up the DNS server and modifying applications +to be able to use SRV records. + +### Practical Note + +It's now common to use a relational database as the backing store for both the +LDAP server and the Kerberos KDC. In these cases the latter two services are +still available for the applications that support (or require) them but many +applications will directly access the database. + +### Java DNS lookup code using JNDI + +This code retrieves and SRV records. + +__Note: I've successfully used similar code in the past but haven't verified this specific implementation yet.__ + +```java +import javax.naming.Context; + +public class ResourceLocator { + // This assumes "DNS_SERVER" system property. + private static final String DEFAULT_DNS_SERVER = "dns://1.1.1.1/"; + private static final String DnsContextFactoryName = "com.sun.jndi.ldap.LdapCtxFactory"; + + enum Protocol { + TCP, + UDP + } + + /** + * Retrieve SRV records + * + * @param service service, e.g., "ldap", "imap", "postgres" + * @param protocol ("tcp", "udp") + * @param domainName domain name to search + * @return list of matching SRV records + */ + List findSrvRecord(String service, Protocol protocol, String domainName) throws NamingException, IOException { + final List srvRecords = new ArrayList<>(); + final String hostname = String.format("_%s._%s.%s.", service, protocol.name().toLowerCase(), domainName); + + // Find JNDI DirContext + final Hashtable env = new Hashtable(); + env.put(Context.PROVIDER_URL, System.getProperty(DNS_SERVER, DEFAULT_DNS_SERVER)); + env.put(Context.INITIAL_CONTEXT_FACTORY, DNS_CONTEXT_FACTORY_NAME); + final DirContext ctx = new InitialDirContext(env); + + try { + final NamingEnumeration searchResultsEnumeration = ctx.search(hostname, new String[]{ "SRV" }); + while (searchResultsEnumertion.hasNext()) { + // check - we may get this as string + srvRecords.add(SrvRecord.parse(searchResultsEnumeration.next())); + } + } catch (NameNotFoundException e) { + // leave srvRecords collection empty + } + + return srvRecords; + } +} + +@Data +public class SrvRecord { + private String service; // the symbolic name of the desired service. + private String proto; // the transport protocol of the desired service; this is usually either TCP or UDP. + private String name; // the domain name for which this record is valid, ending in a dot. + private int ttl; // standard DNS time to live field. + private int priority; // the priority of the target host, lower value means more preferred. + private int weight; // A relative weight for records with the same priority, higher value means higher chance of getting picked. + private int port; // the TCP or UDP port on which the service is to be found. + private String target; // the canonical hostname of the machine providing the service, ending in a dot. + + public SrvRecord parse(SearchResult searchResult) { + final SrvRecord record = new SrvRecord(); + // format is '_service._proto.name. ttl IN SRV priority weight port target.' + // extract attributes + return record; + } +} + +``` + + +## OpenLDAP Module + +This module wraps the bitnami/openldap docker image. + +## Tasks + +- [x] Anonymous access +- [x] Admin access (simple authentication) +- [ ] Add users (simple authentication) +- [ ] Enable or Require TLS + - [ ] Provide server cert + - [ ] Provide third-party certs (/opt/bitnami/openldap/certs/) +- [ ] Add additional schemas + - [ ] Active Directory schema + - [ ] Kerberos +- [ ] Advanced authentication +- [ ] Run initialization scripts (/docker-entrypoint-initdb.d/) + +## Server Functionality Tests + +- [x] Anonymous access +- [x] Admin access (simple authentication) +- [x] List ObjectClasses ('proof of life', etc) +- [ ] Access using TLS +- [ ] Access using stronger authentication + +## Additional Tests + +(Hamcrest matchers...) + +- [ ] User tasks + - [ ] List + - [ ] Add + - [ ] Remove +- [ ] Group tasks + - [ ] List + - [ ] Add + - [ ] Remove + - [ ] Add user to group + - [ ] Remove user from group diff --git a/modules/openldap/build.gradle b/modules/openldap/build.gradle new file mode 100644 index 00000000000..cc63bf31f3c --- /dev/null +++ b/modules/openldap/build.gradle @@ -0,0 +1,7 @@ +description = "Testcontainers :: OpenLdap" + +dependencies { + api project(':testcontainers') + + testImplementation 'org.assertj:assertj-core:3.26.3' +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/ObjectClassInformation.java b/modules/openldap/src/main/java/org/testcontainers/containers/ObjectClassInformation.java new file mode 100644 index 00000000000..fb645cd1e4f --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/ObjectClassInformation.java @@ -0,0 +1,184 @@ +package org.testcontainers.containers; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Information about the LDAP Schema's ObjectClasses + *

+ * Implementation Note + *

+ *

+ * The parser current fails when the definition includes multiple names or multiple superior object classes. + * The known examples are: + *

    + *
  • ( 1.3.6.1.4.1.4203.1.4.1 NAME ( 'OpenLDAProotDSE' 'LDAProotDSE' ) DESC 'OpenLDAP Root DSE object' SUP top STRUCTURAL MAY cn )
  • + *
  • ( 0.9.2342.19200300.100.4.4 NAME ( 'pilotPerson' 'newPilotPerson' ) SUP person STRUCTURAL MAY ( userid $ textEncodedORAddress $ rfc822Mailbox $ favouriteDrink $ roomNumber $ userClass $ homeTelephoneNumber $ homePostalAddress $ secretary $ personalTitle $ preferredDeliveryMethod $ businessCategory $ janetMailbox $ otherMailbox $ mobileTelephoneNumber $ pagerTelephoneNumber $ organizationalStatus $ mailPreferenceOption $ personalSignature ) )
  • + *
  • ( 0.9.2342.19200300.100.4.20 NAME 'pilotOrganization' SUP ( organization $ organizationalUnit ) STRUCTURAL MAY buildingName )
  • + *
+ */ + +@Data +public class ObjectClassInformation { + private static final Logger LOG = LoggerFactory.getLogger(ObjectClassInformation.class); + + private static final String KEYWORD_DESCRIPTION = "DESC"; + private static final String KEYWORD_MAY = "MAY"; + private static final String KEYWORD_MUST = "MUST"; + private static final String KEYWORD_NAME = "NAME"; + private static final String KEYWORD_SUP = "SUP"; + + public enum Type { + ABSTRACT, + STRUCTURAL, + AUXILIARY + }; + + // this does not match ALL of the objectClass definitions! + private static final Pattern PATTERN = Pattern.compile("^\\( " + + "([0-9\\.]+) " + // required + KEYWORD_NAME + " '([^']+)'" + // required + "( " + KEYWORD_DESCRIPTION + " '([^']+)')?" + + "( " + KEYWORD_SUP + " ([^ ]+))?" + + // " (" + String.join("|", Type.values()) + ")" + + " (ABSTRACT|STRUCTURAL|AUXILIARY)" + + "( " + KEYWORD_MUST + " (([^ (]+)|(\\( ([^)]+)\\))))?" + + "( " + KEYWORD_MAY + " (([^ (]+)|(\\( ([^)]+)\\))))?" + + "(.*)" + + " \\)$"); + + // remember that Spring#split uses a regex! + private static final String DELIMITER = " \\$ "; + + private String oid; + + private String name; + + private String description; + + private String parent; + + private Type type; + + private List mustValues = new ArrayList<>(); + + private List mayValues = new ArrayList<>(); + + public void setType(String type) { + this.type = Type.valueOf(type); + } + + /** + * Parse definition from ObjectClass attribute + * + * @param definition + * @return objectClass information + * @throws IllegalArgumentException unsupported definition + */ + public static ObjectClassInformation parse(String definition) { + final ObjectClassInformation info = new ObjectClassInformation(); + + final Matcher m = PATTERN.matcher(definition); + if (!m.matches()) { + // LOG.info(PATTERN.pattern()); + throw new IllegalArgumentException("Unsupported definition: " + definition); + } + + info.oid = m.group(1); + info.name = m.group(2); + info.description = m.group(4); + info.parent = m.group(6); + info.setType(m.group(7)); + + // singleton + if (StringUtils.isNotBlank(m.group(10))) { + info.mustValues.add(m.group(10)); + } + + // list + if (StringUtils.isNotBlank(m.group(12))) { + for (String element : m.group(12).trim().split(DELIMITER)) { + info.mustValues.add(element.trim()); + } + } + + // singleton + if (StringUtils.isNotBlank(m.group(15))) { + info.mayValues.add(m.group(15)); + } + + // list + if (StringUtils.isNotBlank(m.group(17))) { + for (String element : m.group(17).trim().split(DELIMITER)) { + info.mayValues.add(element.trim()); + } + } + + return info; + } + + /** + * Regenerate definition + * @return + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("( "); + sb.append(oid); + sb.append(" "); + sb.append(KEYWORD_NAME); + sb.append(" '"); + sb.append(name); + sb.append("' "); + if (StringUtils.isNotBlank(description)) { + sb.append(KEYWORD_DESCRIPTION); + sb.append(" '"); + sb.append(description); + sb.append("' "); + } + if (StringUtils.isNotBlank(parent)) { + sb.append(KEYWORD_SUP); + sb.append(" "); + sb.append(parent); + sb.append(" "); + } + sb.append(type); + sb.append(" "); + + if (!mustValues.isEmpty()) { + sb.append(KEYWORD_MUST); + sb.append(" "); + if (mustValues.size() == 1) { + sb.append(mustValues.get(0)); + } else { + sb.append("( "); + sb.append(String.join(" $ ", mustValues)); + sb.append(" )"); + } + sb.append(" "); + } + + if (!mayValues.isEmpty()) { + sb.append(KEYWORD_MAY); + sb.append(" "); + if (mayValues.size() == 1) { + sb.append(mayValues.get(0)); + } else { + sb.append("( "); + sb.append(String.join(" $ ", mayValues)); + sb.append(" )"); + } + sb.append(" "); + } + + sb.append(")"); + return sb.toString(); + } +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/OpenLdapConfiguration.java b/modules/openldap/src/main/java/org/testcontainers/containers/OpenLdapConfiguration.java new file mode 100644 index 00000000000..6fbc8f3a360 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/OpenLdapConfiguration.java @@ -0,0 +1,36 @@ +package org.testcontainers.containers; + +import lombok.Data; + +/** + * LDAP configuration + */ +@Data +public class OpenLdapConfiguration { + + private Boolean allowEmptyPassword; + + private Boolean containerDebuggingEnabled; + + private Boolean allowAnonBinding; + + private String baseDN; + + private String adminUsername; + + private String adminPassword; + + private Boolean configAdminEnabled; + + private String configAdminUsername; + + private String configAdminPassword; + + private String userDC; + + private String group; + + private Boolean enableTls; + + private Boolean requireTls; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/OpenLdapContainer.java b/modules/openldap/src/main/java/org/testcontainers/containers/OpenLdapContainer.java new file mode 100644 index 00000000000..6186f950c92 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/OpenLdapContainer.java @@ -0,0 +1,782 @@ +package org.testcontainers.containers; + +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.schema.InetOrgPerson; +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.utility.DockerImageName; + +import javax.naming.Context; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Testcontainers implementation for OpenLDAP. + *

+ * Supported images: {@code bitnami/openldap}, {@code openldap} + *

+ *

+ * Exposed ports: + *

    + *
  • LDAP: 389
  • + *
  • LDAPS: 636
  • + *
+ *

+ * + *

Additional functionality

+ *

+ * This class provides additional functionality intended to simply writing tests. + * The first is a collection of methods to acquire a connection to the server: + *

    + *
  • connectAnonymously()
  • + *
  • connectAsAdmin()
  • + *
  • connectAsConfig()
  • + *
  • connectAsUser(String username, String password)
  • + *
+ *

+ *

+ * The second is a collection of methods to retrieve standard information from + * the server: + *

    + *
  • listObjectClasses()
  • + *
+ *

+ *

+ * Analogous methods are provided for any LDAP connection, although it's important + * to remember that servers have different implementations. E.g., 'schemas' vs + * 'subschemas'. + *

    + *
  • listObjectClasses(DirContext ctx)
  • + *
  • listUsers(DirContext ctx, String query)
  • + *
  • getUserDetails(DirContext ctx, String query)
  • + *
+ *

+ * + *

Limitations

+ *

+ * This implementation does not support the following features of the + * underlying Bitnami docker container: + *

    + *
  • AccessLog Module
  • + *
  • Syncrepl Module
  • + *
  • Proxy Protocol Support
  • + *
+ *

+ * + *

Extensions

+ *

+ *

    + *
  • Additional certs should be added to /opt/bitnami/openldap/certs'
  • + *
  • Custom initialization scripts can be added to '/docker-entrypoint-initdb.d/'
  • + *
  • Persistence can be supported with '/bitnami/openldap/'
  • + *
  • The json log driver is used by default. It can be modified with '--log-driver'
  • + *
+ *

+ */ +public class OpenLdapContainer extends GenericContainer { + private static final Logger LOG = LoggerFactory.getLogger(OpenLdapContainer.class); + + private static final String LDAP_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; + + private static final String LDAP_PROTOCOL_DEFAULT = "ldap"; + + private static final String LDAP_PROTOCOL_SECURED = "ldaps"; + + private static final String LDAP_AUTHENTICATION_ANONYMOUS = "none"; + + // provide username but no password - not sure if this is the correct term + // private static final String LDAP_AUTHENTICATION_UNAUTHENTICATED = "unauthenticated"; + + private static final String LDAP_AUTHENTICATION_SIMPLE = "simple"; + + // advanced techniques. May need to specify something more precise, e.g., "kerberos" + // private static final String LDAP_AUTHENTICATION_SASL = "sasl"; + + // default values used by Bitnami docker image + private static final Boolean DEFAULT_ALLOW_ANON_BINDING = Boolean.FALSE; + + private static final Boolean DEFAULT_ALLOW_EMPTY_PASSWORD = Boolean.FALSE; + + private static final Boolean DEFAULT_BITNAMI_DEBUG = Boolean.FALSE; + + private static final String DEFAULT_BASE_DN = "dc=example,dc=org"; + + private static final String DEFAULT_ADMIN_USERNAME = "admin"; + + private static final String DEFAULT_ADMIN_PASSWORD = "adminpassword"; + + private static final Boolean DEFAULT_CONFIG_ADMIN_ENABLED = Boolean.FALSE; + + private static final String DEFAULT_CONFIG_USERNAME = "admin"; + + private static final String DEFAULT_CONFIG_PASSWORD = "configpassword"; + + private static final String DEFAULT_USER_DC = "users"; + + private static final String DEFAULT_GROUP = "readers"; + + private static final Boolean DEFAULT_ENABLE_TLS = Boolean.FALSE; + + private static final Boolean DEFAULT_REQUIRE_TLS = Boolean.FALSE; + + // default image name + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("bitnami/openldap"); + + // default tag (current as this module is written) + private static final String DEFAULT_TAG = "2.5.18"; + + // default value used by the Bitnami docker container + private static final int LDAP_PORT = 1389; + + // default value used by the Bitnami docker container + private static final int LDAPS_PORT = 1636; + + // default schameas: cosine,inetorgperson,nis + + // https://hub.docker.com/r/bitnami/openldap + + // LDAP_USERS: Comma separated list of LDAP users to create in the default LDAP tree. Default: user01,user02 + // LDAP_PASSWORDS: Comma separated list of passwords to use for LDAP users. Default: bitnami1,bitnami2 + // + // LDAP_ADD_SCHEMAS: Whether to add the schemas specified in LDAP_EXTRA_SCHEMAS. Default: yes + // LDAP_EXTRA_SCHEMAS: Extra schemas to add, among OpenLDAP's distributed schemas. Default: cosine, inetorgperson, nis + // LDAP_SKIP_DEFAULT_TREE: Whether to skip creating the default LDAP tree based on LDAP_USERS, LDAP_PASSWORDS, LDAP_USER_DC and LDAP_GROUP. Please note that this will not skip the addition of schemas or importing of LDIF files. Default: no + // LDAP_CUSTOM_LDIF_DIR: Location of a directory that contains LDIF files that should be used to bootstrap the database. Only files ending in .ldif will be used. Default LDAP tree based on the LDAP_USERS, LDAP_PASSWORDS, LDAP_USER_DC and LDAP_GROUP will be skipped when LDAP_CUSTOM_LDIF_DIR is used. When using this it will override the usage of LDAP_USERS, LDAP_PASSWORDS, LDAP_USER_DC and LDAP_GROUP. You should set LDAP_ROOT to your base to make sure the olcSuffix configured on the database matches the contents imported from the LDIF files. Default: /ldifs + // LDAP_CUSTOM_SCHEMA_FILE: Location of a custom internal schema file that could not be added as custom ldif file (i.e. containing some structuralObjectClass). Default is /schema/custom.ldif" + // LDAP_CUSTOM_SCHEMA_DIR: Location of a directory containing custom internal schema files that could not be added as custom ldif files (i.e. containing some structuralObjectClass). This can be used in addition to or instead of LDAP_CUSTOM_SCHEMA_FILE (above) to add multiple schema files. Default: /schemas + + // LDAP_TLS_CERT_FILE=/opt/bitnami/openldap/certs/openldap.crt + // LDAP_TLS_KEY_FILE=/opt/bitnami/openldap/certs/openldap.key + // LDAP_TLS_CA_FILE=/opt/bitnami/openldap/certs/openldapCA.crt + + private final OpenLdapConfiguration configuration; + + /** + * Default constructor + */ + public OpenLdapContainer() { + this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); + } + + /** + * Standard constructor + * + * @param dockerImageName Docker image compatible with 'bitnami:openldap' + */ + public OpenLdapContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + + this.waitStrategy = + new LogMessageWaitStrategy() + .withRegEx(".*[0-9a-f]{8}.+slapd starting.*\\s") + .withStartupTimeout(Duration.of(10, ChronoUnit.SECONDS)); + this.configuration = new OpenLdapConfiguration(); + // - in case we need something like this... + // super.withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withCapAdd(Capability.IPC_LOCK)); + } + + /** + * Specify whether to allow anonymous binding + * + * @param allowAnonBinding true if anonymous binding should be allowed. (Default: true) + * @return self + */ + public OpenLdapContainer withAllowAnonBinding(Boolean allowAnonBinding) { + configuration.setAllowAnonBinding(allowAnonBinding); + return self(); + } + + /** + * Specify whether empty passwords are allowed + * + * @param allowEmptyPassword true if empty passwords are allowed. (Default: false) + * @return self + */ + public OpenLdapContainer withAllowEmptyPassword(Boolean allowEmptyPassword) { + configuration.setAllowEmptyPassword(allowEmptyPassword); + return self(); + } + + /** + * Specify whether container-based debugging should be enabled + * + * @param containerDebuggingEnabled true if container-based debugging should be enabled. (Default: false) + * @return self + */ + public OpenLdapContainer withContainerDebuggingEnabled(Boolean containerDebuggingEnabled) { + configuration.setContainerDebuggingEnabled(containerDebuggingEnabled); + return self(); + } + + /** + * Specify Base Distinguished Name + * + * @param baseDN Base Distinguished Name (No default value) + * @return self + */ + public OpenLdapContainer withBaseDN(String baseDN) { + configuration.setBaseDN(baseDN); + return self(); + } + + /** + * Specify administrative username and password + * + * @param username administrative user name (default: 'admin') + * @param password administrative user password (default: 'adminpassword') + * @return self + */ + public OpenLdapContainer withAdminUsername(String username, String password) { + configuration.setAdminUsername(username); + configuration.setAdminPassword(password); + return self(); + } + + /** + * Specify configuration administrative username and password + * + * @param username configuration administrative user name (default: 'admin') + * @param password configuration administrative user password (default: 'adminpassword') + * @return self + */ + public OpenLdapContainer withConfigAdminUsername(String username, String password) { + configuration.setConfigAdminEnabled(Boolean.TRUE); + configuration.setConfigAdminUsername(username); + configuration.setConfigAdminPassword(password); + return self(); + } + + /** + * Specify whether to enable TLS + * + * @param enableTls true to enable TLS (default: false) + * @return self + */ + public OpenLdapContainer withEnableTls(Boolean enableTls) { + configuration.setEnableTls(enableTls); + return self(); + } + + /** + * Specify whether to require TLS + * + * @param requireTls true to require TLS (default: false) + * @return self + */ + public OpenLdapContainer withRequireTls(Boolean requireTls) { + configuration.setEnableTls(requireTls); + return self(); + } + + /** + * Specify user domain component + * + * @param userDc user domain component (default: 'users') + * @return self + */ + public OpenLdapContainer withUserDc(String userDc) { + configuration.setUserDC(userDc); + return self(); + } + + /** + * Specify name of group attribute + * + * @param group name of group attribute (default: 'readers') + * @return self + */ + public OpenLdapContainer withGroup(String group) { + configuration.setGroup(group); + return self(); + } + + /** + * Is anonymous binding permitted? + * + * @return true if anonymous binding is permitted + */ + public Boolean getAllowAnonBinding() { + if (configuration.getAllowAnonBinding() != null) { + return configuration.getAllowAnonBinding(); + } + return DEFAULT_ALLOW_ANON_BINDING; + } + + /** + * Are empty passwords allowed? + * + * @return true if empty passwords are allowed + */ + public Boolean getAllowEmptyPassword() { + if (configuration.getAllowEmptyPassword() != null) { + return configuration.getAllowEmptyPassword(); + } + return DEFAULT_ALLOW_EMPTY_PASSWORD; + } + + /** + * Is container debugging enabled? + * + * @return true if container debugging is enabled + */ + public Boolean getContainerDebuggingEnabled() { + if (configuration.getContainerDebuggingEnabled()) { + return configuration.getContainerDebuggingEnabled(); + } + return DEFAULT_BITNAMI_DEBUG; + } + + /** + * Get BaseDN + * + * @return baseDN. May be null. + */ + public String getBaseDN() { + if (StringUtils.isNotBlank(configuration.getBaseDN())) { + return configuration.getBaseDN(); + } + return DEFAULT_BASE_DN; + } + + /** + * Get administrative user name + * + * @return administrative user name + */ + public String getAdminUsername() { + if (StringUtils.isNotBlank(configuration.getAdminUsername())) { + return configuration.getAdminUsername(); + } + return DEFAULT_ADMIN_USERNAME; + } + + /** + * Get administrative user password + * + * @return administrative user password + */ + public String getAdminPassword() { + if (StringUtils.isNotBlank(configuration.getAdminPassword())) { + return configuration.getAdminPassword(); + } + return DEFAULT_ADMIN_PASSWORD; + } + + /** + * Is configuration administrator enabled? + * + * @return true if configuration administration is enabled + */ + public Boolean getConfigAdminEnabled() { + if (configuration.getConfigAdminEnabled() != null) { + return configuration.getConfigAdminEnabled(); + } + return DEFAULT_CONFIG_ADMIN_ENABLED; + } + + /** + * Get configuration administrative user name + * + * @return configuration administrative user name + */ + public String getConfigUsername() { + if (StringUtils.isNotBlank(configuration.getConfigAdminUsername())) { + return configuration.getConfigAdminUsername(); + } + return DEFAULT_CONFIG_USERNAME; + } + + /** + * Get configuration administrative user password + * + * @return configuration administrative user password + */ + public String getConfigPassword() { + if (StringUtils.isNotBlank(configuration.getConfigAdminPassword())) { + return configuration.getConfigAdminPassword(); + } + return DEFAULT_CONFIG_PASSWORD; + } + + /** + * Get user domain component + * + * @return user domain component + */ + public String getUserDC() { + if (StringUtils.isNotBlank(configuration.getUserDC())) { + return configuration.getUserDC(); + } + return DEFAULT_USER_DC; + } + + /** + * Get name of group attribute + * @return name of group attribute + */ + public String getGroup() { + if (StringUtils.isNotBlank(configuration.getGroup())) { + return configuration.getGroup(); + } + return DEFAULT_GROUP; + } + + /** + * Get remapped LDAP (389) port + * + * @return remapped LDAP port, if available + */ + public int getLdapPort() { + return getMappedPort(LDAP_PORT); + } + + /** + * Get remapped LDAPS (636) port + * + * @return remapped LDAPS port, if available + */ + public int getLdapsPort() { + return getMappedPort(LDAPS_PORT); + } + + /** + * {@inheritDoc} + */ + @Override + @SneakyThrows + protected void configure() { + + if (configuration.getAllowEmptyPassword() != null) { + addEnv("ALLOW_EMPTY_PASSWORD", configuration.getAllowEmptyPassword().toString()); + } + + if (configuration.getContainerDebuggingEnabled() != null) { + addEnv("BITNAMI_DEBUG", configuration.getContainerDebuggingEnabled().toString()); + } + + if (configuration.getAllowAnonBinding() != null) { + addEnv("LDAP_ALLOW_ANON_BINDING", configuration.getAllowAnonBinding().toString()); + } + + if (StringUtils.isNotBlank(configuration.getBaseDN())) { + addEnv("LDAP_ROOT", configuration.getBaseDN()); + } + + if (StringUtils.isNotBlank(configuration.getAdminUsername())) { + addEnv("LDAP_ADMIN_USERNAME", configuration.getAdminUsername()); + } + + if (StringUtils.isNotBlank(configuration.getAdminPassword())) { + addEnv("LDAP_ADMIN_PASSWORD", configuration.getAdminPassword()); + } + + if (Boolean.TRUE.equals(configuration.getConfigAdminEnabled())) { + addEnv("LDAP_CONFIG_ADMIN_ENABLED", configuration.getConfigAdminEnabled().toString()); + if (StringUtils.isNotBlank(configuration.getConfigAdminUsername())) { + addEnv("LDAP_CONFIG_ADMIN_USERNAME", configuration.getConfigAdminUsername()); + } + + if (StringUtils.isNotBlank(configuration.getConfigAdminPassword())) { + addEnv("LDAP_CONFIG_ADMIN_PASSWORD", configuration.getConfigAdminPassword()); + } + } + + if (StringUtils.isNotBlank(configuration.getUserDC())) { + addEnv("LDAP_USER_DC", configuration.getUserDC()); + } + + if (StringUtils.isNotBlank(configuration.getGroup())) { + addEnv("LDAP_GROUP", configuration.getGroup()); + } + + // TODO - add users, passwords + + if (configuration.getRequireTls() != null) {} + + // TODO - add schemas + + // Add Default Ports + // note: it's usually one or the other! + this.addExposedPort(LDAP_PORT); + this.addExposedPort(LDAPS_PORT); + } + + @Override + public Set getLivenessCheckPortNumbers() { + return new HashSet<>(getLdapPort()); + } + + @Override + protected void waitUntilContainerStarted() { + getWaitStrategy().waitUntilReady(this); + } + + // @Override + // @SneakyThrows + // private void containerIsStarted(InspectContainerResponse containerInfo) { + // } + + /** + * Get the server's URL + *

+ * Implementation note: we can't use java.net.URL/URI since 'LDAP' does not have + * a registered URL handler. + * + * @return server's URL + */ + public String getUrl() { + // TODO: know whether to use LDAP/LDAPS, and which port + return String.format("%s://%s:%d/", "ldap", getHost(), getLdapPort()); + } + + /** + * Get anonymous connection to the server + * + * @return LDAP connection + * @throws NamingException + */ + public DirContext connectAnonymously() throws NamingException { + return connectAsUser(null, null, LDAP_AUTHENTICATION_ANONYMOUS); + } + + + /** + * Get authenticated connection to the server as the admin user + * + * @return LDAP connection + * @throws NamingException + */ + public DirContext connectAsAdmin() throws NamingException { + return connectAsUser(getAdminUsername(), getAdminPassword(), LDAP_AUTHENTICATION_SIMPLE); + } + + /** + * Get authenticated connection to the server as the configuration admin user + * + * @return LDAP connection + * @throws NamingException + */ + public DirContext connectAsConfigAdmin() throws NamingException { + return connectAsUser(getConfigUsername(), getConfigPassword(), LDAP_AUTHENTICATION_SIMPLE); + } + + /** + * Get authenticated connection to the server as any user + * + * @param username username + * @param password password + * @return LDAP connection + * @throws NamingException + */ + public DirContext connectAsUser(String username, String password) throws NamingException { + return connectAsUser(username, password, LDAP_AUTHENTICATION_SIMPLE); + } + + /** + * Get JNDI DirContext that points to specified LDAP server + * + * @param username username + * @param password password + * @param authentication authentication mechanism + * @return LDAP connection + * @throws NamingException + */ + private DirContext connectAsUser(String username, String password, + String authentication) throws NamingException { + final Hashtable env = new Hashtable(); + env.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_CONTEXT_FACTORY); + env.put(Context.PROVIDER_URL, getUrl()); + env.put(Context.SECURITY_AUTHENTICATION, authentication); + + if (StringUtils.isNotBlank(username)) { + // this may not work with inetOrgPerson users - only tested with admin + env.put(Context.SECURITY_PRINCIPAL, "cn=" + username + "," + getBaseDN()); + } + + if (StringUtils.isNotBlank(password)) { + env.put(Context.SECURITY_CREDENTIALS, password); + } + + return new InitialDirContext(env); + } + + /** + * List object classes. + * + * @return list of objectClasses + * @throws NamingException error communicating with LDAP server + * @throws NameNotFoundException if 'subschema' isn't found (may be schema) + */ + public List listObjectClasses() throws NamingException { + DirContext context = null; + try { + context = connectAsAdmin(); + return listObjectClasses(context); + } finally { + if (context != null) { + context.close(); + } + } + } + + /** + * List object classes. + * + * @param context LDAP connection + * @return list of objectClasses + * @throws NamingException error communicating with LDAP server + * @throws NameNotFoundException if 'subschema' isn't found (may be schema) + */ + public List listObjectClasses(DirContext context) throws NamingException { + final List objectClasses = new ArrayList<>(); + + final SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.OBJECT_SCOPE); + searchControls.setReturningAttributes(new String[]{"objectClasses"}); + + final NamingEnumeration objectClassesSearchResults = + context.search("cn=subschema", "(ObjectClass=*)", searchControls); + + while (objectClassesSearchResults.hasMoreElements()) { + final SearchResult result = objectClassesSearchResults.next(); + + final Attributes attributes = result.getAttributes(); + final Attribute attr = attributes.get("objectClasses"); + + final NamingEnumeration objectClassesEnumeration = attr.getAll(); + while (objectClassesEnumeration.hasMoreElements()) { + final String objectClassesRecord = (String) objectClassesEnumeration.next(); + try { + objectClasses.add(ObjectClassInformation.parse(objectClassesRecord)); + } catch (IllegalArgumentException e) { + // sadly not unexpected... + LOG.warn(e.getMessage()); + } + } + } + + return objectClasses; + } + + /** + * List all users. This method returns a Map with a key of (unordered) user CN + * and a value of all IDs. + * + * @param context + * @param query query (e.g., "ou=people," + String.join("," dc)) + * @return Map of user 'cn' and respective 'ids' + * @throws NamingException + */ + public Map> listUsers(DirContext context, String query) throws NamingException { + final Map> users = new LinkedHashMap<>(); + + // connect to server. + final SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(new String[]{"cn"}); + + try { + final NamingEnumeration userSearchResultEnumeration = + context.search(query, "(ObjectClass=inetOrgPerson)", searchControls); + while (userSearchResultEnumeration.hasMoreElements()) { + final List ids = new ArrayList<>(); + + final SearchResult userSearchResult = userSearchResultEnumeration.next(); + final Attributes entry = userSearchResult.getAttributes(); + final NamingEnumeration idEnumeration = (NamingEnumeration) entry.getIDs(); + while (idEnumeration.hasMoreElements()) { + ids.add(idEnumeration.nextElement()); + } + users.put(entry.get("cn").toString(), ids); + } + } catch (NameNotFoundException e) { + // do nothing - return empty map + } + + return users; + } + + /** + * List details a user. + * + * @param context + * @param dn + * @return optional matching user + * @throws NamingException + */ + public Optional getUserDetails(DirContext context, String dn) throws NamingException { + final List users = new ArrayList<>(); + + final SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.OBJECT_SCOPE); + + try { + final NamingEnumeration inetOrgPersonSearchResultEnumeration = + context.search(dn, "(ObjectClass=inetOrgPerson)", searchControls); + while (inetOrgPersonSearchResultEnumeration.hasMoreElements()) { + final SearchResult inetOrgPersonSearchResult = inetOrgPersonSearchResultEnumeration.next(); + final InetOrgPerson person = parseInetOrgPerson(inetOrgPersonSearchResult); + users.add(person); + } + } catch (NameNotFoundException e) { + return Optional.empty(); + } + + // there should be, at most, a single match + if (users.size() > 1) { + LOG.warn("multiple matches found! dn = '{}'", dn); + } + + return Optional.of(users.get(0)); + } + + /** + * Parse search results for individual person + * @param personSearchResult results of query + * @return user information + */ + InetOrgPerson parseInetOrgPerson(SearchResult personSearchResult) { + final InetOrgPerson person = new InetOrgPerson(); + + final Attributes entry = personSearchResult.getAttributes(); + + // these are required attributes + person.setCn(entry.get("cn").toString()); + person.setSn(entry.get("sn").toString()); + + /* + final NamingEnumeration attrs = (NamingEnumeration) entry.getAll(); + while (attrs.hasMoreElements()) { + final List attrValues = new ArrayList<>(); + final Attribute attr = attrs.nextElement(); + final NamingEnumeration values = (NamingEnumeration) attr.getAll(); + while (values.hasMoreElements()) { + attrValues.add(values.nextElement()); + } + // userDetails.put(attr.getID(), attrValues); + } + */ + + return person; + } +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/ApplicationEntity.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/ApplicationEntity.java new file mode 100644 index 00000000000..bd050b0e69b --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/ApplicationEntity.java @@ -0,0 +1,40 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: an application entity + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.12", description = "RFC2256: an application entity") +public class ApplicationEntity extends Top { + @Must + private String presentationAddress; + + @Must + private String cn; + + @May + private String supportedApplicationContext; + + @May + private String seeAlso; + + @May + private String ou; + + @May + private String o; + + @May + private String l; + + @May + private String description; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/ApplicationProcess.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/ApplicationProcess.java new file mode 100644 index 00000000000..39a6bb702a3 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/ApplicationProcess.java @@ -0,0 +1,28 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: an application process + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.11", description = "RFC2256: an application process") +public class ApplicationProcess extends Top { + @Must + private String cn; + + @May + private String seeAlso; + @May + private String ou; + @May + private String l; + @May + private String description; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/CertificationAuthority.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/CertificationAuthority.java new file mode 100644 index 00000000000..8f7e408e502 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/CertificationAuthority.java @@ -0,0 +1,30 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: a certificate authorith + */ +@Data +@Oid(value = "2.5.6.16", description = "RFC2256: a certificate authority") +@Rfc(RfcValue.RFC_2256) +public class CertificationAuthority extends Top { + public static final String DESCRIPTION = "RFC2256: a certificate authority"; + + @Must + private String authorityRevocationList; + + @Must + private String certificateRevocationList; + + @Must + private String cACertificate; + + @May + private String crossCertificatePair; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/Country.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Country.java new file mode 100644 index 00000000000..2550e2c8a4a --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Country.java @@ -0,0 +1,26 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: a country + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.2", description = "RFC2256: a country") +public class Country extends Top { + + @Must + private String c; + + @May + private String searchGuide; + + @May + private String description; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/DSA.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/DSA.java new file mode 100644 index 00000000000..74f04e7f55e --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/DSA.java @@ -0,0 +1,18 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: a directory system agent (a server) + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.13", description = "RFC2256: a directory system agent (a server)") +public class DSA extends ApplicationEntity { + @May + private String knowledgeInformation; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/Device.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Device.java new file mode 100644 index 00000000000..9dc3704d3db --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Device.java @@ -0,0 +1,40 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * "RFC2256: a device" + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.14", description = "RFC2256: a device") +public class Device extends Top { + @Must + private String cn; + + @May + private String serialNumber; + + @May + private String seeAlso; + + @May + private String owner; + + @May + private String ou; + + @May + private String o; + + @May + private String l; + + @May + private String description; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/GroupOfNames.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/GroupOfNames.java new file mode 100644 index 00000000000..6ccefbe9eb3 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/GroupOfNames.java @@ -0,0 +1,41 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: a group of names (DNs) + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.9", description = "RFC2256: a group of names (DNs)") +public class GroupOfNames extends Top { + + @Must + private String member; + + @Must + private String cn; + + @May + public String businessCategory; + + @May + public String seeAlso; + + @May + public String owner; + + @May + public String ou; + + @May + public String o; + + @May + public String description; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/GroupOfUniqueNames.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/GroupOfUniqueNames.java new file mode 100644 index 00000000000..729808e7f70 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/GroupOfUniqueNames.java @@ -0,0 +1,41 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: a group of unique names (DN and Unique Identifier) + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.17", description = "RFC2256: a group of unique names (DN and Unique Identifier") +public class GroupOfUniqueNames extends Top { + + @Must + private String uniqueMember; + + @Must + private String cn; + + @May + private String businessCategory; + + @May + private String seeAlso; + + @May + private String owner; + + @May + private String ou; + + @May + private String o; + + @May + private String description; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/InetOrgPerson.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/InetOrgPerson.java new file mode 100644 index 00000000000..8ab94743d40 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/InetOrgPerson.java @@ -0,0 +1,72 @@ +package org.testcontainers.containers.schema; + + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2798: Internet Organizational Person + */ +@Data +@Rfc(RfcValue.RFC_2798) +@Oid(value = "2.16.840.1.113730.3.2.2", description = "RFC2798: Internet Organizational Person") +public class InetOrgPerson extends OrganizationalPerson implements PkiUser { + + @May + private String audio; + @May + private String businessCategory; + @May + private String carLicense; + @May + private String departmentNumber; + @May + private String displayName; + @May + private String employeeNumber; + @May + private String employeeType; + @May + private String givenName; + @May + private String homePhone; + @May + private String homePostalAddress; + @May + private String initials; + @May + private String jpegPhoto; + @May + private String labeledURI; + @May + private String mail; + @May + private String manager; + @May + private String mobile; + @May + private String o; + @May + private String pager; + @May + private String photo; + @May + private String roomNumber; + @May + private String secretary; + @May + private String uid; + @May + private String userCertificate; + @May + private String x500uniqueIdentifier; + @May + private String preferredLanguage; + @May + private String userSMIMECertificate; + @May + private String userPKCS12; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/Locality.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Locality.java new file mode 100644 index 00000000000..e689baeed58 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Locality.java @@ -0,0 +1,34 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: a locality + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.3", description = "RFC2256: a locality") +public class Locality extends Top { + + @May + private String street; + + @May + private String seeAlso; + + @May + private String searchGuide; + + @May + private String st; + + @May + private String l; + + @May + private String description;; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/Organization.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Organization.java new file mode 100644 index 00000000000..46d4f60ac8a --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Organization.java @@ -0,0 +1,63 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: an organization + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.4", description = "RFC2256: an organization") +public class Organization extends Top { + + @Must + private String o; + + @May + private String userPassword; + @May + private String searchGuide; + @May + private String seeAlso; + @May + private String businessCategory; + @May + private String x121Address; + @May + private String registeredAddress; + @May + private String destinationIndicator; + @May + private String preferredDeliveryMethod; + @May + private String telexNumber; + @May + private String teletexTerminalIdentifier; + @May + private String telephoneNumber; + @May + private String internationaliSDNNumber; + @May + private String facsimileTelephoneNumber; + @May + private String street; + @May + private String postOfficeBox; + @May + private String postalCode; + @May + private String postalAddress; + @May + private String physicalDeliveryOfficeName; + @May + private String st; + @May + private String l; + @May + private String description; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalPerson.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalPerson.java new file mode 100644 index 00000000000..12f9a55c99a --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalPerson.java @@ -0,0 +1,53 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: an organizational person + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.7", description = "RFC2256: an organizational person") +public class OrganizationalPerson extends Person { + + @May + private String title; + @May + private String x121Address; + @May + private String registeredAddress; + @May + private String destinationIndicator; + @May + private String preferredDeliveryMethod; + @May + private String telexNumber; + @May + private String teletexTerminalIdentifier; + @May + private String telephoneNumber; + @May + private String internationaliSDNNumber; + @May + private String facsimileTelephoneNumber; + @May + private String street; + @May + private String postOfficeBox; + @May + private String postalCode; + @May + private String postalAddress; + @May + private String physicalDeliveryOfficeName; + @May + private String ou; + @May + private String st; + @May + private String l; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalRole.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalRole.java new file mode 100644 index 00000000000..75769621d89 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalRole.java @@ -0,0 +1,61 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: an organizational role + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.8", description = "RFC2256: an organizational role") +public class OrganizationalRole extends Top { + + @Must + public String cn; + + @May + private String x121Address; + @May + private String registeredAddress; + @May + private String destinationIndicator; + @May + private String preferredDeliveryMethod; + @May + private String telexNumber; + @May + private String teletexTerminalIdentifier; + @May + private String telephoneNumber; + @May + private String internationaliSDNNumber; + @May + private String facsimileTelephoneNumber; + @May + private String seeAlso; + @May + private String roleOccupant; + @May + private String street; + @May + private String postOfficeBox; + @May + private String postalCode; + @May + private String postalAddress; + @May + private String physicalDeliveryOfficeName; + @May + private String ou; + @May + private String st; + @May + private String l; + @May + private String description; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalUnit.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalUnit.java new file mode 100644 index 00000000000..287f7997e46 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/OrganizationalUnit.java @@ -0,0 +1,63 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC22256: an organization unit + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.5", description = "RFC2256: an organizational unit") +public class OrganizationalUnit extends Top { + + @Must + private String ou; + + @May + private String userPassword; + @May + private String searchGuide; + @May + private String seeAlso; + @May + private String businessCategory; + @May + private String x121Address; + @May + private String registeredAddress; + @May + private String destinationIndicator; + @May + private String preferredDeliveryMethod; + @May + private String telexNumber; + @May + private String teletexTerminalIdentifier; + @May + private String telephoneNumber; + @May + private String internationaliSDNNumber; + @May + private String facsimileTelephoneNumber; + @May + private String street; + @May + private String postOfficeBox; + @May + private String postalCode; + @May + private String postalAddress; + @May + private String physicalDeliveryOfficeName; + @May + private String st; + @May + private String l; + @May + private String description; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/Person.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Person.java new file mode 100644 index 00000000000..322e5dc4f2a --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Person.java @@ -0,0 +1,35 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: A Person + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.6", description = "RFC2256: a person") +public class Person extends Top { + + @Must + private String sn; + + @Must + private String cn; + + @May + private String userPassword; + + @May + private String telephoneNumber; + + @May + private String seeAlso; + + @May + private String description; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/PkiCA.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/PkiCA.java new file mode 100644 index 00000000000..0f7220f7744 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/PkiCA.java @@ -0,0 +1,28 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2587: PKI certificate authority + */ +@Data +@Rfc(RfcValue.RFC_2587) +@Oid(value = "2.5.6.22", description = "RFC2587: PKI certificate authority") +public class PkiCA extends Top { + + @May + private String authorityRevocationList; + + @May + private String certificateRevocationList; + + @May + private String cACertificate; + + @May + private String crossCertificatePair; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/PkiUser.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/PkiUser.java new file mode 100644 index 00000000000..63fbcf6cc3a --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/PkiUser.java @@ -0,0 +1,18 @@ +package org.testcontainers.containers.schema; + +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2587: a PKI user + */ +@Rfc(RfcValue.RFC_2587) +@Oid(value = "2.5.6.21", description = "RFC2587: a PKI user") +public interface PkiUser { + + // MAY + String getUserCertificate(); + + void setUserCertificate(String userCertificate); +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/ResidentialPerson.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/ResidentialPerson.java new file mode 100644 index 00000000000..a65b56d6f1a --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/ResidentialPerson.java @@ -0,0 +1,52 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.May; +import org.testcontainers.containers.schema.annotations.Must; +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: an residential person + */ +@Data +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.10", description = "RFC2256: an residential person") +public class ResidentialPerson extends Person { + @Must + private String l; + + @May + private String businessCategory; + @May + private String x121Address; + @May + private String registeredAddress; + @May + private String destinationIndicator; + @May + private String preferredDeliveryMethod; + @May + private String telexNumber; + @May + private String teletexTerminalIdentifier; + @May + private String telephoneNumber; + @May + private String internationaliSDNNumber; + @May + private String facsimileTelephoneNumber; + @May + private String street; + @May + private String postOfficeBox; + @May + private String postalCode; + @May + private String postalAddress; + @May + private String physicalDeliveryOfficeName; + @May + private String st; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/StrongAuthenticationUser.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/StrongAuthenticationUser.java new file mode 100644 index 00000000000..d8bb30a3b96 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/StrongAuthenticationUser.java @@ -0,0 +1,18 @@ +package org.testcontainers.containers.schema; + +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: a strong authentication user + */ +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.15", description = "RFC2256: a strong authentication user") +public interface StrongAuthenticationUser { + + // MUST + String getUserCertificate(); + + void setUserCertificate(String userCertificate); +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/Top.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Top.java new file mode 100644 index 00000000000..e7f63b13d49 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/Top.java @@ -0,0 +1,13 @@ +package org.testcontainers.containers.schema; + +import lombok.Data; +import org.testcontainers.containers.schema.annotations.Must; + +/** + * Top element + */ +@Data +public abstract class Top { + @Must + private String objectClass; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/UserSecurityInformation.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/UserSecurityInformation.java new file mode 100644 index 00000000000..4a6b1567102 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/UserSecurityInformation.java @@ -0,0 +1,18 @@ +package org.testcontainers.containers.schema; + +import org.testcontainers.containers.schema.annotations.Oid; +import org.testcontainers.containers.schema.annotations.Rfc; +import org.testcontainers.containers.schema.annotations.RfcValue; + +/** + * RFC2256: a user security information + */ +@Rfc(RfcValue.RFC_2256) +@Oid(value = "2.5.6.18", description = "RFC2256: a user security information") +public interface UserSecurityInformation { + + // MAY + String getSupportedAlgorithms(); + + void setSupportedAlgorithms(String algorithms); +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/May.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/May.java new file mode 100644 index 00000000000..8289a7bd2aa --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/May.java @@ -0,0 +1,15 @@ +package org.testcontainers.containers.schema.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation indicating that a field is optional + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface May { + String value() default ""; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Must.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Must.java new file mode 100644 index 00000000000..1ab3876c7b1 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Must.java @@ -0,0 +1,17 @@ +package org.testcontainers.containers.schema.annotations; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation indicating that a field is required. + *

+ * TODO: integrate into lombok for not-null checks + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Must { + String value() default ""; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Oid.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Oid.java new file mode 100644 index 00000000000..7db073fd0c7 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Oid.java @@ -0,0 +1,16 @@ +package org.testcontainers.containers.schema.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation providing OID and optional description + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Oid { + String value() default ""; + String description() default ""; +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Rfc.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Rfc.java new file mode 100644 index 00000000000..8a3ae0b5161 --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/Rfc.java @@ -0,0 +1,15 @@ +package org.testcontainers.containers.schema.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation giving the schema's RFC + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Rfc { + RfcValue value(); +} diff --git a/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/RfcValue.java b/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/RfcValue.java new file mode 100644 index 00000000000..38ef8d2030f --- /dev/null +++ b/modules/openldap/src/main/java/org/testcontainers/containers/schema/annotations/RfcValue.java @@ -0,0 +1,7 @@ +package org.testcontainers.containers.schema.annotations; + +public enum RfcValue { + RFC_2256, + RFC_2587, + RFC_2798 +} diff --git a/modules/openldap/src/test/java/org/testcontainers/containers/ObjectClassInformationTest.java b/modules/openldap/src/test/java/org/testcontainers/containers/ObjectClassInformationTest.java new file mode 100644 index 00000000000..acbcf8b4bfb --- /dev/null +++ b/modules/openldap/src/test/java/org/testcontainers/containers/ObjectClassInformationTest.java @@ -0,0 +1,47 @@ +package org.testcontainers.containers; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * ObjectClassInformation unit tests + */ +public class ObjectClassInformationTest { + + private static final Logger LOG = LoggerFactory.getLogger(ObjectClassInformationTest.class); + + /** + * Test the ObjectClassInformation parser + *

+ * We can cheat a bit - due to the way the class is implemented it's enough to ensure that + * parse() and toString() are consistent. + *

+ * + * @throws IOException an error occurred when reading the file containing the test data + */ + @Test + public void testParser() throws IOException { + URL url = Thread.currentThread().getContextClassLoader().getResource("objectClassDefinitions.txt"); + try (InputStream is = url.openStream(); + Reader r = new InputStreamReader(is); + BufferedReader br = new BufferedReader(r)) { + + for (String line = br.readLine(); line != null; line = br.readLine()) { + if (!line.isEmpty() && !line.startsWith("#")) { + final ObjectClassInformation info = ObjectClassInformation.parse(line); + assertThat(info.toString()).isEqualTo(line); + } + } + } + } +} diff --git a/modules/openldap/src/test/java/org/testcontainers/containers/OpenLdapContainerTest.java b/modules/openldap/src/test/java/org/testcontainers/containers/OpenLdapContainerTest.java new file mode 100644 index 00000000000..d8549664e83 --- /dev/null +++ b/modules/openldap/src/test/java/org/testcontainers/containers/OpenLdapContainerTest.java @@ -0,0 +1,81 @@ +package org.testcontainers.containers; + +import org.junit.ClassRule; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import java.net.MalformedURLException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This test shows the pattern to use the OpenLdapContainer @ClassRule for a junit test. + */ +public class OpenLdapContainerTest { + + private static final Logger LOG = LoggerFactory.getLogger(OpenLdapContainerTest.class); + + @ClassRule + public static OpenLdapContainer ldapContainer = new OpenLdapContainer(); + + /** + * Check anonymous access + * + * @throws NamingException + * @throws MalformedURLException + */ + @Test + public void testAnonymousAccess() throws NamingException, MalformedURLException { + DirContext context = null; + try { + context = ldapContainer.connectAnonymously(); + } finally { + if (context != null) { + context.close(); + } + } + } + + /** + * Check admin access + * + * @throws NamingException + * @throws MalformedURLException + */ + @Test + public void testAdminAccess() throws NamingException, MalformedURLException { + DirContext context = null; + try { + context = ldapContainer.connectAsAdmin(); + } finally { + if (context != null) { + context.close(); + } + } + } + + /** + * Check whether listObjectClasses returns anything + * + * @throws NamingException + * @throws MalformedURLException + */ + @Test + public void testListObjectClasses() throws NamingException, MalformedURLException { + DirContext context = null; + try { + context = ldapContainer.connectAsAdmin(); + + final List objectClasses = ldapContainer.listObjectClasses(); + assertThat(objectClasses.isEmpty()).isFalse(); + } finally { + if (context != null) { + context.close(); + } + } + } +} diff --git a/modules/openldap/src/test/resources/logback-test.xml b/modules/openldap/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/openldap/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/openldap/src/test/resources/objectClassDefinitions.txt b/modules/openldap/src/test/resources/objectClassDefinitions.txt new file mode 100644 index 00000000000..02bfe13cd38 --- /dev/null +++ b/modules/openldap/src/test/resources/objectClassDefinitions.txt @@ -0,0 +1,73 @@ +# list of acceptable definitions from default LDAP server configuration +( 0.9.2342.19200300.100.4.13 NAME 'domain' SUP top STRUCTURAL MUST domainComponent MAY ( associatedName $ organizationName $ description $ businessCategory $ seeAlso $ searchGuide $ userPassword $ localityName $ stateOrProvinceName $ streetAddress $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ streetAddress $ facsimileTelephoneNumber $ internationalISDNNumber $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ preferredDeliveryMethod $ destinationIndicator $ registeredAddress $ x121Address ) ) +( 0.9.2342.19200300.100.4.14 NAME 'RFC822localPart' SUP domain STRUCTURAL MAY ( commonName $ surname $ description $ seeAlso $ telephoneNumber $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ streetAddress $ facsimileTelephoneNumber $ internationalISDNNumber $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ preferredDeliveryMethod $ destinationIndicator $ registeredAddress $ x121Address ) ) +( 0.9.2342.19200300.100.4.15 NAME 'dNSDomain' SUP domain STRUCTURAL MAY ( ARecord $ MDRecord $ MXRecord $ NSRecord $ SOARecord $ CNAMERecord ) ) +( 0.9.2342.19200300.100.4.17 NAME 'domainRelatedObject' DESC 'RFC1274: an object related to an domain' SUP top AUXILIARY MUST associatedDomain ) +( 0.9.2342.19200300.100.4.18 NAME 'friendlyCountry' SUP country STRUCTURAL MUST friendlyCountryName ) +( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' DESC 'RFC1274: simple security object' SUP top AUXILIARY MUST userPassword ) +( 0.9.2342.19200300.100.4.21 NAME 'pilotDSA' SUP dsa STRUCTURAL MAY dSAQuality ) +( 0.9.2342.19200300.100.4.22 NAME 'qualityLabelledData' SUP top AUXILIARY MUST dsaQuality MAY ( subtreeMinimumQuality $ subtreeMaximumQuality ) ) +( 0.9.2342.19200300.100.4.5 NAME 'account' SUP top STRUCTURAL MUST userid MAY ( description $ seeAlso $ localityName $ organizationName $ organizationalUnitName $ host ) ) +( 0.9.2342.19200300.100.4.6 NAME 'document' SUP top STRUCTURAL MUST documentIdentifier MAY ( commonName $ description $ seeAlso $ localityName $ organizationName $ organizationalUnitName $ documentTitle $ documentVersion $ documentAuthor $ documentLocation $ documentPublisher ) ) +( 0.9.2342.19200300.100.4.7 NAME 'room' SUP top STRUCTURAL MUST commonName MAY ( roomNumber $ description $ seeAlso $ telephoneNumber ) ) +( 0.9.2342.19200300.100.4.9 NAME 'documentSeries' SUP top STRUCTURAL MUST commonName MAY ( description $ seeAlso $ telephonenumber $ localityName $ organizationName $ organizationalUnitName ) ) +( 1.3.6.1.1.1.2.0 NAME 'posixAccount' DESC 'Abstraction of an account with POSIX attributes' SUP top AUXILIARY MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( userPassword $ loginShell $ gecos $ description ) ) +( 1.3.6.1.1.1.2.10 NAME 'nisObject' DESC 'An entry in a NIS map' SUP top STRUCTURAL MUST ( cn $ nisMapEntry $ nisMapName ) MAY description ) +( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' DESC 'A device with a MAC address' SUP top AUXILIARY MAY macAddress ) +( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' DESC 'A device with boot parameters' SUP top AUXILIARY MAY ( bootFile $ bootParameter ) ) +( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' DESC 'Additional attributes for shadow passwords' SUP top AUXILIARY MUST uid MAY ( userPassword $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ description ) ) +( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction of a group of accounts' SUP top STRUCTURAL MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) ) +( 1.3.6.1.1.1.2.3 NAME 'ipService' DESC 'Abstraction an Internet Protocol service' SUP top STRUCTURAL MUST ( cn $ ipServicePort $ ipServiceProtocol ) MAY description ) +( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' DESC 'Abstraction of an IP protocol' SUP top STRUCTURAL MUST ( cn $ ipProtocolNumber $ description ) MAY description ) +( 1.3.6.1.1.1.2.5 NAME 'oncRpc' DESC 'Abstraction of an ONC/RPC binding' SUP top STRUCTURAL MUST ( cn $ oncRpcNumber $ description ) MAY description ) +( 1.3.6.1.1.1.2.6 NAME 'ipHost' DESC 'Abstraction of a host, an IP device' SUP top AUXILIARY MUST ( cn $ ipHostNumber ) MAY ( l $ description $ manager ) ) +( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' DESC 'Abstraction of an IP network' SUP top STRUCTURAL MUST ( cn $ ipNetworkNumber ) MAY ( ipNetmaskNumber $ l $ description $ manager ) ) +( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' DESC 'Abstraction of a netgroup' SUP top STRUCTURAL MUST cn MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) ) +( 1.3.6.1.1.1.2.9 NAME 'nisMap' DESC 'A generic abstraction of a NIS map' SUP top STRUCTURAL MUST nisMapName MAY description ) +( 1.3.6.1.1.3.1 NAME 'uidObject' DESC 'RFC2377: uid object' SUP top AUXILIARY MUST uid ) +( 1.3.6.1.4.1.1466.101.119.2 NAME 'dynamicObject' DESC 'RFC2589: Dynamic Object' SUP top AUXILIARY ) +( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' DESC 'RFC4512: extensible object' SUP top AUXILIARY ) +( 1.3.6.1.4.1.1466.344 NAME 'dcObject' DESC 'RFC2247: domain component object' SUP top AUXILIARY MUST dc ) +( 1.3.6.1.4.1.250.3.15 NAME 'labeledURIObject' DESC 'RFC2079: object that contains the URI attribute type' SUP top AUXILIARY MAY labeledURI ) +( 1.3.6.1.4.1.4203.1.12.2.4.0.0 NAME 'olcConfig' DESC 'OpenLDAP configuration object' SUP top ABSTRACT ) +( 1.3.6.1.4.1.4203.1.12.2.4.0.1 NAME 'olcGlobal' DESC 'OpenLDAP Global configuration options' SUP olcConfig STRUCTURAL MAY ( cn $ olcConfigFile $ olcConfigDir $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcDisallows $ olcGentleHUP $ olcIdleTimeout $ olcIndexSubstrIfMaxLen $ olcIndexSubstrIfMinLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexHash64 $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcLogFile $ olcLogLevel $ olcMaxFilterDepth $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPluginLogFile $ olcReadOnly $ olcReferral $ olcReplogFile $ olcRequires $ olcRestrict $ olcReverseLookup $ olcRootDSE $ olcSaslAuxprops $ olcSaslAuxpropsDontUseCopy $ olcSaslAuxpropsDontUseCopyIgnore $ olcSaslCBinding $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcTCPBuffer $ olcThreads $ olcThreadQueues $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSCACertificate $ olcTLSCertificate $ olcTLSCertificateKey $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSECName $ olcTLSCRLFile $ olcTLSProtocolMin $ olcToolThreads $ olcWriteTimeout $ olcObjectIdentifier $ olcAttributeTypes $ olcObjectClasses $ olcDitContentRules $ olcLdapSyntaxes ) ) +( 1.3.6.1.4.1.4203.1.12.2.4.0.2 NAME 'olcSchemaConfig' DESC 'OpenLDAP schema object' SUP olcConfig STRUCTURAL MAY ( cn $ olcObjectIdentifier $ olcLdapSyntaxes $ olcAttributeTypes $ olcObjectClasses $ olcDitContentRules ) ) +( 1.3.6.1.4.1.4203.1.12.2.4.0.3 NAME 'olcBackendConfig' DESC 'OpenLDAP Backend-specific options' SUP olcConfig STRUCTURAL MUST olcBackend ) +( 1.3.6.1.4.1.4203.1.12.2.4.0.4 NAME 'olcDatabaseConfig' DESC 'OpenLDAP Database-specific options' SUP olcConfig STRUCTURAL MUST olcDatabase MAY ( olcDisabled $ olcHidden $ olcSuffix $ olcSubordinate $ olcAccess $ olcAddContentAcl $ olcLastMod $ olcLastBind $ olcLimits $ olcMaxDerefDepth $ olcPlugin $ olcReadOnly $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplicationInterval $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDN $ olcRootPW $ olcSchemaDN $ olcSecurity $ olcSizeLimit $ olcSyncUseSubentry $ olcSyncrepl $ olcTimeLimit $ olcUpdateDN $ olcUpdateRef $ olcMultiProvider $ olcMonitoring $ olcExtraAttrs ) ) +( 1.3.6.1.4.1.4203.1.12.2.4.0.5 NAME 'olcOverlayConfig' DESC 'OpenLDAP Overlay-specific options' SUP olcConfig STRUCTURAL MUST olcOverlay MAY olcDisabled ) +( 1.3.6.1.4.1.4203.1.12.2.4.0.6 NAME 'olcIncludeFile' DESC 'OpenLDAP configuration include file' SUP olcConfig STRUCTURAL MUST olcInclude MAY ( cn $ olcRootDSE ) ) +( 1.3.6.1.4.1.4203.1.12.2.4.0.7 NAME 'olcFrontendConfig' DESC 'OpenLDAP frontend configuration' AUXILIARY MAY ( olcDefaultSearchBase $ olcPasswordHash $ olcSortVals ) ) +( 1.3.6.1.4.1.4203.1.12.2.4.0.8 NAME 'olcModuleList' DESC 'OpenLDAP dynamic module info' SUP olcConfig STRUCTURAL MAY ( cn $ olcModulePath $ olcModuleLoad ) ) +( 1.3.6.1.4.1.4203.1.12.2.4.1.12.1 NAME 'olcMdbBkConfig' DESC 'MDB backend configuration' SUP olcBackendConfig STRUCTURAL MAY olcBkMdbIdlExp ) +( 1.3.6.1.4.1.4203.1.12.2.4.2.12.1 NAME 'olcMdbConfig' DESC 'MDB database configuration' SUP olcDatabaseConfig STRUCTURAL MUST olcDbDirectory MAY ( olcDbCheckpoint $ olcDbEnvFlags $ olcDbNoSync $ olcDbIndex $ olcDbMaxReaders $ olcDbMaxSize $ olcDbMode $ olcDbSearchStack $ olcDbMaxEntrySize $ olcDbRtxnSize $ olcDbMultival ) ) +( 1.3.6.1.4.1.4203.1.12.2.4.2.2.1 NAME 'olcLdifConfig' DESC 'LDIF backend configuration' SUP olcDatabaseConfig STRUCTURAL MUST olcDbDirectory ) +( 1.3.6.1.4.1.4203.1.12.2.4.2.4.1 NAME 'olcMonitorConfig' DESC 'Monitor backend configuration' SUP olcDatabaseConfig STRUCTURAL ) +( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' DESC 'RFC2798: Internet Organizational Person' SUP organizationalPerson STRUCTURAL MAY ( audio $ businessCategory $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ givenName $ homePhone $ homePostalAddress $ initials $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $ roomNumber $ secretary $ uid $ userCertificate $ x500uniqueIdentifier $ preferredLanguage $ userSMIMECertificate $ userPKCS12 ) ) +( 2.16.840.1.113730.3.2.6 NAME 'referral' DESC 'namedref: named subordinate referral' SUP top STRUCTURAL MUST ref ) +( 2.5.17.0 NAME 'subentry' DESC 'RFC3672: subentry' SUP top STRUCTURAL MUST ( cn $ subtreeSpecification ) ) +( 2.5.20.1 NAME 'subschema' DESC 'RFC4512: controlling subschema (sub)entry' AUXILIARY MAY ( dITStructureRules $ nameForms $ dITContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) ) +( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass ) +( 2.5.6.10 NAME 'residentialPerson' DESC 'RFC2256: an residential person' SUP person STRUCTURAL MUST l MAY ( businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l ) ) +( 2.5.6.11 NAME 'applicationProcess' DESC 'RFC2256: an application process' SUP top STRUCTURAL MUST cn MAY ( seeAlso $ ou $ l $ description ) ) +( 2.5.6.12 NAME 'applicationEntity' DESC 'RFC2256: an application entity' SUP top STRUCTURAL MUST ( presentationAddress $ cn ) MAY ( supportedApplicationContext $ seeAlso $ ou $ o $ l $ description ) ) +( 2.5.6.13 NAME 'dSA' DESC 'RFC2256: a directory system agent (a server)' SUP applicationEntity STRUCTURAL MAY knowledgeInformation ) +( 2.5.6.14 NAME 'device' DESC 'RFC2256: a device' SUP top STRUCTURAL MUST cn MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $ description ) ) +( 2.5.6.15 NAME 'strongAuthenticationUser' DESC 'RFC2256: a strong authentication user' SUP top AUXILIARY MUST userCertificate ) +( 2.5.6.16.2 NAME 'certificationAuthority-V2' SUP certificationAuthority AUXILIARY MAY deltaRevocationList ) +( 2.5.6.16 NAME 'certificationAuthority' DESC 'RFC2256: a certificate authority' SUP top AUXILIARY MUST ( authorityRevocationList $ certificateRevocationList $ cACertificate ) MAY crossCertificatePair ) +( 2.5.6.17 NAME 'groupOfUniqueNames' DESC 'RFC2256: a group of unique names (DN and Unique Identifier)' SUP top STRUCTURAL MUST ( uniqueMember $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) ) +( 2.5.6.18 NAME 'userSecurityInformation' DESC 'RFC2256: a user security information' SUP top AUXILIARY MAY supportedAlgorithms ) +( 2.5.6.19 NAME 'cRLDistributionPoint' SUP top STRUCTURAL MUST cn MAY ( certificateRevocationList $ authorityRevocationList $ deltaRevocationList ) ) +( 2.5.6.1 NAME 'alias' DESC 'RFC4512: an alias' SUP top STRUCTURAL MUST aliasedObjectName ) +( 2.5.6.20 NAME 'dmd' SUP top STRUCTURAL MUST dmdName MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) ) +( 2.5.6.21 NAME 'pkiUser' DESC 'RFC2587: a PKI user' SUP top AUXILIARY MAY userCertificate ) +( 2.5.6.22 NAME 'pkiCA' DESC 'RFC2587: PKI certificate authority' SUP top AUXILIARY MAY ( authorityRevocationList $ certificateRevocationList $ cACertificate $ crossCertificatePair ) ) +( 2.5.6.23 NAME 'deltaCRL' DESC 'RFC4523: X.509 delta CRL' SUP top AUXILIARY MAY deltaRevocationList ) +( 2.5.6.2 NAME 'country' DESC 'RFC2256: a country' SUP top STRUCTURAL MUST c MAY ( searchGuide $ description ) ) +( 2.5.6.3 NAME 'locality' DESC 'RFC2256: a locality' SUP top STRUCTURAL MAY ( street $ seeAlso $ searchGuide $ st $ l $ description ) ) +( 2.5.6.4 NAME 'organization' DESC 'RFC2256: an organization' SUP top STRUCTURAL MUST o MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) ) +( 2.5.6.5 NAME 'organizationalUnit' DESC 'RFC2256: an organizational unit' SUP top STRUCTURAL MUST ou MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) ) +( 2.5.6.6 NAME 'person' DESC 'RFC2256: a person' SUP top STRUCTURAL MUST ( sn $ cn ) MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) ) +( 2.5.6.7 NAME 'organizationalPerson' DESC 'RFC2256: an organizational person' SUP person STRUCTURAL MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) ) +( 2.5.6.8 NAME 'organizationalRole' DESC 'RFC2256: an organizational role' SUP top STRUCTURAL MUST cn MAY ( x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ seeAlso $ roleOccupant $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l $ description ) ) +( 2.5.6.9 NAME 'groupOfNames' DESC 'RFC2256: a group of names (DNs)' SUP top STRUCTURAL MUST ( member $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )