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 */
017package org.apache.activemq.jaas;
018
019import java.io.IOException;
020import java.security.Principal;
021import java.text.MessageFormat;
022import java.util.ArrayList;
023import java.util.HashSet;
024import java.util.Hashtable;
025import java.util.Iterator;
026import java.util.Map;
027import java.util.Set;
028
029import javax.naming.AuthenticationException;
030import javax.naming.CommunicationException;
031import javax.naming.Context;
032import javax.naming.Name;
033import javax.naming.NameParser;
034import javax.naming.NamingEnumeration;
035import javax.naming.NamingException;
036import javax.naming.directory.Attribute;
037import javax.naming.directory.Attributes;
038import javax.naming.directory.DirContext;
039import javax.naming.directory.InitialDirContext;
040import javax.naming.directory.SearchControls;
041import javax.naming.directory.SearchResult;
042import javax.security.auth.Subject;
043import javax.security.auth.callback.Callback;
044import javax.security.auth.callback.CallbackHandler;
045import javax.security.auth.callback.NameCallback;
046import javax.security.auth.callback.PasswordCallback;
047import javax.security.auth.callback.UnsupportedCallbackException;
048import javax.security.auth.login.FailedLoginException;
049import javax.security.auth.login.LoginException;
050import javax.security.auth.spi.LoginModule;
051
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054
055/**
056 * @version $Rev: $ $Date: $
057 */
058public 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 Logger log = LoggerFactory.getLogger(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.toString());
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.toString());
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}