001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.activemq.jaas;
018    
019    import java.io.IOException;
020    import java.security.Principal;
021    import java.text.MessageFormat;
022    import java.util.ArrayList;
023    import java.util.HashSet;
024    import java.util.Hashtable;
025    import java.util.Iterator;
026    import java.util.Map;
027    import java.util.Set;
028    
029    import javax.naming.AuthenticationException;
030    import javax.naming.CommunicationException;
031    import javax.naming.Context;
032    import javax.naming.Name;
033    import javax.naming.NameParser;
034    import javax.naming.NamingEnumeration;
035    import javax.naming.NamingException;
036    import javax.naming.directory.Attribute;
037    import javax.naming.directory.Attributes;
038    import javax.naming.directory.DirContext;
039    import javax.naming.directory.InitialDirContext;
040    import javax.naming.directory.SearchControls;
041    import javax.naming.directory.SearchResult;
042    import javax.security.auth.Subject;
043    import javax.security.auth.callback.Callback;
044    import javax.security.auth.callback.CallbackHandler;
045    import javax.security.auth.callback.NameCallback;
046    import javax.security.auth.callback.PasswordCallback;
047    import javax.security.auth.callback.UnsupportedCallbackException;
048    import javax.security.auth.login.FailedLoginException;
049    import javax.security.auth.login.LoginException;
050    import javax.security.auth.spi.LoginModule;
051    
052    import org.apache.commons.logging.Log;
053    import org.apache.commons.logging.LogFactory;
054    
055    /**
056     * @version $Rev: $ $Date: $
057     */
058    public class LDAPLoginModule implements LoginModule {
059    
060        private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory";
061        private static final String CONNECTION_URL = "connectionURL";
062        private static final String CONNECTION_USERNAME = "connectionUsername";
063        private static final String CONNECTION_PASSWORD = "connectionPassword";
064        private static final String CONNECTION_PROTOCOL = "connectionProtocol";
065        private static final String AUTHENTICATION = "authentication";
066        private static final String USER_BASE = "userBase";
067        private static final String USER_SEARCH_MATCHING = "userSearchMatching";
068        private static final String USER_SEARCH_SUBTREE = "userSearchSubtree";
069        private static final String ROLE_BASE = "roleBase";
070        private static final String ROLE_NAME = "roleName";
071        private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching";
072        private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree";
073        private static final String USER_ROLE_NAME = "userRoleName";
074    
075        private static Log log = LogFactory.getLog(LDAPLoginModule.class);
076    
077        protected DirContext context;
078    
079        private Subject subject;
080        private CallbackHandler handler;  
081        private LDAPLoginProperty [] config;
082        private String username;
083        private Set<GroupPrincipal> groups = new HashSet<GroupPrincipal>();
084    
085        public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
086            this.subject = subject;
087            this.handler = callbackHandler;
088            
089            config = new LDAPLoginProperty [] {
090                            new LDAPLoginProperty (INITIAL_CONTEXT_FACTORY, (String)options.get(INITIAL_CONTEXT_FACTORY)),
091                            new LDAPLoginProperty (CONNECTION_URL, (String)options.get(CONNECTION_URL)),
092                            new LDAPLoginProperty (CONNECTION_USERNAME, (String)options.get(CONNECTION_USERNAME)),
093                            new LDAPLoginProperty (CONNECTION_PASSWORD, (String)options.get(CONNECTION_PASSWORD)),
094                            new LDAPLoginProperty (CONNECTION_PROTOCOL, (String)options.get(CONNECTION_PROTOCOL)),
095                            new LDAPLoginProperty (AUTHENTICATION, (String)options.get(AUTHENTICATION)),
096                            new LDAPLoginProperty (USER_BASE, (String)options.get(USER_BASE)),
097                            new LDAPLoginProperty (USER_SEARCH_MATCHING, (String)options.get(USER_SEARCH_MATCHING)),
098                            new LDAPLoginProperty (USER_SEARCH_SUBTREE, (String)options.get(USER_SEARCH_SUBTREE)),
099                            new LDAPLoginProperty (ROLE_BASE, (String)options.get(ROLE_BASE)),
100                            new LDAPLoginProperty (ROLE_NAME, (String)options.get(ROLE_NAME)),
101                            new LDAPLoginProperty (ROLE_SEARCH_MATCHING, (String)options.get(ROLE_SEARCH_MATCHING)),
102                            new LDAPLoginProperty (ROLE_SEARCH_SUBTREE, (String)options.get(ROLE_SEARCH_SUBTREE)),
103                            new LDAPLoginProperty (USER_ROLE_NAME, (String)options.get(USER_ROLE_NAME)),
104                            };
105        }
106    
107        public boolean login() throws LoginException {
108    
109            Callback[] callbacks = new Callback[2];
110    
111            callbacks[0] = new NameCallback("User name");
112            callbacks[1] = new PasswordCallback("Password", false);
113            try {
114                handler.handle(callbacks);
115            } catch (IOException ioe) {
116                throw (LoginException)new LoginException().initCause(ioe);
117            } catch (UnsupportedCallbackException uce) {
118                throw (LoginException)new LoginException().initCause(uce);
119            }
120            
121            String password;
122            
123            username = ((NameCallback)callbacks[0]).getName();
124            if (username == null)
125                    return false;
126                    
127            if (((PasswordCallback)callbacks[1]).getPassword() != null)
128                    password = new String(((PasswordCallback)callbacks[1]).getPassword());
129            else
130                    password="";
131    
132            try {
133                boolean result = authenticate(username, password);
134                if (!result) {
135                    throw new FailedLoginException();
136                } else {
137                    return true;
138                }
139            } catch (Exception e) {
140                throw (LoginException)new LoginException("LDAP Error").initCause(e);
141            }
142        }
143    
144        public boolean logout() throws LoginException {
145            username = null;
146            return true;
147        }
148    
149        public boolean commit() throws LoginException {
150            Set<Principal> principals = subject.getPrincipals();
151            principals.add(new UserPrincipal(username));
152            Iterator<GroupPrincipal> iter = groups.iterator();
153            while (iter.hasNext()) {
154                principals.add(iter.next());
155            }
156            return true;
157        }
158    
159        public boolean abort() throws LoginException {
160            username = null;
161            return true;
162        }
163    
164        protected void close(DirContext context) {
165            try {
166                context.close();
167            } catch (Exception e) {
168                log.error(e);
169            }
170        }
171    
172        protected boolean authenticate(String username, String password) throws Exception {
173    
174            MessageFormat userSearchMatchingFormat;
175            boolean userSearchSubtreeBool;
176            
177            DirContext context = null;
178            context = open();
179            
180            if (!isLoginPropertySet(USER_SEARCH_MATCHING))
181                    return false;
182    
183            userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING));
184            userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue();
185    
186            try {
187    
188                String filter = userSearchMatchingFormat.format(new String[] {
189                    username
190                });
191                SearchControls constraints = new SearchControls();
192                if (userSearchSubtreeBool) {
193                    constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
194                } else {
195                    constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
196                }
197    
198                // setup attributes
199                ArrayList<String> list = new ArrayList<String>();
200                if (isLoginPropertySet(USER_ROLE_NAME)) {
201                    list.add(getLDAPPropertyValue(USER_ROLE_NAME));
202                }
203                String[] attribs = new String[list.size()];
204                list.toArray(attribs);
205                constraints.setReturningAttributes(attribs);
206    
207                NamingEnumeration results = context.search(getLDAPPropertyValue(USER_BASE), filter, constraints);
208    
209                if (results == null || !results.hasMore()) {
210                    return false;
211                }
212    
213                SearchResult result = (SearchResult)results.next();
214    
215                if (results.hasMore()) {
216                    // ignore for now
217                }
218                NameParser parser = context.getNameParser("");
219                Name contextName = parser.parse(context.getNameInNamespace());
220                Name baseName = parser.parse(getLDAPPropertyValue(USER_BASE));
221                Name entryName = parser.parse(result.getName());
222                Name name = contextName.addAll(baseName);
223                name = name.addAll(entryName);
224                String dn = name.toString();
225    
226                Attributes attrs = result.getAttributes();
227                if (attrs == null) {
228                    return false;
229                }
230                ArrayList<String> roles = null;
231                if (isLoginPropertySet(USER_ROLE_NAME)) {
232                    roles = addAttributeValues(getLDAPPropertyValue(USER_ROLE_NAME), attrs, roles);
233                }
234    
235                // check the credentials by binding to server
236                if (bindUser(context, dn, password)) {
237                    // if authenticated add more roles
238                    roles = getRoles(context, dn, username, roles);
239                    for (int i = 0; i < roles.size(); i++) {
240                        groups.add(new GroupPrincipal(roles.get(i)));
241                    }
242                } else {
243                    return false;
244                }
245            } catch (CommunicationException e) {
246    
247            } catch (NamingException e) {
248                if (context != null) {
249                    close(context);
250                }
251                return false;
252            }
253    
254            return true;
255        }
256    
257        protected ArrayList<String> getRoles(DirContext context, String dn, String username, ArrayList<String> currentRoles) throws NamingException {
258            ArrayList<String> list = currentRoles;
259            MessageFormat roleSearchMatchingFormat;
260            boolean roleSearchSubtreeBool;
261            roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ROLE_SEARCH_MATCHING));
262            roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue();
263            
264            if (list == null) {
265                list = new ArrayList<String>();
266            }
267            if (!isLoginPropertySet(ROLE_NAME)) {
268                return list;
269            }
270            String filter = roleSearchMatchingFormat.format(new String[] {
271                doRFC2254Encoding(dn), username
272            });
273    
274            SearchControls constraints = new SearchControls();
275            if (roleSearchSubtreeBool) {
276                constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
277            } else {
278                constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
279            }
280            NamingEnumeration results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints);
281            while (results.hasMore()) {
282                SearchResult result = (SearchResult)results.next();
283                Attributes attrs = result.getAttributes();
284                if (attrs == null) {
285                    continue;
286                }
287                list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list);
288            }
289            return list;
290    
291        }
292    
293        protected String doRFC2254Encoding(String inputString) {
294            StringBuffer buf = new StringBuffer(inputString.length());
295            for (int i = 0; i < inputString.length(); i++) {
296                char c = inputString.charAt(i);
297                switch (c) {
298                case '\\':
299                    buf.append("\\5c");
300                    break;
301                case '*':
302                    buf.append("\\2a");
303                    break;
304                case '(':
305                    buf.append("\\28");
306                    break;
307                case ')':
308                    buf.append("\\29");
309                    break;
310                case '\0':
311                    buf.append("\\00");
312                    break;
313                default:
314                    buf.append(c);
315                    break;
316                }
317            }
318            return buf.toString();
319        }
320    
321        protected boolean bindUser(DirContext context, String dn, String password) throws NamingException {
322            boolean isValid = false;
323    
324            context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
325            context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
326            try {
327                context.getAttributes("", null);
328                isValid = true;
329            } catch (AuthenticationException e) {
330                isValid = false;
331                log.debug("Authentication failed for dn=" + dn);
332            }
333    
334            if (isLoginPropertySet(CONNECTION_USERNAME)) {
335                context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME));
336            } else {
337                context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
338            }
339    
340            if (isLoginPropertySet(CONNECTION_PASSWORD)) {
341                context.addToEnvironment(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD));
342            } else {
343                context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
344            }
345    
346            return isValid;
347        }
348    
349        private ArrayList<String> addAttributeValues(String attrId, Attributes attrs, ArrayList<String> values) throws NamingException {
350    
351            if (attrId == null || attrs == null) {
352                return values;
353            }
354            if (values == null) {
355                values = new ArrayList<String>();
356            }
357            Attribute attr = attrs.get(attrId);
358            if (attr == null) {
359                return values;
360            }
361            NamingEnumeration e = attr.getAll();
362            while (e.hasMore()) {
363                String value = (String)e.next();
364                values.add(value);
365            }
366            return values;
367        }
368    
369        protected DirContext open() throws NamingException {
370            try {
371                Hashtable<String, String> env = new Hashtable<String, String>();
372                env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(INITIAL_CONTEXT_FACTORY));
373                if (isLoginPropertySet(CONNECTION_USERNAME)) {
374                    env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME));
375                }
376                if (isLoginPropertySet(CONNECTION_PASSWORD)) {
377                    env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD));
378                }
379                env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL));
380                env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL));
381                env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION));
382                context = new InitialDirContext(env);
383    
384            } catch (NamingException e) {
385                log.error(e);
386                throw e;
387            }
388            return context;
389        }
390        
391        private String getLDAPPropertyValue (String propertyName){
392            for (int i=0; i < config.length; i++ )
393                    if (config[i].getPropertyName() == propertyName)
394                            return config[i].getPropertyValue();
395            return null;
396        }
397        
398        private boolean isLoginPropertySet(String propertyName) {
399            for (int i=0; i < config.length; i++ ) {
400                    if (config[i].getPropertyName() == propertyName && config[i].getPropertyValue() != null)
401                                    return true;
402            }
403            return false;
404        }
405    
406    }