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:
parent
09ecbf0e24
commit
05e1541e13
|
@ -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+"
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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">"</span><span class="content">dc=example,dc=com</span><span class="delimiter">"</span></span>
|
||||
<span class="key">bindDN</span>: <span class="string"><span class="delimiter">"</span><span class="content">cn=admin</span><span class="delimiter">"</span></span>
|
||||
<span class="key">bindPassword</span>: <span class="string"><span class="delimiter">"</span><span class="content">secret</span><span class="delimiter">"</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>
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -141,6 +141,6 @@ public class StubAppRunner<C extends Configuration> {
|
|||
}
|
||||
|
||||
void setConfigDirectory(String configDirectory) {
|
||||
application.configDirectory = configDirectory
|
||||
application.configuration.configDirectory = configDirectory
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
type: deploydb.models.promotion.ManualLDAPPromotionImpl
|
||||
description: "Manual LDAP Promotion"
|
||||
attributes:
|
||||
allowedGroup: GRP-AWS-Dev-Eng
|
|
@ -0,0 +1,8 @@
|
|||
description: "Advanced Service"
|
||||
artifacts:
|
||||
- adv.group.1:bg1
|
||||
- adv.group.2:bg2
|
||||
pipelines:
|
||||
- basicPipe
|
||||
promotions:
|
||||
- manualPromo
|
|
@ -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
|
||||
----
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -46,10 +46,10 @@ class Promotion {
|
|||
boolean isType() {
|
||||
try {
|
||||
PromotionImpl impl = getPromotionImpl()
|
||||
return impl instanceof PromotionImpl
|
||||
} catch (all) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue