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 ) )