|
|
Line 1: |
Line 1: |
− | {{ScoutPage|cat=HowTo 4.0}}
| + | The Scout documentation has been moved to https://eclipsescout.github.io/. |
− | | + | |
− | The [[Scout/Concepts/Security|Security Concept]] page is rather brief on the use of the LDAPSecurityFilter. This how-to describes how to use it and offers an alternate security filter. | + | |
− | | + | |
− | = LDAPSecurityFilter =
| + | |
− | | + | |
− | In order to use the LDAPSecurityFilter, the corresponding filter class must be added to the server's plugin.xml in the org.eclipse.scout.rt.server.commons.filters extension point
| + | |
− | | + | |
− | <source lang="xml">
| + | |
− | <filter
| + | |
− | aliases="/process"
| + | |
− | class="org.eclipse.scout.http.servletfilter.security.LDAPSecurityFilter"
| + | |
− | ranking="50">
| + | |
− | </filter>
| + | |
− | </source>
| + | |
− | | + | |
− | Next, the necessary configuration lines must be added to the server's config.ini file:
| + | |
− | <pre>org.eclipse.scout.http.servletfilter.security.LDAPSecurityFilter#active=true
| + | |
− | org.eclipse.scout.http.servletfilter.security.LDAPSecurityFilter#failover=false
| + | |
− | org.eclipse.scout.http.servletfilter.security.LDAPSecurityFilter#realm=developmnet
| + | |
− | org.eclipse.scout.http.servletfilter.security.LDAPSecurityFilter#ldapServer=ldap://ldap.company.com:389
| + | |
− | org.eclipse.scout.http.servletfilter.security.LDAPSecurityFilter#ldapBaseDN=dc=company,dc=com
| + | |
− | org.eclipse.scout.http.servletfilter.security.LDAPSecurityFilter#lDAPgroupDN=???
| + | |
− | org.eclipse.scout.http.servletfilter.security.LDAPSecurityFilter#lDAPgroupAttributeId=cn
| + | |
− | </pre>
| + | |
− | '''''Note:''' It is not yet quite clear what value is needed for the lDAPgroupDN parameter.''
| + | |
− | | + | |
− | The LDAPSecurityFilter connects to the LDAP server anonymously. This may well work for many servers, but the some LDAP servers only return empty responses when queried anonymously.
| + | |
− | | + | |
− | = LDAPExSecurityFilter =
| + | |
− | | + | |
− | To work around this problem, an extended LDAPSecurityFilter which connects to the LDAP server using a pre-configured system user can be used.
| + | |
− | <pre>package org.eclipse.minicrm.server.services.custom.security;
| + | |
− | | + | |
− | import java.io.IOException;
| + | |
− | import java.util.Enumeration;
| + | |
− | import java.util.Hashtable;
| + | |
− | import javax.naming.Context;
| + | |
− | import javax.naming.NamingEnumeration;
| + | |
− | import javax.naming.NamingException;
| + | |
− | import javax.naming.directory.DirContext;
| + | |
− | import javax.naming.directory.InitialDirContext;
| + | |
− | import javax.naming.directory.SearchControls;
| + | |
− | import javax.naming.directory.SearchResult;
| + | |
− | import javax.servlet.FilterConfig;
| + | |
− | import javax.servlet.ServletException;
| + | |
− | import javax.servlet.http.HttpServletRequest;
| + | |
− | import javax.servlet.http.HttpServletResponse;
| + | |
− | import org.eclipse.scout.commons.Base64Utility;
| + | |
− | import org.eclipse.scout.commons.logger.IScoutLogger;
| + | |
− | import org.eclipse.scout.commons.logger.ScoutLogManager;
| + | |
− | import org.eclipse.scout.commons.security.SimplePrincipal;
| + | |
− | import org.eclipse.scout.http.servletfilter.FilterConfigInjection;
| + | |
− | import org.eclipse.scout.http.servletfilter.security.AbstractChainableSecurityFilter;
| + | |
− | import org.eclipse.scout.http.servletfilter.security.PrincipalHolder;
| + | |
− | /** | + | |
− | * LDAPSecurityFilter The following properties can be set in the config.ini file:
| + | |
− | *
| + | |
− | * <fully qualified name of class>#active=true/false might be set in the extension point
| + | |
− | * <fully qualified name of class>#realm=abcde required
| + | |
− | * <fully qualified name of class>#failover=true/false default false
| + | |
− | * <fully qualified name of class>#ldapDirectory=[e.g. ldap://100.100.29.4:389] required
| + | |
− | * <fully qualified name of class>#ldapBaseDN=[e.g. dc=bridgesolutions,dc=net] required
| + | |
− | * <fully qualified name of class>#ldapSearchFilter=[e.g. (uid=%s)] required
| + | |
− | * <fully qualified name of class>#ldapSearchBindPassword=[e.g. changeme]
| + | |
− | * <fully qualified name of class>#m_searchBindDN=[e.g. cn=readonly,dc=bridgesolutions,dc=net]
| + | |
− | *
| + | |
− | * based on org.eclipse.scout.http.servletfilter.security.LDAPSecurityFilter
| + | |
− | */
| + | |
− | | + | |
− | public class LDAPExSecurityFilter extends AbstractChainableSecurityFilter {
| + | |
− | private static final IScoutLogger LOG = ScoutLogManager.getLogger(LDAPExSecurityFilter.class);
| + | |
− | public static final String PROP_BASIC_ATTEMPT = "LDAPExSecurityFilter.basicAttempt";
| + | |
− | | + | |
− | private String m_directory;
| + | |
− | private String m_baseDn;
| + | |
− | private String m_searchFilter;
| + | |
− | private String m_searchBindPassword;
| + | |
− | private String m_searchBindDN;
| + | |
− | | + | |
− | @Override
| + | |
− | public void init(FilterConfig config0) throws ServletException {
| + | |
− | super.init(config0);
| + | |
− | FilterConfigInjection.FilterConfig config = new FilterConfigInjection(config0, getClass()).getAnyConfig();
| + | |
− | m_directory = getParam(config, "ldapDirectory", false);
| + | |
− | m_baseDn = getParam(config, "ldapBaseDN", false);
| + | |
− | m_searchFilter = getParam(config, "ldapSearchFilter", false);
| + | |
− | m_searchBindPassword = getParam(config, "ldapSearchBindPassword", true);
| + | |
− | m_searchBindDN = getParam(config, "ldapSearchBindDN", true);
| + | |
− | }
| + | |
− | | + | |
− | protected String getParam(FilterConfig filterConfig, String paramName, boolean nullAllowed) throws ServletException {
| + | |
− | String paramValue = filterConfig.getInitParameter(paramName);
| + | |
− | boolean exists = false;
| + | |
− | if (paramValue == null && nullAllowed) { // check if parameter exists
| + | |
− | Enumeration initParameterNames = filterConfig.getInitParameterNames();
| + | |
− | while (initParameterNames.hasMoreElements() && exists == false) {
| + | |
− | String object = (String) initParameterNames.nextElement();
| + | |
− | exists = object.equals(paramName);
| + | |
− | }
| + | |
− | }
| + | |
− | if (paramValue == null && !exists) {
| + | |
− | throw new ServletException("Missing init-param with name '" + paramName + "'.");
| + | |
− | }
| + | |
− | return paramValue;
| + | |
− | }
| + | |
− | | + | |
− | @Override
| + | |
− | protected int negotiate(HttpServletRequest req, HttpServletResponse resp, PrincipalHolder holder) throws IOException, ServletException {
| + | |
− | String h = req.getHeader("Authorization");
| + | |
− | if (h != null && h.matches("Basic .*")) {
| + | |
− | String[] a = new String(Base64Utility.decode(h.substring(6)), "ISO-8859-1").split(":", 2);
| + | |
− | String user = a[0].toLowerCase();
| + | |
− | String pass = a[1];
| + | |
− | if (user != null && pass != null) {
| + | |
− | if (login(user, pass, m_directory, m_baseDn, m_searchBindDN, m_searchBindPassword, m_searchFilter)) {
| + | |
− | // success
| + | |
− | holder.setPrincipal(new SimplePrincipal(user));
| + | |
− | return STATUS_CONTINUE_WITH_PRINCIPAL;
| + | |
− | }
| + | |
− | }
| + | |
− | }
| + | |
− | int attempts = getBasicAttempt(req);
| + | |
− | if (attempts > 2) {
| + | |
− | return STATUS_CONTINUE_CHAIN;
| + | |
− | }
| + | |
− | else {
| + | |
− | setBasicAttept(req, attempts + 1);
| + | |
− | resp.setHeader("WWW-Authenticate", "Basic realm=\"" + getRealm() + "\"");
| + | |
− | return STATUS_CONTINUE_CHAIN;
| + | |
− | }
| + | |
− | }
| + | |
− | | + | |
− | private int getBasicAttempt(HttpServletRequest req) {
| + | |
− | int basicAtttempt = 0;
| + | |
− | Object attribute = req.getSession().getAttribute(PROP_BASIC_ATTEMPT);
| + | |
− | if (attribute instanceof Integer) {
| + | |
− | basicAtttempt = ((Integer) attribute).intValue();
| + | |
− | }
| + | |
− | return basicAtttempt;
| + | |
− | }
| + | |
− | | + | |
− | private void setBasicAttept(HttpServletRequest req, int attempts) {
| + | |
− | req.getSession().setAttribute(PROP_BASIC_ATTEMPT, attempts);
| + | |
− | }
| + | |
− | | + | |
− | @SuppressWarnings("unchecked")
| + | |
− | protected boolean login(String username, String userPassword, String directory, String baseDN, String searchBindDN, String searchBindPassword, String searchFilter) throws ServletException {
| + | |
− | boolean isAuthenticated = false;
| + | |
− | | + | |
− | Hashtable env = new Hashtable();
| + | |
− | env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
| + | |
− | env.put(Context.PROVIDER_URL, directory);
| + | |
− | env.put(Context.SECURITY_PRINCIPAL, searchBindDN);
| + | |
− | env.put(Context.SECURITY_CREDENTIALS, searchBindPassword);
| + | |
− | | + | |
− | try {
| + | |
− | DirContext ctx = new InitialDirContext(env);
| + | |
− | SearchControls constraints = new SearchControls();
| + | |
− | constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
| + | |
− | String search = String.format(searchFilter, username);
| + | |
− | | + | |
− | NamingEnumeration<SearchResult> results = ctx.search(baseDN, search, constraints);
| + | |
− | | + | |
− | SearchResult sr = null;
| + | |
− | | + | |
− | if (results != null && results.hasMore()) {
| + | |
− | // Step 2 - simple authentication
| + | |
− | sr = results.next();
| + | |
− | | + | |
− | env.put(Context.SECURITY_AUTHENTICATION, "simple");
| + | |
− | if (sr.isRelative()) {
| + | |
− | env.put(Context.SECURITY_PRINCIPAL, sr.getName() + "," + baseDN);
| + | |
− | }
| + | |
− | else {
| + | |
− | env.put(Context.SECURITY_PRINCIPAL, sr.getName());
| + | |
− | }
| + | |
− | env.put(Context.SECURITY_CREDENTIALS, userPassword);
| + | |
− | | + | |
− | try {
| + | |
− | new InitialDirContext(env);
| + | |
− | isAuthenticated = true;
| + | |
− | }
| + | |
− | catch (NamingException ne) {
| + | |
− | LOG.warn("User '" + username + "' could not be authenticated");
| + | |
− | isAuthenticated = false;
| + | |
− | }
| + | |
− | }
| + | |
− | else {
| + | |
− | LOG.warn("User '" + username + "' does not exist on LDAP");
| + | |
− | isAuthenticated = false;
| + | |
− | }
| + | |
− | }
| + | |
− | catch (NamingException ne) {
| + | |
− | LOG.error("Exception in getting user DN from LDAP: " + ne);
| + | |
− | throw new SecurityException(ne.getMessage(), ne);
| + | |
− | }
| + | |
− | return isAuthenticated;
| + | |
− | }
| + | |
− | }
| + | |
− | </pre>
| + | |
− | It must be registered in the server's plugin.xml file in the org.eclipse.scout.http.servletfilter.filters extension point
| + | |
− | | + | |
− | <filter
| + | |
− | aliases="/process"
| + | |
− | class="org.eclipse.minicrm.server.services.custom.security.LDAPExSecurityFilter"
| + | |
− | ranking="30">
| + | |
− | </filter>
| + | |
− | | + | |
− | It must be configured in the server product's config.ini file:
| + | |
− | <pre>org.eclipse.minicrm.server.services.custom.security.LDAPExSecurityFilter#active=true
| + | |
− | org.eclipse.minicrm.server.services.custom.security.LDAPExSecurityFilter#realm=LDAP
| + | |
− | org.eclipse.minicrm.server.services.custom.security.LDAPExSecurityFilter#ldapDirectory=ldap://ldap.company.com:389
| + | |
− | org.eclipse.minicrm.server.services.custom.security.LDAPExSecurityFilter#ldapBaseDN=dc=company,dc=com
| + | |
− | org.eclipse.minicrm.server.services.custom.security.LDAPExSecurityFilter#ldapSearchFilter=(cn=%s)
| + | |
− | org.eclipse.minicrm.server.services.custom.security.LDAPExSecurityFilter#ldapSearchBindDN=cn=<system user name>,ou=Administrators,dc=company,dc=com
| + | |
− | org.eclipse.minicrm.server.services.custom.security.LDAPExSecurityFilter#ldapSearchBindPassword=<system user password>
| + | |
− | org.eclipse.minicrm.server.services.custom.security.LDAPExSecurityFilter#failover=false
| + | |
− | org.eclipse.minicrm.server.services.custom.security.LDAPExSecurityFilter#silent=true
| + | |
− | </pre>
| + | |
− | <br>
| + | |