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