You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@griffin.apache.org by gu...@apache.org on 2018/10/25 14:20:47 UTC
incubator-griffin git commit: [GRIFFIN-207] LDAP login service
improvements
Repository: incubator-griffin
Updated Branches:
refs/heads/master 3c61e3f1f -> 73a7d84b6
[GRIFFIN-207] LDAP login service improvements
- allow non-CN usernames
- allow disabling certificate checks
- allow limiting set of login users
- improve logging
Author: Nikolay Sokolov <ch...@gmail.com>
Closes #441 from chemikadze/GRIFFIN-207.
Project: http://git-wip-us.apache.org/repos/asf/incubator-griffin/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-griffin/commit/73a7d84b
Tree: http://git-wip-us.apache.org/repos/asf/incubator-griffin/tree/73a7d84b
Diff: http://git-wip-us.apache.org/repos/asf/incubator-griffin/diff/73a7d84b
Branch: refs/heads/master
Commit: 73a7d84b672fded4b773cfd37c32c0ab01191040
Parents: 3c61e3f
Author: Nikolay Sokolov <ch...@gmail.com>
Authored: Thu Oct 25 22:20:41 2018 +0800
Committer: William Guo <gu...@apache.org>
Committed: Thu Oct 25 22:20:41 2018 +0800
----------------------------------------------------------------------
.../apache/griffin/core/config/LoginConfig.java | 9 +-
.../core/login/LoginServiceLdapImpl.java | 122 +++++++++++++++----
.../login/ldap/SelfSignedSocketFactory.java | 102 ++++++++++++++++
3 files changed, 207 insertions(+), 26 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/73a7d84b/service/src/main/java/org/apache/griffin/core/config/LoginConfig.java
----------------------------------------------------------------------
diff --git a/service/src/main/java/org/apache/griffin/core/config/LoginConfig.java b/service/src/main/java/org/apache/griffin/core/config/LoginConfig.java
index ca43751..8c9e4a2 100644
--- a/service/src/main/java/org/apache/griffin/core/config/LoginConfig.java
+++ b/service/src/main/java/org/apache/griffin/core/config/LoginConfig.java
@@ -39,7 +39,12 @@ public class LoginConfig {
private String searchBase;
@Value("${ldap.searchPattern}")
private String searchPattern;
-
+ @Value("${ldap.sslSkipVerify:false}")
+ private boolean sslSkipVerify;
+ @Value("${ldap.bindDN:}")
+ private String bindDN;
+ @Value("${ldap.bindPassword:}")
+ private String bindPassword;
@Bean
public LoginService loginService() {
@@ -48,7 +53,7 @@ public class LoginConfig {
return new LoginServiceDefaultImpl();
case "ldap":
return new LoginServiceLdapImpl(url, email, searchBase,
- searchPattern);
+ searchPattern, sslSkipVerify, bindDN, bindPassword);
default:
return null;
}
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/73a7d84b/service/src/main/java/org/apache/griffin/core/login/LoginServiceLdapImpl.java
----------------------------------------------------------------------
diff --git a/service/src/main/java/org/apache/griffin/core/login/LoginServiceLdapImpl.java b/service/src/main/java/org/apache/griffin/core/login/LoginServiceLdapImpl.java
index 9a96cb9..56f6765 100644
--- a/service/src/main/java/org/apache/griffin/core/login/LoginServiceLdapImpl.java
+++ b/service/src/main/java/org/apache/griffin/core/login/LoginServiceLdapImpl.java
@@ -22,15 +22,20 @@ package org.apache.griffin.core.login;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
+import java.util.NoSuchElementException;
+import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
+import org.apache.commons.lang.StringUtils;
+import org.apache.griffin.core.login.ldap.SelfSignedSocketFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
@@ -48,13 +53,20 @@ public class LoginServiceLdapImpl implements LoginService {
private String searchBase;
private String searchPattern;
private SearchControls searchControls;
+ private boolean sslSkipVerify;
+ private String bindDN;
+ private String bindPassword;
public LoginServiceLdapImpl(String url, String email, String searchBase,
- String searchPattern) {
+ String searchPattern, boolean sslSkipVerify,
+ String bindDN, String bindPassword) {
this.url = url;
this.email = email;
this.searchBase = searchBase;
this.searchPattern = searchPattern;
+ this.sslSkipVerify = sslSkipVerify;
+ this.bindDN = bindDN;
+ this.bindPassword = bindPassword;
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
this.searchControls = searchControls;
@@ -62,54 +74,116 @@ public class LoginServiceLdapImpl implements LoginService {
@Override
public ResponseEntity<Map<String, Object>> login(Map<String, String> map) {
- String ntAccount = map.get("username");
+ String username = map.get("username");
String password = map.get("password");
- String searchFilter = searchPattern.replace("{0}", ntAccount);
+
+ // use separate bind credentials, if bindDN is provided
+ String bindAccount = StringUtils.isEmpty(this.bindDN) ? username : this.bindDN;
+ String bindPassword = StringUtils.isEmpty(this.bindDN) ? password : this.bindPassword;
+ String searchFilter = searchPattern.replace("{0}", username);
+ LdapContext ctx = null;
try {
- LdapContext ctx = getContextInstance(ntAccount, password);
+ ctx = getContextInstance(toPrincipal(bindAccount), bindPassword);
+
NamingEnumeration<SearchResult> results = ctx.search(searchBase,
searchFilter, searchControls);
- String fullName = getFullName(results, ntAccount);
+ SearchResult userObject = getSingleUser(results);
+
+ // verify password if different bind user is used
+ if (!StringUtils.equals(username, bindAccount)) {
+ String userDN = getAttributeValue(userObject, "distinguishedName", toPrincipal(username));
+ checkPassword(userDN, password);
+ }
+
Map<String, Object> message = new HashMap<>();
- message.put("ntAccount", ntAccount);
- message.put("fullName", fullName);
+ message.put("ntAccount", username);
+ message.put("fullName", getFullName(userObject, username));
message.put("status", 0);
return new ResponseEntity<>(message, HttpStatus.OK);
- } catch (NamingException e) {
- LOGGER.warn("User {} failed to login with LDAP auth. {}", ntAccount,
+ } catch (AuthenticationException e) {
+ LOGGER.warn("User {} failed to login with LDAP auth. {}", username,
e.getMessage());
+ } catch (NamingException e) {
+ LOGGER.warn(String.format("User %s failed to login with LDAP auth.", username), e);
+ } finally {
+ if (ctx != null) {
+ try {
+ ctx.close();
+ } catch (NamingException e) {
+ LOGGER.debug("Failed to close LDAP context", e);
+ }
+ }
}
return null;
}
- private String getFullName(NamingEnumeration<SearchResult> results,
- String ntAccount) {
- String fullName = ntAccount;
+ private void checkPassword(String name, String password) throws NamingException {
+ getContextInstance(name, password).close();
+ }
+
+ private SearchResult getSingleUser(NamingEnumeration<SearchResult> results) throws NamingException {
+ if (!results.hasMoreElements()) {
+ throw new AuthenticationException("User does not exist or not allowed by search string");
+ }
+ SearchResult result = results.nextElement();
+ if (results.hasMoreElements()) {
+ SearchResult second = results.nextElement();
+ throw new NamingException(String.format("Ambiguous search, found two users: %s, %s",
+ result.getNameInNamespace(), second.getNameInNamespace()));
+ }
+ return result;
+ }
+
+ private String getAttributeValue(SearchResult searchResult, String key, String defaultValue) throws NamingException {
+ Attributes attrs = searchResult.getAttributes();
+ if (attrs == null) {
+ return defaultValue;
+ }
+ Attribute attrObj = attrs.get(key);
+ if (attrObj == null) {
+ return defaultValue;
+ }
try {
- while (results.hasMoreElements()) {
- SearchResult searchResult = results.nextElement();
- Attributes attrs = searchResult.getAttributes();
- if (attrs != null && attrs.get("cn") != null) {
- String cnName = (String) attrs.get("cn").get();
- if (cnName.indexOf("(") > 0) {
- fullName = cnName.substring(0, cnName.indexOf("("));
- }
- }
+ return (String) attrObj.get();
+ } catch (NoSuchElementException e) {
+ return defaultValue;
+ }
+ }
+
+ private String getFullName(SearchResult searchResult, String ntAccount) {
+ try {
+ String cnName = getAttributeValue(searchResult, "cn", null);
+ if (cnName.indexOf("(") > 0) {
+ return cnName.substring(0, cnName.indexOf("("));
+ } else {
+ // old behavior ignores CNs without "("
+ return ntAccount;
}
} catch (NamingException e) {
LOGGER.warn("User {} successfully login with LDAP auth, " +
"but failed to get full name.", ntAccount);
+ return ntAccount;
+ }
+ }
+
+ private String toPrincipal(String ntAccount) {
+ if (ntAccount.toUpperCase().startsWith("CN=")) {
+ return ntAccount;
+ } else {
+ return ntAccount + email;
}
- return fullName;
}
- private LdapContext getContextInstance(String ntAccount, String password)
+ private LdapContext getContextInstance(String principal, String password)
throws NamingException {
Hashtable<String, String> ht = new Hashtable<>();
ht.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_FACTORY);
ht.put(Context.PROVIDER_URL, url);
- ht.put(Context.SECURITY_PRINCIPAL, ntAccount + email);
+ ht.put(Context.SECURITY_PRINCIPAL, principal);
ht.put(Context.SECURITY_CREDENTIALS, password);
+ if (url.startsWith("ldaps") && sslSkipVerify) {
+ ht.put("java.naming.ldap.factory.socket", SelfSignedSocketFactory.class.getName());
+ }
return new InitialLdapContext(ht, null);
}
}
http://git-wip-us.apache.org/repos/asf/incubator-griffin/blob/73a7d84b/service/src/main/java/org/apache/griffin/core/login/ldap/SelfSignedSocketFactory.java
----------------------------------------------------------------------
diff --git a/service/src/main/java/org/apache/griffin/core/login/ldap/SelfSignedSocketFactory.java b/service/src/main/java/org/apache/griffin/core/login/ldap/SelfSignedSocketFactory.java
new file mode 100644
index 0000000..2daa2c3
--- /dev/null
+++ b/service/src/main/java/org/apache/griffin/core/login/ldap/SelfSignedSocketFactory.java
@@ -0,0 +1,102 @@
+/*
+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.griffin.core.login.ldap;
+
+import org.apache.griffin.core.exception.GriffinException;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * SocketFactory ignoring insecure (self-signed, expired) certificates.
+ *
+ * Maintains internal {@code SSLSocketFactory} configured with {@code NoopTrustManager}.
+ * All SocketFactory methods are proxied to internal SSLSocketFactory instance.
+ * Accepts all client and server certificates, from any issuers.
+ */
+public class SelfSignedSocketFactory extends SocketFactory {
+ private SSLSocketFactory sf;
+
+ private SelfSignedSocketFactory() throws Exception {
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(null, new TrustManager[]{new NoopTrustManager()}, null);
+ sf = ctx.getSocketFactory();
+ }
+
+ /**
+ * Part of SocketFactory contract, used by javax.net internals to create new instance.
+ */
+ public static SocketFactory getDefault() {
+ try {
+ return new SelfSignedSocketFactory();
+ } catch (Exception e) {
+ throw new GriffinException.ServiceException("Failed to create socket factory", e);
+ }
+ }
+
+ /**
+ * Insecure trust manager accepting any client and server certificates.
+ */
+ public static class NoopTrustManager implements X509TrustManager {
+ public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {
+ }
+
+ public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return new java.security.cert.X509Certificate[0];
+ }
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ return sf.createSocket();
+ }
+
+ @Override
+ public Socket createSocket(String s, int i) throws IOException, UnknownHostException {
+ return sf.createSocket(s, i);
+ }
+
+ @Override
+ public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException, UnknownHostException {
+ return sf.createSocket(s, i, inetAddress, i1);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress inetAddress, int i) throws IOException {
+ return sf.createSocket(inetAddress, i);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException {
+ return sf.createSocket(inetAddress, i, inetAddress1, i1);
+ }
+}