Repository: incubator-eagle Updated Branches: refs/heads/master 446c22720 -> 164e844c1
[EAGLE-583] implement ldap authentication logic integrated with Basic Authentication implement ldap authentication logic integrated with Basic Authentication Author: anyway1021 <m...@apache.org> Closes #649 from anyway1021/EAGLE-583. Project: http://git-wip-us.apache.org/repos/asf/incubator-eagle/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-eagle/commit/164e844c Tree: http://git-wip-us.apache.org/repos/asf/incubator-eagle/tree/164e844c Diff: http://git-wip-us.apache.org/repos/asf/incubator-eagle/diff/164e844c Branch: refs/heads/master Commit: 164e844c1ec9e16b4228eddd5fb6a2ba8196e661 Parents: 446c227 Author: anyway1021 <m...@apache.org> Authored: Tue Nov 15 18:37:10 2016 +0800 Committer: anyway1021 <m...@apache.org> Committed: Tue Nov 15 18:37:10 2016 +0800 ---------------------------------------------------------------------- .../authenticator/LdapBasicAuthenticator.java | 55 +++++++++- .../authentication/config/LdapSettings.java | 110 +++++-------------- .../src/main/resources/configuration.yml | 31 +++--- .../LdapBasicAuthenticatorTest.java | 86 +++++++++++++++ .../SimpleBasicAuthenticatorTest.java | 71 ++++++++++++ .../src/test/resources/configuration.yml | 31 ++++-- 6 files changed, 270 insertions(+), 114 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/164e844c/eagle-server/src/main/java/org/apache/eagle/server/authentication/authenticator/LdapBasicAuthenticator.java ---------------------------------------------------------------------- diff --git a/eagle-server/src/main/java/org/apache/eagle/server/authentication/authenticator/LdapBasicAuthenticator.java b/eagle-server/src/main/java/org/apache/eagle/server/authentication/authenticator/LdapBasicAuthenticator.java index 87ebb34..14652c3 100644 --- a/eagle-server/src/main/java/org/apache/eagle/server/authentication/authenticator/LdapBasicAuthenticator.java +++ b/eagle-server/src/main/java/org/apache/eagle/server/authentication/authenticator/LdapBasicAuthenticator.java @@ -22,8 +22,18 @@ import io.dropwizard.auth.Authenticator; import io.dropwizard.auth.basic.BasicCredentials; import org.apache.eagle.common.authentication.User; import org.apache.eagle.server.authentication.config.LdapSettings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.naming.Context; +import javax.naming.directory.InitialDirContext; +import java.util.Hashtable; public class LdapBasicAuthenticator implements Authenticator<BasicCredentials, User> { + private static final Logger LOGGER = LoggerFactory.getLogger(LdapBasicAuthenticator.class); + private static final String LDAP_LDAP_CTX_FACTORY_NAME = "com.sun.jndi.ldap.LdapCtxFactory"; + private static final String LDAP_CONNECT_TIMEOUT_KEY = "com.sun.jndi.ldap.connect.timeout"; + private static final String LDAP_READ_TIMEOUT_KEY = "com.sun.jndi.ldap.read.timeout"; private LdapSettings settings = null; public LdapBasicAuthenticator(LdapSettings settings) { @@ -31,11 +41,46 @@ public class LdapBasicAuthenticator implements Authenticator<BasicCredentials, U } public Optional<User> authenticate(BasicCredentials credentials) throws AuthenticationException { - // TODO need to implement ldap authentication logic - boolean pass = true; - if (pass) { - return Optional.of(new User("ldap.username")); + String sanitizedUsername = sanitizeUsername(credentials.getUsername()); + try { + new InitialDirContext(getContextEnvironment(sanitizedUsername, credentials.getPassword())); + return Optional.of(new User(sanitizedUsername)); + } catch (javax.naming.AuthenticationException ae) { + LOGGER.warn(String.format("Authentication failed for user[%s]: wrong username or password", sanitizedUsername)); + return Optional.absent(); + } catch (Exception e) { + throw new AuthenticationException(String.format("Error occurs while trying to authenticate for user[%s]: %s", sanitizedUsername, e.getMessage()), e); } - return Optional.absent(); } + + Hashtable<String, String> getContextEnvironment(String sanitizedUsername, String password) { + String providerUrl = settings.getProviderUrl(); + if (providerUrl == null) { + throw new IllegalArgumentException("providerUrl of the ldap service shouldn't be null"); + } + + Hashtable<String, String> env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_LDAP_CTX_FACTORY_NAME); + env.put(Context.PROVIDER_URL, providerUrl); + env.put(LDAP_CONNECT_TIMEOUT_KEY, String.valueOf(settings.getConnectingTimeout().toMilliseconds())); + env.put(LDAP_READ_TIMEOUT_KEY, String.valueOf(settings.getReadingTimeout().toMilliseconds())); + + String strategy = settings.getStrategy(); + if (!"".equals(strategy)) { + env.put(Context.SECURITY_AUTHENTICATION, strategy); + } + + env.put(Context.SECURITY_PRINCIPAL, comprisePrincipal(sanitizedUsername)); + env.put(Context.SECURITY_CREDENTIALS, password); + return env; + } + + String comprisePrincipal(String sanitizedUsername) { + return settings.getPrincipalTemplate().replaceAll("\\$\\{USERNAME\\}", sanitizedUsername); + } + + String sanitizeUsername(String username) { + return username.replaceAll("[^a-zA-Z0-9_.]", ""); + } + } http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/164e844c/eagle-server/src/main/java/org/apache/eagle/server/authentication/config/LdapSettings.java ---------------------------------------------------------------------- diff --git a/eagle-server/src/main/java/org/apache/eagle/server/authentication/config/LdapSettings.java b/eagle-server/src/main/java/org/apache/eagle/server/authentication/config/LdapSettings.java index 0d04645..6bb3303 100644 --- a/eagle-server/src/main/java/org/apache/eagle/server/authentication/config/LdapSettings.java +++ b/eagle-server/src/main/java/org/apache/eagle/server/authentication/config/LdapSettings.java @@ -16,127 +16,69 @@ */ package org.apache.eagle.server.authentication.config; +import io.dropwizard.util.Duration; import org.codehaus.jackson.annotate.JsonProperty; public class LdapSettings { - private String uri = null; - private String userFilter = null; - private String groupFilter = null; - private String userNameAttribute = null; - private String groupNameAttribute = null; - private String groupMembershipAttribute = null; - private String groupClassName = null; - private String[] restrictToGroups = null; - private String connectTimeout = null; - private String readTimeout = null; - @JsonProperty - public String getUri() { - return uri; - } - - @JsonProperty - public LdapSettings setUri(String uri) { - this.uri = uri; - return this; - } - - @JsonProperty - public String getUserFilter() { - return userFilter; - } - - @JsonProperty - public LdapSettings setUserFilter(String userFilter) { - this.userFilter = userFilter; - return this; - } - - @JsonProperty - public String getGroupFilter() { - return groupFilter; - } - - @JsonProperty - public LdapSettings setGroupFilter(String groupFilter) { - this.groupFilter = groupFilter; - return this; - } - - @JsonProperty - public String getUserNameAttribute() { - return userNameAttribute; - } - - @JsonProperty - public LdapSettings setUserNameAttribute(String userNameAttribute) { - this.userNameAttribute = userNameAttribute; - return this; - } - - @JsonProperty - public String getGroupNameAttribute() { - return groupNameAttribute; - } - - @JsonProperty - public LdapSettings setGroupNameAttribute(String groupNameAttribute) { - this.groupNameAttribute = groupNameAttribute; - return this; - } + private String providerUrl = ""; + private String strategy = ""; + private String principalTemplate = ""; + private Duration connectingTimeout = Duration.parse("500ms"); + private Duration readingTimeout = Duration.parse("500ms"); @JsonProperty - public String getGroupMembershipAttribute() { - return groupMembershipAttribute; + public String getProviderUrl() { + return providerUrl; } @JsonProperty - public LdapSettings setGroupMembershipAttribute(String groupMembershipAttribute) { - this.groupMembershipAttribute = groupMembershipAttribute; + public LdapSettings setProviderUrl(String providerUrl) { + this.providerUrl = providerUrl; return this; } @JsonProperty - public String getGroupClassName() { - return groupClassName; + public String getPrincipalTemplate() { + return principalTemplate; } @JsonProperty - public LdapSettings setGroupClassName(String groupClassName) { - this.groupClassName = groupClassName; + public LdapSettings setPrincipalTemplate(String principalTemplate) { + this.principalTemplate = principalTemplate; return this; } @JsonProperty - public String[] getRestrictToGroups() { - return restrictToGroups; + public String getStrategy() { + return strategy; } @JsonProperty - public LdapSettings setRestrictToGroups(String[] restrictToGroups) { - this.restrictToGroups = restrictToGroups; + public LdapSettings setStrategy(String strategy) { + this.strategy = strategy; return this; } @JsonProperty - public String getConnectTimeout() { - return connectTimeout; + public Duration getConnectingTimeout() { + return connectingTimeout; } @JsonProperty - public LdapSettings setConnectTimeout(String connectTimeout) { - this.connectTimeout = connectTimeout; + public LdapSettings setConnectingTimeout(Duration connectingTimeout) { + this.connectingTimeout = connectingTimeout; return this; } @JsonProperty - public String getReadTimeout() { - return readTimeout; + public Duration getReadingTimeout() { + return readingTimeout; } @JsonProperty - public LdapSettings setReadTimeout(String readTimeout) { - this.readTimeout = readTimeout; + public LdapSettings setReadingTimeout(Duration readingTimeout) { + this.readingTimeout = readingTimeout; return this; } } http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/164e844c/eagle-server/src/main/resources/configuration.yml ---------------------------------------------------------------------- diff --git a/eagle-server/src/main/resources/configuration.yml b/eagle-server/src/main/resources/configuration.yml index 6090c4f..8d388b0 100644 --- a/eagle-server/src/main/resources/configuration.yml +++ b/eagle-server/src/main/resources/configuration.yml @@ -21,7 +21,6 @@ server: - type: http port: 9091 - # --------------------------------------------- # Eagle Authentication Configuration # --------------------------------------------- @@ -54,15 +53,21 @@ auth: # for ldap authentication, effective only when auth.mode=ldap ldap: - uri: ldaps://ldap.server.address:636 - userFilter: ou=x,dc=y,dc=z - groupFilter: ou=x,dc=y,dc=z - userNameAttribute: cn - groupNameAttribute: cn - groupMembershipAttribute: memberUid - groupClassName: posixGroup - restrictToGroups: - - user - - admin - connectTimeout: 500ms - readTimeout: 500ms + # url providing ldap service. By convention, the port for typical ldap service is 389, and ldap service over ssl + # uses port 636 with protocol "ldaps", which requires certificates pre-installed. + providerUrl: ldap://server.address.or.domain:port + + # template string containing ${USERNAME} placeholder. This is designed for some orgs who don't use plain usernames + # to authenticate, e.g. they may use its members' email address as the username: ${USERNAME}@some.org. When username + # is supposed to be recognized originally, just configure this parameter as ${USERNAME} + principalTemplate: ${USERNAME}@maybe.email.suffix + + # string of strategy used by ldap service. "simple" is usually supported in most circumstances, we can use it by + # default or leave it a blank string. + strategy: simple + + # timeout expression for connecting to ldap service endpoint + connectingTimeout: 500ms + + # timeout expression for reading from ldap service + readingTimeout: 500ms http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/164e844c/eagle-server/src/test/java/org/apache/eagle/server/authentication/authenticator/LdapBasicAuthenticatorTest.java ---------------------------------------------------------------------- diff --git a/eagle-server/src/test/java/org/apache/eagle/server/authentication/authenticator/LdapBasicAuthenticatorTest.java b/eagle-server/src/test/java/org/apache/eagle/server/authentication/authenticator/LdapBasicAuthenticatorTest.java new file mode 100644 index 0000000..492521f --- /dev/null +++ b/eagle-server/src/test/java/org/apache/eagle/server/authentication/authenticator/LdapBasicAuthenticatorTest.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.eagle.server.authentication.authenticator; + +import io.dropwizard.util.Duration; +import org.apache.eagle.server.authentication.config.LdapSettings; +import org.junit.Assert; +import org.junit.Test; + +import javax.naming.Context; +import java.util.Hashtable; + +public class LdapBasicAuthenticatorTest { + + private static final String USERNAME_SUFFIX = "@some.emailbox.suffix"; + private static final String USERNAME_TEMPLATE = "${USERNAME}" + USERNAME_SUFFIX; + private static final String LDAP_SERVICE_PROVIDER_URL = "ldap://some.address:port"; + private static final String STRATEGY_SIMPLE = "customized"; + private static final String CONNECTING_TIMEOUT_VALUE = "500ms"; + private static final String READING_TIMEOUT_VALUE = "800ms"; + private static final String LDAP_CTX_FACTORY_NAME = "com.sun.jndi.ldap.LdapCtxFactory"; + private static final String LDAP_CONNECT_TIMEOUT_KEY = "com.sun.jndi.ldap.connect.timeout"; + private static final String LDAP_READ_TIMEOUT_KEY = "com.sun.jndi.ldap.read.timeout"; + private static final LdapBasicAuthenticator AUTHENTICATOR_FOR_UTIL_METHODS = new LdapBasicAuthenticator( + new LdapSettings() + .setProviderUrl(LDAP_SERVICE_PROVIDER_URL) + .setPrincipalTemplate(USERNAME_TEMPLATE) + .setStrategy(STRATEGY_SIMPLE) + .setConnectingTimeout(Duration.parse(CONNECTING_TIMEOUT_VALUE)) + .setReadingTimeout(Duration.parse(READING_TIMEOUT_VALUE)) + ); + + @Test + public void testSanitizeUsername() { + String correctUsername = "userNAME_123.45Z"; + String sanitized = AUTHENTICATOR_FOR_UTIL_METHODS.sanitizeUsername(correctUsername); + Assert.assertEquals(correctUsername, sanitized); + + String incorrectUsername = "userNAME-~!@#$%^&777*()_+-=`[]\\{}|;':\",./<>?ä½ "; + sanitized = AUTHENTICATOR_FOR_UTIL_METHODS.sanitizeUsername(incorrectUsername); + System.out.println(sanitized); + Assert.assertEquals("userNAME777_.", sanitized); + } + + @Test + public void testComprisePrincipal() { + String username = "my.userNAME_123"; + String principal = AUTHENTICATOR_FOR_UTIL_METHODS.comprisePrincipal(username); + Assert.assertEquals(username+USERNAME_SUFFIX, principal); + } + + @Test + public void testGetContextEnvironment() { + String username = "username"; + String secret_phrase = "secret-phrase"; + Hashtable<String, String> env = AUTHENTICATOR_FOR_UTIL_METHODS.getContextEnvironment(username, secret_phrase); + + Assert.assertEquals("unexpected ldap context factory name", LDAP_CTX_FACTORY_NAME, env.get(Context.INITIAL_CONTEXT_FACTORY)); + Assert.assertEquals("unexpected ldap serivce provider url", LDAP_SERVICE_PROVIDER_URL, env.get(Context.PROVIDER_URL)); + Assert.assertEquals("unexpected connecting timeout value", String.valueOf(Duration.parse(CONNECTING_TIMEOUT_VALUE).toMilliseconds()), env.get(LDAP_CONNECT_TIMEOUT_KEY)); + Assert.assertEquals("unexpected reading timeout value", String.valueOf(Duration.parse(READING_TIMEOUT_VALUE).toMilliseconds()), env.get(LDAP_READ_TIMEOUT_KEY)); + Assert.assertEquals("unexpected username", username+USERNAME_SUFFIX, env.get(Context.SECURITY_PRINCIPAL)); + Assert.assertEquals("unexpected secret credentials", secret_phrase, env.get(Context.SECURITY_CREDENTIALS)); + Assert.assertEquals("unexpected strategy", STRATEGY_SIMPLE, env.get(Context.SECURITY_AUTHENTICATION)); + + // check strategy while it's configured as "" + LdapBasicAuthenticator blankStrategyAuthenticator = new LdapBasicAuthenticator(new LdapSettings().setStrategy("")); + String strategyMaybeBlank = blankStrategyAuthenticator.getContextEnvironment(username, secret_phrase).get(Context.SECURITY_AUTHENTICATION); + Assert.assertNull("unexpected strategy", strategyMaybeBlank); + } +} http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/164e844c/eagle-server/src/test/java/org/apache/eagle/server/authentication/authenticator/SimpleBasicAuthenticatorTest.java ---------------------------------------------------------------------- diff --git a/eagle-server/src/test/java/org/apache/eagle/server/authentication/authenticator/SimpleBasicAuthenticatorTest.java b/eagle-server/src/test/java/org/apache/eagle/server/authentication/authenticator/SimpleBasicAuthenticatorTest.java new file mode 100644 index 0000000..e896a7c --- /dev/null +++ b/eagle-server/src/test/java/org/apache/eagle/server/authentication/authenticator/SimpleBasicAuthenticatorTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.eagle.server.authentication.authenticator; + +import com.google.common.base.Optional; +import io.dropwizard.auth.AuthenticationException; +import io.dropwizard.auth.basic.BasicCredentials; +import org.apache.eagle.common.authentication.User; +import org.apache.eagle.server.authentication.config.SimpleSettings; +import org.junit.Assert; +import org.junit.Test; + +public class SimpleBasicAuthenticatorTest { + + private static final String TEST_USERNAME = "normal-username"; + private static final String TEST_SECRET_PHRASE = "secret-phrase"; + private static final String TEST_UNEXISTING_USERNAME = "unexisting-username"; + private static final String TEST_WRONG_SECRET_PHRASE = "wrong-secret-phrase"; + private static SimpleBasicAuthenticator authenticator = new SimpleBasicAuthenticator(new SimpleSettings().setUsername(TEST_USERNAME).setPassword(TEST_SECRET_PHRASE)); + + @Test + public void testNormal() { + try { + BasicCredentials credentials = new BasicCredentials(TEST_USERNAME, TEST_SECRET_PHRASE); + Optional<User> result = authenticator.authenticate(credentials); + Assert.assertTrue("result isn't present when passed correct credentials", result.isPresent()); + User user = result.get(); + Assert.assertEquals("authenticated user is not expected", TEST_USERNAME, user.getName()); + } + catch (AuthenticationException e) { + Assert.fail("unexpected error occurs: "+e.getMessage()); + } + } + + @Test + public void testUnexistingUsername() { + try { + Optional<User> result = authenticator.authenticate(new BasicCredentials(TEST_UNEXISTING_USERNAME, TEST_SECRET_PHRASE)); + Assert.assertFalse("result is present when passed unexisting username", result.isPresent()); + } + catch (AuthenticationException e) { + Assert.fail("unexpected error occurs: "+e.getMessage()); + } + } + + @Test + public void testWrongPassword() { + try { + Optional<User> result = authenticator.authenticate(new BasicCredentials(TEST_USERNAME, TEST_WRONG_SECRET_PHRASE)); + Assert.assertFalse("result is present when passed wrong password", result.isPresent()); + } + catch (AuthenticationException e) { + Assert.fail("unexpected error occurs: "+e.getMessage()); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/164e844c/eagle-server/src/test/resources/configuration.yml ---------------------------------------------------------------------- diff --git a/eagle-server/src/test/resources/configuration.yml b/eagle-server/src/test/resources/configuration.yml index 6795aac..8d388b0 100644 --- a/eagle-server/src/test/resources/configuration.yml +++ b/eagle-server/src/test/resources/configuration.yml @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + server: applicationConnectors: - type: http @@ -52,15 +53,21 @@ auth: # for ldap authentication, effective only when auth.mode=ldap ldap: - uri: ldaps://ldap.server.address:636 - userFilter: ou=x,dc=y,dc=z - groupFilter: ou=x,dc=y,dc=z - userNameAttribute: cn - groupNameAttribute: cn - groupMembershipAttribute: memberUid - groupClassName: posixGroup - restrictToGroups: - - user - - admin - connectTimeout: 500ms - readTimeout: 500ms + # url providing ldap service. By convention, the port for typical ldap service is 389, and ldap service over ssl + # uses port 636 with protocol "ldaps", which requires certificates pre-installed. + providerUrl: ldap://server.address.or.domain:port + + # template string containing ${USERNAME} placeholder. This is designed for some orgs who don't use plain usernames + # to authenticate, e.g. they may use its members' email address as the username: ${USERNAME}@some.org. When username + # is supposed to be recognized originally, just configure this parameter as ${USERNAME} + principalTemplate: ${USERNAME}@maybe.email.suffix + + # string of strategy used by ldap service. "simple" is usually supported in most circumstances, we can use it by + # default or leave it a blank string. + strategy: simple + + # timeout expression for connecting to ldap service endpoint + connectingTimeout: 500ms + + # timeout expression for reading from ldap service + readingTimeout: 500ms