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 <chemika...@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 <chemika...@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); + } +}