Updated auth module as per the interop requirements of the with AD server

- Authentication is performed in 4 step process
1. Create a context (A) using bind credentials
2. Search userDN with the context (A) and username
3. Rebind or create a new context (B) using userDN and password
4. Search groups with context (A) and userDN
- Added informative javadocs
- Updated the configuration to fall in line with the requirements
- Moved the Test LDAP server to common test code
- Updated the LdapAuthenticator Spock tests to use TestLdapServer
- Save DeployDBConfigurtion object in the DeployDB app
- Added manualPromo.yml and advServ.yml for testing manual promotion

References #164
This commit is contained in:
Mahesh V Kelkar 2015-04-29 17:41:32 -04:00
parent 09ecbf0e24
commit 05e1541e13
26 changed files with 521 additions and 269 deletions

View File

@ -66,9 +66,6 @@ dependencies {
cucumberCompile 'info.cukes:cucumber-groovy:1.2.+'
cucumberCompile project(':dropwizard-integtest')
/* Include unboundid sdk for in-memory ldap test server */
cucumberCompile 'com.unboundid:unboundid-ldapsdk:2.3.8'
codenarc(
"org.codenarc:CodeNarc:0.22",
"org.codehaus.groovy:groovy-all:2.4.0+"

View File

@ -27,11 +27,14 @@ logging:
ldap:
uri: ldap://localhost:10389
cachePolicy: maximumSize=10000, expireAfterWrite=10m
userFilter: ou=people,dc=yourcompany,dc=com
groupFilter: ou=groups,dc=yourcompany,dc=com
userNameAttribute: cn
groupNameAttribute: cn
groupMembershipAttribute: memberUid
groupClassName: groupOfUniqueNames
baseDC: "dc=example,dc=com"
bindDN: "cn=admin"
bindPassword: "secret"
userNamePrefix: cn
userObjectClass: posixUser
groupNamePrefix: cn
groupMembershipPrefix: memberUid
groupObjectClass: posixGroup
distinguishedNamePrefix: entryDN
connectTimeout: 500ms
readTimeout: 500ms

View File

@ -22,4 +22,19 @@ logging:
currentLogFilename: ./logs/deploydb-spock.log
threshold: ALL
archive: false
timeZone: UTC
timeZone: UTC
ldap:
uri: ldap://localhost:10389
cachePolicy: maximumSize=10000, expireAfterWrite=10m
baseDC: "dc=example,dc=com"
bindDN: "cn=admin"
bindPassword: "secret"
userNamePrefix: cn
userObjectClass: posixUser
groupNamePrefix: cn
groupMembershipPrefix: memberUid
groupObjectClass: posixGroup
distinguishedNamePrefix: entryDN
connectTimeout: 500ms
readTimeout: 500ms

View File

@ -556,14 +556,18 @@ over the admin port.</p>
<span class="key">uri</span>: <span class="string"><span class="content">ldap://server.example.com:10389</span></span>
<span class="comment"># Cache 10000 credentials for at least 10 minutes</span>
<span class="key">cachePolicy</span>: <span class="string"><span class="content">maximumSize=10000, expireAfterWrite=10m</span></span>
<span class="key">userFilter</span>: <span class="string"><span class="content">ou=people,dc=yourcompany,dc=com</span></span>
<span class="key">groupFilter</span>: <span class="string"><span class="content">ou=groups,dc=yourcompany,dc=com</span></span>
<span class="key">userNameAttribute</span>: <span class="string"><span class="content">cn</span></span>
<span class="key">groupNameAttribute</span>: <span class="string"><span class="content">cn</span></span>
<span class="key">baseDC</span>: <span class="string"><span class="delimiter">&quot;</span><span class="content">dc=example,dc=com</span><span class="delimiter">&quot;</span></span>
<span class="key">bindDN</span>: <span class="string"><span class="delimiter">&quot;</span><span class="content">cn=admin</span><span class="delimiter">&quot;</span></span>
<span class="key">bindPassword</span>: <span class="string"><span class="delimiter">&quot;</span><span class="content">secret</span><span class="delimiter">&quot;</span></span>
<span class="key">userNamePrefix</span>: <span class="string"><span class="content">cn</span></span>
<span class="comment"># Filter groups by ObjectClass associated with user</span>
<span class="key">userObjectClass</span>: <span class="string"><span class="content">posixUser</span></span>
<span class="key">groupNamePrefix</span>: <span class="string"><span class="content">cn</span></span>
<span class="comment"># Attribute that defines the association</span>
<span class="key">groupMembershipAttribute</span>: <span class="string"><span class="content">memberUid</span></span>
<span class="key">groupMembershipPrefix</span>: <span class="string"><span class="content">memberUid</span></span>
<span class="comment"># Filter groups by ObjectClass associated with group</span>
<span class="key">groupClassName</span>: <span class="string"><span class="content">posixGroup</span></span>
<span class="key">groupObjectClass</span>: <span class="string"><span class="content">posixGroup</span></span>
<span class="key">distinguishedNamePrefix</span>: <span class="string"><span class="content">dn</span></span>
<span class="key">connectTimeout</span>: <span class="string"><span class="content">500ms</span></span>
<span class="key">readTimeout</span>: <span class="string"><span class="content">500ms</span></span></code></pre>
</div>
@ -589,7 +593,7 @@ in the launch config for end-to-end security</p>
</div>
<div id="footer">
<div id="footer-text">
Last updated 2015-04-15 09:21:30 EDT
Last updated 2015-04-29 17:11:46 EDT
</div>
</div>
</body>

View File

@ -30,6 +30,9 @@ dependencies {
compile ('io.dropwizard.modules:dropwizard-flyway:0.7.0-1')
compile ('joda-time:joda-time:2.6+')
/* Include unboundid sdk for in-memory ldap test server */
compile 'com.unboundid:unboundid-ldapsdk:2.3.8'
}

View File

@ -141,6 +141,6 @@ public class StubAppRunner<C extends Configuration> {
}
void setConfigDirectory(String configDirectory) {
application.configDirectory = configDirectory
application.configuration.configDirectory = configDirectory
}
}

View File

@ -1,4 +1,4 @@
package deploydb.cucumber
package dropwizardintegtest
import com.unboundid.ldap.listener.InMemoryDirectoryServer
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig
@ -6,7 +6,7 @@ import com.unboundid.ldap.listener.InMemoryListenerConfig
import com.unboundid.ldap.sdk.Attribute
import com.unboundid.ldap.sdk.DN
import com.unboundid.ldap.sdk.Entry
import com.unboundid.ldap.sdk.LDAPException
import com.unboundid.ldap.sdk.OperationType
import com.unboundid.ldif.LDIFReader
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@ -20,13 +20,13 @@ final class TestLdapServer {
private InMemoryDirectoryServer server = null
/**
* The root DN of the LDAP directory.
* The base/root DN of the LDAP directory.
*/
private String root = "dc=yourcompany,dc=com"
private String baseDN = "dc=example,dc=com"
/**
* The distinguished name of the admin account.
*/
private String authDn = "uid=admin,ou=system"
private String adminCN = "cn=admin"
/**
* The password for the admin account.
*/
@ -43,21 +43,22 @@ final class TestLdapServer {
private static final Logger logger = LoggerFactory.getLogger(TestLdapServer.class)
/**
* Configure and start the embedded UnboundID server creating the root DN and loading the LDIF seed data.
* Configure and start the embedded UnboundID server creating the base DN and loading the LDIF seed data.
*/
void start() {
try {
logger.info("Starting UnboundID server")
final InMemoryListenerConfig listenerConfig =
InMemoryListenerConfig.createLDAPConfig("testLdapListener", serverPort)
final InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(new DN(root))
final InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(new DN(baseDN))
config.setListenerConfigs(listenerConfig)
config.setSchema(null)
if (authDn != null) {
config.addAdditionalBindCredentials(authDn, passwd)
if (adminCN != null) {
config.addAdditionalBindCredentials(adminCN, passwd)
}
config.setAuthenticationRequiredOperationTypes(OperationType.BIND)
server = new InMemoryDirectoryServer(config)
server.add(new Entry(root, new Attribute("objectclass", "domain", "top")))
server.add(new Entry(baseDN, new Attribute("objectclass", "domain", "top")))
if (ldifFile != null) {
final InputStream inputStream = new FileInputStream(ldifFile)
try {
@ -69,11 +70,7 @@ final class TestLdapServer {
}
server.startListening()
logger.info("Started UnboundID server")
} catch (final LDAPException e) {
e.printStackTrace()
logger.error("Could not launch embedded UnboundID directory server", e)
} catch (final IOException e) {
e.printStackTrace()
} catch (final Exception e) {
logger.error("Could not launch embedded UnboundID directory server", e)
}
}
@ -82,8 +79,10 @@ final class TestLdapServer {
* Shutdown the the embedded UnboundID server.
*/
void stop() {
logger.info("Stopping UnboundID server")
server.shutDown(true)
if (server) {
logger.info("Stopping UnboundID server")
server.shutDown(true)
}
logger.info("Stopped UnboundID server")
}
}

View File

@ -0,0 +1,4 @@
type: deploydb.models.promotion.ManualLDAPPromotionImpl
description: "Manual LDAP Promotion"
attributes:
allowedGroup: GRP-AWS-Dev-Eng

View File

@ -0,0 +1,8 @@
description: "Advanced Service"
artifacts:
- adv.group.1:bg1
- adv.group.2:bg2
pipelines:
- basicPipe
promotions:
- manualPromo

View File

@ -25,14 +25,18 @@ ldap:
uri: ldap://server.example.com:10389
# Cache 10000 credentials for at least 10 minutes
cachePolicy: maximumSize=10000, expireAfterWrite=10m
userFilter: ou=people,dc=yourcompany,dc=com
groupFilter: ou=groups,dc=yourcompany,dc=com
userNameAttribute: cn
groupNameAttribute: cn
baseDC: "dc=example,dc=com"
bindDN: "cn=admin"
bindPassword: "secret"
userNamePrefix: cn
# Filter groups by ObjectClass associated with user
userObjectClass: posixUser
groupNamePrefix: cn
# Attribute that defines the association
groupMembershipAttribute: memberUid
groupMembershipPrefix: memberUid
# Filter groups by ObjectClass associated with group
groupClassName: posixGroup
groupObjectClass: posixGroup
distinguishedNamePrefix: dn
connectTimeout: 500ms
readTimeout: 500ms
----

View File

@ -20,6 +20,7 @@ import org.hibernate.Session
import org.hibernate.SessionFactory
import org.hibernate.context.internal.ManagedSessionContext
import dropwizardintegtest.StubAppRunner
import dropwizardintegtest.TestLdapServer
import dropwizardintegtest.WebhookTestServerAppRunner
import dropwizardintegtest.webhookTestServerApp
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature

View File

@ -37,7 +37,7 @@ class DeployDBApp extends Application<DeployDBConfiguration> {
private WebhookManager webhooksManager
private WorkFlow workFlow
private provider.V1TypeProvider typeProvider
private String configDirectory
private DeployDBConfiguration configuration
private String configChecksum
static void main(String[] args) throws Exception {
@ -130,12 +130,12 @@ class DeployDBApp extends Application<DeployDBConfiguration> {
/** DeployDB is up and running */
@Override
public void run(DeployDBConfiguration configuration,
public void run(DeployDBConfiguration deployDBConfiguration,
Environment environment) {
/*
* Create webhook manager based on configuration
*/
webhooksManager = new WebhookManager(configuration)
webhooksManager = new WebhookManager(deployDBConfiguration)
/**
* Initialize the workflow object
@ -146,7 +146,7 @@ class DeployDBApp extends Application<DeployDBConfiguration> {
/**
* Load configuration models
*/
this.configDirectory = configuration.configDirectory
this.configuration = deployDBConfiguration
loadModelConfiguration()
/**
@ -166,8 +166,8 @@ class DeployDBApp extends Application<DeployDBConfiguration> {
/** Register Ldap Authentication */
CachingAuthenticator<BasicCredentials, auth.User> authenticator = new CachingAuthenticator<>(
environment.metrics(),
new auth.LdapAuthenticator(configuration.ldapConfiguration),
configuration.ldapConfiguration.cachePolicy)
new auth.LdapAuthenticator(this.configuration.ldapConfiguration),
this.configuration.ldapConfiguration.cachePolicy)
environment.jersey().register(
AuthFactory.binder(new BasicAuthFactory<auth.User>(authenticator,
"Please enter the user credentials",
@ -213,7 +213,7 @@ class DeployDBApp extends Application<DeployDBConfiguration> {
workFlow.loadConfigModels()
} catch (Exception e) {
logger.error("failed to read config from directory: " +
"${configDirectory} with an exception: ", e)
"${this.configuration.configDirectory} with an exception: ", e)
throw e
}
}

View File

@ -134,7 +134,7 @@ class WorkFlow {
void loadConfigModels() {
/** Validate base config directory */
File baseConfigDirectory = new File(this.deployDBApp.configDirectory)
File baseConfigDirectory = new File(this.deployDBApp.configuration.configDirectory)
if (!baseConfigDirectory.exists() || !baseConfigDirectory.isDirectory()) {
throw new Exception("No DeployDB configuration found. DeployDB would not function properly")
}
@ -159,7 +159,7 @@ class WorkFlow {
List<models.ModelConfig> modelConfigList = []
/* Load promotions */
String promotionsDirName = this.deployDBApp.configDirectory + "/promotions"
String promotionsDirName = this.deployDBApp.configuration.configDirectory + "/promotions"
loadConfigModelsCommon(promotionsDirName, ModelType.PROMOTION,
tmpPromotionRegistry, this.promotionLoader,
inputStreams, modelConfigList) { models.promotion.Promotion promotion ->
@ -167,7 +167,7 @@ class WorkFlow {
}
/* Load environments */
String environmentsDirName = this.deployDBApp.configDirectory + "/environments"
String environmentsDirName = this.deployDBApp.configuration.configDirectory + "/environments"
loadConfigModelsCommon(environmentsDirName, ModelType.ENVIRONMENT,
tmpEnvironmentRegistry, this.environmentLoader,
inputStreams, modelConfigList) { models.Environment environment ->
@ -175,7 +175,7 @@ class WorkFlow {
}
/* Load pipelines */
String pipelinesDirName = this.deployDBApp.configDirectory + "/pipelines"
String pipelinesDirName = this.deployDBApp.configuration.configDirectory + "/pipelines"
loadConfigModelsCommon(pipelinesDirName, ModelType.PIPELINE,
tmpPipelineRegistry, this.pipelineLoader,
inputStreams, modelConfigList) { models.pipeline.Pipeline pipeline ->
@ -203,7 +203,7 @@ class WorkFlow {
}
/* Load services */
String servicesDirName = this.deployDBApp.configDirectory + "/services"
String servicesDirName = this.deployDBApp.configuration.configDirectory + "/services"
loadConfigModelsCommon(servicesDirName, ModelType.SERVICE,
tmpServiceRegistry, this.serviceLoader,
inputStreams, modelConfigList) { models.Service service ->
@ -228,7 +228,7 @@ class WorkFlow {
}
/* Load webhook */
String webhookDirName = this.deployDBApp.configDirectory + "/webhook"
String webhookDirName = this.deployDBApp.configuration.configDirectory + "/webhook"
try {
loadConfigModelsCommon(webhookDirName, ModelType.WEBHOOK,
null, this.webhookLoader,

View File

@ -5,6 +5,7 @@ import groovy.transform.TypeChecked
import io.dropwizard.auth.Authenticator
import io.dropwizard.auth.basic.BasicCredentials
import javax.naming.Context
import javax.naming.AuthenticationException
import javax.naming.NamingEnumeration
import javax.naming.NamingException
import javax.naming.directory.InitialDirContext
@ -27,6 +28,17 @@ class LdapAuthenticator implements Authenticator<BasicCredentials, BasicCredenti
*/
static final String contextFactoryClassName = "com.sun.jndi.ldap.LdapCtxFactory"
/**
* Handling the referral:
*
* When you search in AD, if AD thinks there are more information
* available in another place, it returns a referral (place to find more info)
* along with your search results. You could avoid this exception by setting
* Context.REFERRAL to follow. Then it would search in the referral also
* (That's why it takes more time to return result).
*/
static final String referralAction = "follow"
/**
* The constant holds the name of property for specifying connect timeout
*/
@ -60,12 +72,11 @@ class LdapAuthenticator implements Authenticator<BasicCredentials, BasicCredenti
* @return Directory context
* @throws NamingException if naming exception is encountered by underlying JNDI
* @throws ConnectException if uri is invalid
* @throws AuthenticationException is bind credentials are invalid
*/
protected InitialDirContext buildContext(BasicCredentials credentials)
throws NamingException, ConnectException {
protected InitialDirContext bindContext()
throws NamingException, ConnectException, AuthenticationException {
final String userDN = String.format("%s=%s,%s", configuration.userNameAttribute,
credentials.username, configuration.userFilter)
final Hashtable<String, String> env = new Hashtable<>()
/**
@ -84,6 +95,11 @@ class LdapAuthenticator implements Authenticator<BasicCredentials, BasicCredenti
*/
env.put(Context.PROVIDER_URL, configuration.uri.toString())
/**
* Set referral action (See above for details)
*/
env.put(Context.REFERRAL, referralAction)
/**
* If cannot establish a connection within a certain timeout period,
* it aborts the connection attempt.
@ -115,14 +131,62 @@ class LdapAuthenticator implements Authenticator<BasicCredentials, BasicCredenti
*/
env.put(connectPoolTimeoutName, connectPoolTimeoutValue)
env.put("javax.net.ssl.trustStore", "/Users/mkelkar/deploydb/kelkarkeystore")
/** User specific attributes */
env.put(Context.SECURITY_PRINCIPAL, userDN)
env.put(Context.SECURITY_CREDENTIALS, credentials.password)
env.put(Context.SECURITY_AUTHENTICATION, "simple")
env.put(Context.SECURITY_PRINCIPAL, configuration.bindDN)
env.put(Context.SECURITY_CREDENTIALS, configuration.bindPassword)
/** Create a context instance and initiate a connection to LDAP server */
return new InitialDirContext(env)
}
/**
* Search context for this criteria and return the sanitized attribute values
* for the attributeName
* @param context
* @param name
* @param filter
* @param attributeName
* @return
* @throws NamingException
*/
protected Set<String> searchContext(InitialDirContext context, String name,
String filter, String attributeName)
throws NamingException {
/**
* Optimize the output search results to single attribute only
*/
SearchControls searchCtls = new SearchControls()
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE)
String[] returnedAtts = [attributeName] as String[]
searchCtls.setReturningAttributes(returnedAtts)
/** Search for attribute with name, filter & controls */
final NamingEnumeration<SearchResult> results = context.search(
name, filter, searchCtls)
/** Walk and prepare the response */
Set<String> attribValues = new HashSet<>()
try {
while (results.hasMore()) {
SearchResult current = results.next()
/** Get the specified attribute's value */
if (current.getAttributes() != null &&
current.getAttributes().get(attributeName) != null) {
String attribute = (String) current.getAttributes().get(attributeName).get(0)
attribValues.add(attribute)
}
}
} finally {
results.close()
}
return attribValues
}
/**
* Search authorized groups for this user
*
@ -133,48 +197,83 @@ class LdapAuthenticator implements Authenticator<BasicCredentials, BasicCredenti
* @param username
* @return Set of groupNames
*/
protected Set<String> getGroupMemberships(InitialDirContext context, String username)
protected Set<String> getGroupMemberships(InitialDirContext context, String userDN)
throws NamingException {
/**
* Filter the user groups for configured groupClass (aka objectClass)
*
* Details about filter format can be found here:
* <http://docstore.mik.ua/orelly/java-ent/jenut/ch06_12.htm>
*
* E.g. (&(memberUid=myusername)(objectClass=groupOfUniqueNames))
* - Filter the groups where myusername is member AND objectClass is groupOfUniqueNames
* We are searching from the top i.e. baseDC; filter for the groups that username
* belong to and has the given group's ObjectClass
*/
final String filter = String.format("(&(%s=%s)(objectClass=%s))",
configuration.groupMembershipAttribute, username,
configuration.groupClassName)
configuration.groupMembershipPrefix, userDN,
configuration.groupObjectClass)
/**
* Optimize the output search results to single groupNameAttribute only
*/
SearchControls searchCtls = new SearchControls()
String[] returnedAtts = [configuration.groupNameAttribute] as String[]
searchCtls.setReturningAttributes(returnedAtts)
/** Search from baseDC */
return searchContext(context, configuration.baseDC, filter, configuration.groupNamePrefix)
}
/** Search for group with groupFilter string, filter & controls */
final NamingEnumeration<SearchResult> results = context.search(
configuration.groupFilter, filter, searchCtls)
/**
* Get user attributes and match the password
*
* The configuration attributes used in here are guaranteed to be non-null by annotation.
* If those parameters are mis-configured, then search query would fail
/** Walk and prepare the response */
Set<String> groups = new HashSet<>()
* @param context
* @param credentials
* @return true on success
* @throws NamingException if naming exception is encountered by underlying JNDI
* @throws AuthenticationException is user credentials are invalid
*/
protected String authenticateUser(InitialDirContext context, BasicCredentials credentials)
throws NamingException, AuthenticationException {
InitialDirContext userContext = null
try {
while (results.hasMore()) {
SearchResult current = results.next()
if (current.getAttributes() != null &&
current.getAttributes().get(configuration.groupNameAttribute) != null) {
String group = (String) current.getAttributes().get(configuration.groupNameAttribute).get(0)
groups.add(group)
}
/**
* Find User DN
*
* In order to authenticate, we should bind (again) to AD with credentials. But,
* to do so we need fully qualified user distinguished name (DN). We cannot
* construct it based on available information.
*/
/**
* We are searching from the top i.e. baseDC; filter the output using username and
* ObjectClass that user belong to
*/
final String filter = String.format("(&(%s=%s)(objectClass=%s))",
configuration.userNamePrefix, credentials.username,
configuration.userObjectClass)
/** Search from baseDC */
Set<String> distinguishedNames = searchContext(context, configuration.baseDC,
filter, configuration.distinguishedNamePrefix)
/**
* The search should yield us 1 user DN. If we received anything but that, then
* raise an exception
*/
if (distinguishedNames.size() != 1) {
throw new Exception("failed to find User DN for ${credentials.username}")
}
String userDN = distinguishedNames[0]
/* Using environment attributes from the existing context and authenticate the user */
Hashtable env = context.getEnvironment()
Hashtable environment = (Hashtable)env.clone()
environment.put(Context.SECURITY_AUTHENTICATION, "simple")
environment.put(Context.SECURITY_PRINCIPAL, userDN)
environment.put(Context.SECURITY_CREDENTIALS, credentials.password)
userContext = new InitialDirContext(environment)
/** Return authenticated user distinguished name */
return userDN
} finally {
results.close()
if (userContext) {
userContext.close()
}
}
return groups
}
/**
@ -183,8 +282,13 @@ class LdapAuthenticator implements Authenticator<BasicCredentials, BasicCredenti
* If there are no groups, user is still considered as authenticated,
* but is not associated with any groups.
*
* Logs all the authentication or naming exceptions and return authentication
* failure
* Logs all the authentication or naming exceptions and returns failure
*
* Authentication is a 4 step process
* 1. Create a context (A) using bind credentials
* 2. Search userDN with the context (A) and username
* 3. Rebind or create a new context (B) using userDN and password
* 4. Search groups with context (A) and userDN
*
* @param credentials
* @return User - Optional User class
@ -193,11 +297,15 @@ class LdapAuthenticator implements Authenticator<BasicCredentials, BasicCredenti
Optional<User> authenticate(BasicCredentials credentials) {
InitialDirContext context = null
try {
/** Bind */
context = bindContext()
/** Authenticate */
context = buildContext(credentials)
String userDN = authenticateUser(context, credentials)
/** Get a list of groups that this user is authorized for */
Set<String> groupMemberships = getGroupMemberships(context, credentials.username)
Set<String> groupMemberships = getGroupMemberships(context, userDN)
return Optional.of(new User(credentials.username, groupMemberships))
} catch (Exception err) {

View File

@ -21,7 +21,7 @@ class LdapConfiguration {
/**
* Uri to LDAP Server
*
* - Valid format: ldap://<hostname>:<optional port number>
* - Valid format: ldap[s]://<hostname>:<optional port number>
* - "uri" can be null only if we are using default setting. In that case
* no authentication is performed.
* - if "uri" is empty or invalid, then deploydb will attempt to connect and
@ -35,29 +35,92 @@ class LdapConfiguration {
@JsonProperty
private CacheBuilderSpec cachePolicy = CacheBuilderSpec.disableCaching()
/**
* Base domain component
*
* Used as a base name in the LDAP search query
*/
@NotEmpty
@JsonProperty
private String userFilter = "ou=people,dc=example,dc=com"
private String baseDC = "dc=example,dc=com"
/** Bind Username */
@NotEmpty
@JsonProperty
private String groupFilter = "ou=groups,dc=example,dc=com"
private String bindDN = "cn=admin"
/** Bind password */
@NotEmpty
@JsonProperty
private String userNameAttribute = "cn"
private String bindPassword = "secret"
/**
* User common name prefix
*
* Used in LDAP search query to:
* - filter matches e.g. (&(cn=username)(objectClass=posixUser))
* - control returned attributes in the matched entry
*/
@NotEmpty
@JsonProperty
private String groupNameAttribute = "cn"
private String userNamePrefix = "cn"
/**
* User object class name
*
* This is used in the query to search user data record in AD
*
* An objectClass is a collection of attributes (or an attribute container).
* More info can be found at <http://www.zytrax.com/books/ldap/ch3/#objectclasses>
*/
@NotEmpty
@JsonProperty
private String groupMembershipAttribute = "memberUid"
private String userObjectClass = "posixUser"
/**
* Group common name prefix
*
* Used in LDAP search query to:
* - control returned attributes in the matched entry
*/
@NotEmpty
@JsonProperty
private String groupClassName = "posixGroup"
private String groupNamePrefix = "cn"
/**
* Group membership prefix
*
* Note that attribute value for this prefix is assumed to userDN (distinguished name)
* E.g. cn=username,OU=users,DC=example,DC=com
*
* Used in LDAP search query to:
* - filter matches e.g. (&(memberUid=cn=username,OU=users,DC=example,DC=com)(objectClass=posixGroup))
*/
@NotEmpty
@JsonProperty
private String groupMembershipPrefix = "memberUid"
/**
* Group object class name
*
* This is used in the query to search group data record in AD
*
* An objectClass is a collection of attributes (or an attribute container).
* More info can be found at <http://www.zytrax.com/books/ldap/ch3/#objectclasses>
*/
@NotEmpty
@JsonProperty
private String groupObjectClass = "posixGroup"
/**
* Domain name prefix
*
* Used in LDAP search query to:
* - controls returned attributes in the matched entry
*/
@NotEmpty
@JsonProperty
private String distinguishedNamePrefix = "dn"
@Valid
@JsonProperty

View File

@ -46,10 +46,10 @@ class Promotion {
boolean isType() {
try {
PromotionImpl impl = getPromotionImpl()
return impl instanceof PromotionImpl
} catch (all) {
return false
}
return true
}
/**

View File

@ -12,7 +12,7 @@ class DeploymentCompletedNotificationsSpec extends Specification {
mcfgHelper.setup()
integAppHelper.startAppWithConfiguration('deploydb.spock.yml')
integAppHelper.startWebhookTestServerWithConfiguration('webhookTestServer.example.yml')
integAppHelper.runner.getApplication().configDirectory = mcfgHelper.baseCfgDirName
integAppHelper.runner.getApplication().configuration.configDirectory = mcfgHelper.baseCfgDirName
integAppHelper.webhookRunner.requestWebhookObject.contentTypeParam =
"application/vnd.deploydb.deploymentcompleted.v1+json"
}

View File

@ -14,7 +14,7 @@ class DeploymentCreatedNotificationsSpec extends Specification {
integAppHelper.startWebhookTestServerWithConfiguration('webhookTestServer.example.yml')
integAppHelper.webhookRunner.requestWebhookObject.contentTypeParam =
"application/vnd.deploydb.deploymentcreated.v1+json"
integAppHelper.runner.getApplication().configDirectory = mcfgHelper.baseCfgDirName
integAppHelper.runner.getApplication().configuration.configDirectory = mcfgHelper.baseCfgDirName
}
def cleanup() {

View File

@ -12,7 +12,7 @@ class DeploymentStartedNotificationsSpec extends Specification {
mcfgHelper.setup()
integAppHelper.startAppWithConfiguration('deploydb.spock.yml')
integAppHelper.startWebhookTestServerWithConfiguration('webhookTestServer.example.yml')
integAppHelper.runner.getApplication().configDirectory = mcfgHelper.baseCfgDirName
integAppHelper.runner.getApplication().configuration.configDirectory = mcfgHelper.baseCfgDirName
integAppHelper.webhookRunner.requestWebhookObject.contentTypeParam =
"application/vnd.deploydb.deploymentstarted.v1+json"
}

View File

@ -16,6 +16,7 @@ import org.hibernate.Session
import org.hibernate.SessionFactory
import org.hibernate.context.internal.ManagedSessionContext
import dropwizardintegtest.StubAppRunner
import dropwizardintegtest.TestLdapServer
import dropwizardintegtest.WebhookTestServerAppRunner
import dropwizardintegtest.webhookTestServerApp
@ -24,6 +25,7 @@ class IntegrationTestAppHelper {
private StubAppRunner runner = null
private Client jerseyClient = null
private WebhookTestServerAppRunner webhookRunner = null
private TestLdapServer testLdapServer = null
SessionFactory getSessionFactory() {
return this.runner.sessionFactory
@ -135,6 +137,8 @@ class IntegrationTestAppHelper {
println("start application with config ${config}")
this.runner = new StubAppRunner(DeployDBApp.class, config)
this.runner.start()
this.testLdapServer = new TestLdapServer()
this.testLdapServer.start()
}
@ -142,6 +146,9 @@ class IntegrationTestAppHelper {
if (this.runner != null) {
this.runner.stop()
}
if (this.testLdapServer != null) {
this.testLdapServer.stop()
}
}
void startWebhookTestServerWithConfiguration(String config) {

View File

@ -12,7 +12,7 @@ class MultipleEnvironmentsNotificationsSpec extends Specification {
mcfgHelper.setup()
integAppHelper.startAppWithConfiguration('deploydb.spock.yml')
integAppHelper.startWebhookTestServerWithConfiguration('webhookTestServer.example.yml')
integAppHelper.runner.getApplication().configDirectory = mcfgHelper.baseCfgDirName
integAppHelper.runner.getApplication().configuration.configDirectory = mcfgHelper.baseCfgDirName
integAppHelper.webhookRunner.requestWebhookObject.contentTypeParam =
"application/vnd.deploydb.deploymentcreated.v1+json"
integAppHelper.startWebhookTestServerWithConfiguration('webhookTestServer.example.yml')

View File

@ -13,7 +13,7 @@ class PromotionCompletedNotificationsSpec extends Specification {
integAppHelper.startAppWithConfiguration('deploydb.spock.yml')
integAppHelper.startWebhookTestServerWithConfiguration('webhookTestServer.example.yml')
// load up the configuration
integAppHelper.runner.getApplication().configDirectory = mcfgHelper.baseCfgDirName
integAppHelper.runner.getApplication().configuration.configDirectory = mcfgHelper.baseCfgDirName
integAppHelper.webhookRunner.requestWebhookObject.contentTypeParam =
"application/vnd.deploydb.promotioncompleted.v1+json"

View File

@ -17,16 +17,18 @@ class WorkFlowSpec extends Specification {
}
}
class workFlowWithArgsSpec extends Specification {
class WorkFlowWithArgsSpec extends Specification {
private ModelConfigHelper modelConfigHelper = new ModelConfigHelper()
private DeployDBApp app = new DeployDBApp()
private DeployDBConfiguration deployDBConfiguration = new DeployDBConfiguration()
private WorkFlow workFlow
private FlowDAO fdao = Mock(FlowDAO)
private ModelConfigDAO mdao = Mock(ModelConfigDAO)
def setup() {
modelConfigHelper.setup()
app.configDirectory = modelConfigHelper.baseCfgDirName
app.configuration = deployDBConfiguration
app.configuration.configDirectory = modelConfigHelper.baseCfgDirName
app.configChecksum = null
workFlow = new WorkFlow(app)
workFlow.initializeRegistry()

View File

@ -1,13 +1,15 @@
package deploydb.auth
import com.google.common.base.Optional
import deploydb.IntegrationModelHelper
import deploydb.IntegrationTestAppHelper
import deploydb.ModelConfigHelper
import dropwizardintegtest.TestLdapServer
import io.dropwizard.auth.basic.BasicCredentials
import javax.naming.AuthenticationException
import javax.naming.NamingEnumeration
import javax.naming.NamingException
import javax.naming.directory.InitialDirContext
import javax.naming.directory.Attributes
import javax.naming.directory.BasicAttribute
import javax.naming.directory.BasicAttributes
import javax.naming.directory.SearchResult
import spock.lang.*
@ -26,8 +28,23 @@ class LdapAuthenticatorSpec extends Specification {
}
class LdapAuthenticatorWithArgsSpec extends Specification {
private IntegrationTestAppHelper integAppHelper = new IntegrationTestAppHelper()
private IntegrationModelHelper integModelHelper = new IntegrationModelHelper(integAppHelper)
private ModelConfigHelper mcfgHelper = new ModelConfigHelper()
LdapConfiguration ldapConfiguration
def setup() {
mcfgHelper.setup()
integAppHelper.startAppWithConfiguration('deploydb.spock.yml')
integAppHelper.runner.getApplication().configuration.configDirectory = mcfgHelper.baseCfgDirName
ldapConfiguration = integAppHelper.runner.getApplication().configuration.ldapConfiguration
}
def cleanup() {
integAppHelper.stopApp()
mcfgHelper.cleanup()
}
class TestNamingEnumeration<SearchResult> extends NamingEnumeration {
@Override
SearchResult next() throws NamingException { return null }
@ -41,60 +58,76 @@ class LdapAuthenticatorWithArgsSpec extends Specification {
SearchResult nextElement() { throw new NoSuchElementException() }
}
def setup() {
ldapConfiguration = new LdapConfiguration()
}
def "authenticate() - successful authentication"() {
def "authenticate() - real successful authentication"() {
given:
/** Define interface objects */
Set<String> groups = ["Fandango"]
InitialDirContext context = Mock(InitialDirContext)
1 * context.close() >> _
/** Create LdapAuthenticator */
LdapAuthenticator ldapAuthenticator =
Spy(LdapAuthenticator, constructorArgs: [ldapConfiguration]) {
/** Mock interface functions */
buildContext(_) >> context
getGroupMemberships(_, _) >> groups
}
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
when:
Optional<User> userOpt = ldapAuthenticator.authenticate(new BasicCredentials("foo", "bar"))
Optional<User> userOpt = ldapAuthenticator.authenticate(new BasicCredentials("peter", "griffin"))
then:
userOpt.isPresent() == true
userOpt.get().name == "foo"
userOpt.get().groups.size() == 1
userOpt.get().groups[0] == "Fandango"
userOpt.get().name == "peter"
userOpt.get().groups.size() == 2
userOpt.get().groups.contains("fox")
userOpt.get().groups.contains("familyguy")
}
def "authenticate() - when buildContext throws NamingException, then should return failure"() {
def "authenticate() - when bindContext throws NamingException, then should return failure"() {
given:
LdapAuthenticator ldapAuthenticator =
Spy(LdapAuthenticator, constructorArgs: [ldapConfiguration]) {
/** Mock interface functions */
buildContext(_, _) >> { throw new NamingException("test") }
bindContext() >> { throw new NamingException("test") }
}
when:
Optional<User> userOpt = ldapAuthenticator.authenticate(new BasicCredentials("foo", "bar"))
Optional<User> userOpt = ldapAuthenticator.authenticate(new BasicCredentials("peter", "griffin"))
then:
userOpt.isPresent() == false
}
def "authenticate() - when buildContext throws ConnectException, then should return failure"() {
def "authenticate() - when bindContext throws ConnectException, then should return failure"() {
given:
LdapAuthenticator ldapAuthenticator =
Spy(LdapAuthenticator, constructorArgs: [ldapConfiguration]) {
/** Mock interface functions */
buildContext(_, _) >> { throw new ConnectException("test") }
bindContext() >> { throw new ConnectException("test") }
}
when:
Optional<User> userOpt = ldapAuthenticator.authenticate(new BasicCredentials("foo", "bar"))
Optional<User> userOpt = ldapAuthenticator.authenticate(new BasicCredentials("peter", "griffin"))
then:
userOpt.isPresent() == false
}
def "authenticate() - when bindContext throws AuthenticationException, then should return failure"() {
given:
LdapAuthenticator ldapAuthenticator =
Spy(LdapAuthenticator, constructorArgs: [ldapConfiguration]) {
/** Mock interface functions */
bindContext() >> { throw new AuthenticationException("test") }
}
when:
Optional<User> userOpt = ldapAuthenticator.authenticate(new BasicCredentials("peter", "griffin"))
then:
userOpt.isPresent() == false
}
def "authenticate() - when authenticateUser throws an exception"() {
given:
LdapAuthenticator ldapAuthenticator =
Spy(LdapAuthenticator, constructorArgs: [ldapConfiguration]) {
/** Mock interface functions */
authenticateUser(_, _) >> { throw new NamingException("test") }
}
when:
Optional<User> userOpt = ldapAuthenticator.authenticate(new BasicCredentials("peter", "griffin"))
then:
userOpt.isPresent() == false
@ -102,116 +135,113 @@ class LdapAuthenticatorWithArgsSpec extends Specification {
def "authenticate() - when getGroupMemberShips throws an exception"() {
given:
/** Define interface objects */
InitialDirContext context = Mock(InitialDirContext)
1 * context.close() >> _
/** Create LdapAuthenticator */
LdapAuthenticator ldapAuthenticator =
Spy(LdapAuthenticator, constructorArgs: [ldapConfiguration]) {
/** Mock interface functions */
buildContext(_) >> context
getGroupMemberships(_, _) >> { throw new NamingException("test") }
}
when:
Optional<User> userOpt = ldapAuthenticator.authenticate(new BasicCredentials("foo", "bar"))
Optional<User> userOpt = ldapAuthenticator.authenticate(new BasicCredentials("peter", "griffin"))
then:
userOpt.isPresent() == false
}
@Ignore
def "buildContext() - successful authentication"() {
//TO DO - when LDAP server is available with SPOCK tests
//when:
// launch ldap server
// create dir context and make sure it returns a valid context
//then:
//success
}
def "bindContext() - successful authentication"() {
given:
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
@Ignore
def "buildContext() - ldap server returns authentication failure"() {
//TO DO - when LDAP server is available with SPOCK tests
//when:
// launch ldap server
// create dir context and make sure it returns a valid context
//then:
// Authentication exception
}
def "buildContext() - empty uri causes ConfigurationException to be thrown"() {
when:
InitialDirContext context = ldapAuthenticator.bindContext()
then:
context != null
}
def "bindContext() - authentication failure raise AuthenticationException"() {
given:
ldapConfiguration.bindPassword = "fake"
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
when:
ldapAuthenticator.bindContext()
then:
thrown (javax.naming.AuthenticationException)
}
def "bindContext() - empty uri causes ConfigurationException to be thrown"() {
given:
ldapConfiguration.uri = new URI("")
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
InitialDirContext context = ldapAuthenticator.buildContext(new BasicCredentials("foo", "bar"))
when:
ldapAuthenticator.bindContext()
then:
thrown(javax.naming.ConfigurationException)
}
def "buildContext() - uri with non-ldap protocol causes NamingException to be thrown"() {
when:
def "bindContext() - uri with non-ldap protocol causes NamingException to be thrown"() {
given:
ldapConfiguration.uri = new URI("http://localhost:10389")
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
InitialDirContext context = ldapAuthenticator.buildContext(new BasicCredentials("foo", "bar"))
when:
ldapAuthenticator.bindContext()
then:
thrown(javax.naming.NamingException)
}
def "buildContext() - malformed uri causes NamingException to be thrown"() {
def "bindContext() - malformed uri causes NamingException to be thrown"() {
when:
ldapConfiguration.uri = new URI("ldap:localhost:10389")
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
InitialDirContext context = ldapAuthenticator.buildContext(new BasicCredentials("foo", "bar"))
ldapAuthenticator.bindContext()
then:
thrown(javax.naming.NamingException)
}
def "getGroupMemberships() - successful to retrieve groups"() {
def "searchContext() - successful to retrieve attributes"() {
given:
ldapConfiguration.groupNameAttribute = "pizza"
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
/**
* Mock the interfaces to return following attributes
*/
InitialDirContext context = mockGetGroupMembershipInterface("pizza", "Fandango")
InitialDirContext context = ldapAuthenticator.bindContext()
final String filter = String.format("(&(%s=%s)(objectClass=%s))",
ldapConfiguration.userNamePrefix, "peter",
ldapConfiguration.userObjectClass)
when:
Set<String> groups = ldapAuthenticator.getGroupMemberships(context, "foo")
Set<String> attributes = ldapAuthenticator.searchContext(
context, ldapConfiguration.baseDC, filter,
ldapConfiguration.distinguishedNamePrefix)
then:
groups.size() == 1
groups[0] == "Fandango"
attributes.size() == 1
attributes[0] == "cn=peter griffin,ou=people,dc=example,dc=com"
}
def "getGroupMemberships() - returns empty SET on groupNameAttribute mismatch"() {
def "searchContext() - returns empty SET on filter fails to match"() {
given:
ldapConfiguration.groupNameAttribute = "pizza"
ldapConfiguration.distinguishedNamePrefix = "invalidDN"
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
/**
* Mock the interfaces to return following attributes
*/
InitialDirContext context = mockGetGroupMembershipInterface("BadPizza", "Fandango")
InitialDirContext context = ldapAuthenticator.bindContext()
final String filter = String.format("(&(%s=%s)(objectClass=%s))",
ldapConfiguration.userNamePrefix, "peter",
ldapConfiguration.userObjectClass)
when:
Set<String> groups = ldapAuthenticator.getGroupMemberships(context, "foo")
Set<String> attributes = ldapAuthenticator.searchContext(
context, ldapConfiguration.baseDC, filter,
ldapConfiguration.distinguishedNamePrefix)
then:
groups.size() == 0
attributes.size() == 0
}
@Ignore
def "getGroupMemberships() - returns empty SET on groupClassName fails to match"() {
//TO DO - when LDAP server is available with SPOCK tests
}
def "getGroupMembership() - returns empty SET when context.search() returns are empty"() {
def "searchContext() - returns empty SET when context.search() returns are empty"() {
given:
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
@ -222,13 +252,14 @@ class LdapAuthenticatorWithArgsSpec extends Specification {
InitialDirContext context = mockInitialDirContext() { return emptyResults }
when:
Set<String> groups = ldapAuthenticator.getGroupMemberships(context, "foo")
Set<String> attributes = ldapAuthenticator.searchContext(
context,"dc=example,dc=com", "(cn=foo)", "pizza")
then:
groups.size() == 0
attributes.size() == 0
}
def "getGroupMembership() - rethrows the NamingException raised by context.search()"() {
def "searchContext() - rethrows the NamingException raised by context.search()"() {
given:
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
@ -238,13 +269,13 @@ class LdapAuthenticatorWithArgsSpec extends Specification {
0 * context._
when:
Set<String> groups = ldapAuthenticator.getGroupMemberships(context, "foo")
ldapAuthenticator.searchContext(context,"dc=example,dc=com", "(cn=foo)", "pizza")
then:
thrown(NamingException)
}
def "getGroupMembership() - rethrows the NamingException raised by results.hasMore()"() {
def "searchContext() - rethrows the NamingException raised by results.hasMore()"() {
given:
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
@ -254,56 +285,47 @@ class LdapAuthenticatorWithArgsSpec extends Specification {
1 * results.close()
0 * results._
/** Mock InitialDirContext to return "results"*/
/** Mock InitialDirContext to return "results" */
InitialDirContext context = mockInitialDirContext() { return results }
when:
Set<String> groups = ldapAuthenticator.getGroupMemberships(context, "foo")
ldapAuthenticator.searchContext(context,"dc=example,dc=com", "(cn=foo)", "pizza")
then:
thrown(NamingException)
}
def "authenticateUser() - successful user authentication"() {
given:
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
InitialDirContext context = ldapAuthenticator.bindContext()
when:
String userDN = ldapAuthenticator.authenticateUser(context, new BasicCredentials("peter", "griffin"))
then:
userDN == "cn=peter griffin,ou=people,dc=example,dc=com"
}
def "authenticateUser() - invalid credentials throw AuthenticationException"() {
given:
LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration)
InitialDirContext context = ldapAuthenticator.bindContext()
when:
String userDN = ldapAuthenticator.authenticateUser(context, new BasicCredentials("peter", "simpson"))
then:
thrown(javax.naming.AuthenticationException)
}
/* Class Helpers */
/** Mock InitialDirContext */
InitialDirContext mockInitialDirContext(Closure closure) {
private InitialDirContext mockInitialDirContext(Closure closure) {
InitialDirContext context = Mock(InitialDirContext)
1 * context.search(_, _, _) >> closure.call()
0 * context._
return context
}
/* Populate SearchResult */
SearchResult populateSearchResult(String attribType, String attribValue) {
Attributes matchAttrs = new BasicAttributes(true)
matchAttrs.put(new BasicAttribute(attribType, attribValue))
return new SearchResult("faux_name", null, matchAttrs)
}
/** Mock TestNamingEnumeration for 1 walk through the while loop */
TestNamingEnumeration<SearchResult> mockResultsWalkOneLoop(Closure closure) {
TestNamingEnumeration<SearchResult> results = Mock(TestNamingEnumeration)
1 * results.hasMore() >> true
1 * results.hasMore() >> false
1 * results.close()
1 * results.next() >> closure.call()
0 * results._
return results
}
/** Mocking all interfaces of getGroupMemberships() */
InitialDirContext mockGetGroupMembershipInterface(String attribType, String attribValue) {
/** Populate SearchResult */
SearchResult result = populateSearchResult(attribType, attribValue)
/** Mock NamingEnumeration to walk through while loop once and return "result" */
TestNamingEnumeration<SearchResult> results = mockResultsWalkOneLoop() { return result }
/** Mock InitialDirContext to return "results"*/
InitialDirContext context = mockInitialDirContext() { return results }
return context
}
}

View File

@ -170,7 +170,7 @@ description: Manual LDAP Promotion Smoke Test
thrown(ConfigurationValidationException)
}
def "Loading a promotion config with invalid type throws a validation exception"() {
def "Loading a promotion config with invalid classname throws a validation exception"() {
when:
Promotion promotion = promotionLoader.loadFromString("""
type: deploydb.invalid.promotion.classname.BasicPromotionImpl
@ -180,4 +180,13 @@ description: Basic Promotion Smoke Test
thrown(ConfigurationValidationException)
}
def "Loading a promotion config with invalid derivation throws a validation exception"() {
when:
Promotion promotion = promotionLoader.loadFromString("""
type: deploydb.models.promotion.Promotion
description: Basic Promotion Smoke Test
""")
then:
thrown(ConfigurationValidationException)
}
}

View File

@ -1,57 +1,60 @@
dn: ou=people,dc=yourcompany,dc=com
dn: ou=people,dc=example,dc=com
ou: people
objectClass: organizationalUnit
dn: ou=groups,dc=yourcompany,dc=com
dn: ou=groups,dc=example,dc=com
ou: groups
objectClass: organizationalUnit
dn: cn=fox,ou=groups,dc=yourcompany,dc=com
dn: cn=Fox,ou=groups,dc=example,dc=com
cn: fox
add: memberUid
memberUid: bart
memberUid: lisa
memberUid: peter
memberUid: lois
objectClass: groupOfUniqueNames
memberUid: cn=Bart Simpson,ou=people,dc=example,dc=com
memberUid: cn=Lisa Simpson,ou=people,dc=example,dc=com
memberUid: cn=Peter Griffin,ou=people,dc=example,dc=com
memberUid: cn=Lois Griffin,ou=people,dc=example,dc=com
objectClass: posixGroup
dn: cn=Simpsons,ou=groups,dc=yourcompany,dc=com
cn: Simpsons
memberUid: bart
memberUid: lisa
objectClass: groupOfUniqueNames
dn: cn=Simpsons,ou=groups,dc=example,dc=com
cn: simpsons
memberUid: cn=Bart Simpson,ou=people,dc=example,dc=com
memberUid: cn=Lisa Simpson,ou=people,dc=example,dc=com
objectClass: posixGroup
dn: cn=FamilyGuy,ou=groups,dc=yourcompany,dc=com
cn: FamilyGuy
memberUid: peter
memberUid: lois
objectClass: groupOfUniqueNames
dn: cn=Family Guy,ou=groups,dc=example,dc=com
cn: familyguy
memberUid: cn=Peter Griffin,ou=people,dc=example,dc=com
memberUid: cn=Lois Griffin,ou=people,dc=example,dc=com
objectClass: posixGroup
dn: cn=bart,ou=people,dc=yourcompany,dc=com
cn: Bart Simpson
dn: cn=Bart Simpson,ou=people,dc=example,dc=com
cn: bart
sn: Simpson
givenName: Bart
uid: bart
objectClass: inetOrgPerson
objectClass: posixUser
userpassword: simpson
dn: cn=lisa,ou=people,dc=yourcompany,dc=com
cn: Lisa Simpson
dn: cn=Lisa Simpson,ou=people,dc=example,dc=com
cn: lisa
sn: Simpson
givenName: Lisa
uid: lisa
objectClass: inetOrgPerson
objectClass: posixUser
userpassword: simpson
dn: cn=peter,ou=people,dc=yourcompany,dc=com
cn: Peter Griffin
dn: cn=Peter Griffin,ou=people,dc=example,dc=com
cn: peter
sn: Griffin
givenName: Peter
uid: peter
objectClass: inetOrgPerson
objectClass: posixUser
userpassword: griffin
dn: cn=lois,ou=people,dc=yourcompany,dc=com
cn: Lois Griffin
dn: cn=Lois Griffin,ou=people,dc=example,dc=com
cn: lois
sn: Griffin
givenName: Lois
uid: lois
objectClass: inetOrgPerson
objectClass: posixUser
userpassword: griffin