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