1   /*
2    * Copyright 2003 - 2013 The eFaps Team
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Revision:        $Rev$
17   * Last Changed:    $Date$
18   * Last Changed By: $Author$
19   */
20  
21  package org.efaps.jaas.xml;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Map;
29  
30  import javax.security.auth.Subject;
31  import javax.security.auth.callback.Callback;
32  import javax.security.auth.callback.CallbackHandler;
33  import javax.security.auth.callback.NameCallback;
34  import javax.security.auth.callback.PasswordCallback;
35  import javax.security.auth.callback.UnsupportedCallbackException;
36  import javax.security.auth.login.FailedLoginException;
37  import javax.security.auth.login.LoginException;
38  import javax.security.auth.spi.LoginModule;
39  
40  import org.apache.commons.digester.Digester;
41  import org.efaps.jaas.ActionCallback;
42  import org.slf4j.Logger;
43  import org.slf4j.LoggerFactory;
44  import org.xml.sax.SAXException;
45  
46  /***
47   *
48   * @author The eFaps Team
49   * @version $Id$
50   */
51  public class XMLUserLoginModule
52      implements LoginModule
53  {
54      /**
55       * Logging instance used to give logging information of this class.
56       */
57      private static final Logger LOG = LoggerFactory.getLogger(XMLUserLoginModule.class);
58  
59      /**
60       * Stores the mode in which mode the login module is called.
61       */
62      private ActionCallback.Mode mode = ActionCallback.Mode.UNDEFINED;
63  
64      /**
65       * The subject is stored from the initialize method to store all single
66       * entities for the person logging in.
67       */
68      private Subject subject = null;
69  
70      /**
71       *
72       */
73      private CallbackHandler callbackHandler = null;
74  
75      /**
76       * The logged in person principal is stored in this variable. The variable
77       * is set if the user logged in and reset to null if the user is logged
78       * out.
79       */
80      private XMLPersonPrincipal person = null;
81  
82      /**
83       *
84       */
85      private final Map<String, XMLPersonPrincipal> allPersons = new HashMap<String, XMLPersonPrincipal>();
86  
87      /**
88       * Initialize this LoginModule.
89       *
90       * <p> This method is called by the <code>LoginContext</code>
91       * after this <code>LoginModule</code> has been instantiated.
92       * The purpose of this method is to initialize this
93       * <code>LoginModule</code> with the relevant information.
94       * If this <code>LoginModule</code> does not understand
95       * any of the data stored in <code>sharedState</code> or
96       * <code>options</code> parameters, they can be ignored.
97       *
98       * <p>
99       *
100      * @param _subject      the <code>Subject</code> to be authenticated. <p>
101      * @param _callbackHandler a <code>CallbackHandler</code> for communicating
102      *          with the end user (prompting for usernames and
103      *          passwords, for example). <p>
104      * @param _sharedState state shared with other configured LoginModules. <p>
105      * @param _options options specified in the login
106      *          <code>Configuration</code> for this particular
107      *          <code>LoginModule</code>.
108      */
109     @Override
110     public final void initialize(final Subject _subject,
111                                  final CallbackHandler _callbackHandler,
112                                  final Map < String, ? > _sharedState,
113                                  final Map < String, ? > _options)
114     {
115         XMLUserLoginModule.LOG.debug("Init");
116         this.subject = _subject;
117         this.callbackHandler = _callbackHandler;
118         readPersons((String) _options.get("xmlFileName"));
119     }
120 
121     /**
122      * Method to authenticate a <code>Subject</code> (phase 1).
123      *
124      * <p> The implementation of this method authenticates
125      * a <code>Subject</code>.  For example, it may prompt for
126      * <code>Subject</code> information such
127      * as a username and password and then attempt to verify the password.
128      * This method saves the result of the authentication attempt
129      * as private state within the LoginModule.
130      *
131      * <p>
132      *
133      * @exception LoginException if the authentication fails
134      *
135      * @return true if the authentication succeeded, or false if this
136      *          <code>LoginModule</code> should be ignored.
137      */
138     public final boolean login()
139         throws LoginException
140     {
141         boolean ret = false;
142 
143         final Callback[] callbacks = new Callback[3];
144         callbacks[0] = new ActionCallback();
145         callbacks[1] = new NameCallback("Username: ");
146         callbacks[2] = new PasswordCallback("Password: ", false);
147         // Interact with the user to retrieve the username and password
148         String userName = null;
149         String password = null;
150         try {
151             this.callbackHandler.handle(callbacks);
152             this.mode = ((ActionCallback) callbacks[0]).getMode();
153             userName = ((NameCallback) callbacks[1]).getName();
154             if (((PasswordCallback) callbacks[2]).getPassword() != null) {
155                 password = new String(((PasswordCallback) callbacks[2]).getPassword());
156             }
157         } catch (final IOException e) {
158             throw new LoginException(e.toString());
159         } catch (final UnsupportedCallbackException e) {
160             throw new LoginException(e.toString());
161         }
162 
163         if (this.mode == ActionCallback.Mode.ALL_PERSONS) {
164             ret = true;
165         } else if (this.mode == ActionCallback.Mode.PERSON_INFORMATION) {
166             this.person = this.allPersons.get(userName);
167             if (this.person != null) {
168                 if (XMLUserLoginModule.LOG.isDebugEnabled()) {
169                     XMLUserLoginModule.LOG.debug("found '" + this.person + "'");
170                 }
171                 ret = true;
172             }
173         } else {
174             this.person = this.allPersons.get(userName);
175             if (this.person != null) {
176                 if ((password == null)
177                                 || ((password != null)
178                                 && !password.equals(this.person.getPassword()))) {
179 
180                     XMLUserLoginModule.LOG.error("person '" + this.person + "' tried to log in with wrong password");
181                     this.person = null;
182                     throw new FailedLoginException("Username or password is incorrect");
183                 }
184                 if (XMLUserLoginModule.LOG.isDebugEnabled()) {
185                     XMLUserLoginModule.LOG.debug("log in of '" + this.person + "'");
186                 }
187                 this.mode = ActionCallback.Mode.LOGIN;
188                 ret = true;
189             }
190         }
191 
192         return ret;
193     }
194 
195     /**
196      * Method to commit the authentication process (phase 2).
197      *
198      * <p> This method is called if the LoginContext's
199      * overall authentication succeeded
200      * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
201      * succeeded).
202      *
203      * <p> If this LoginModule's own authentication attempt
204      * succeeded (checked by retrieving the private state saved by the
205      * <code>login</code> method), then this method associates relevant
206      * Principals and Credentials with the <code>Subject</code> located in the
207      * <code>LoginModule</code>.  If this LoginModule's own
208      * authentication attempted failed, then this method removes/destroys
209      * any state that was originally saved.
210      *
211      * <p>
212      *
213      * @exception LoginException if the commit fails
214      *
215      * @return true if this method succeeded, or false if this
216      *          <code>LoginModule</code> should be ignored.
217      */
218     public final boolean commit()
219         throws LoginException
220     {
221         boolean ret = false;
222 
223         if (this.mode == ActionCallback.Mode.ALL_PERSONS)  {
224             for (final XMLPersonPrincipal personTmp : this.allPersons.values())  {
225                 if (!this.subject.getPrincipals().contains(personTmp))  {
226                     if (XMLUserLoginModule.LOG.isDebugEnabled())  {
227                         XMLUserLoginModule.LOG.debug("commit person '" + personTmp + "'");
228                     }
229                     this.subject.getPrincipals().add(personTmp);
230                 }
231             }
232             ret = true;
233         } else if (this.person != null)  {
234             if (XMLUserLoginModule.LOG.isDebugEnabled())  {
235                 XMLUserLoginModule.LOG.debug("commit of '" + this.person + "'");
236             }
237             if (!this.subject.getPrincipals().contains(this.person))  {
238                 this.subject.getPrincipals().add(this.person);
239                 for (final XMLRolePrincipal principal : this.person.getRoles())  {
240                     this.subject.getPrincipals().add(principal);
241                 }
242                 for (final XMLGroupPrincipal principal : this.person.getGroups())  {
243                     this.subject.getPrincipals().add(principal);
244                 }
245             }
246             ret = true;
247         }
248 
249         return ret;
250     }
251 
252     /**
253      * Method to abort the authentication process (phase 2).
254      *
255      * <p> This method is called if the LoginContext's
256      * overall authentication failed.
257      * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
258      * did not succeed).
259      *
260      * <p> If this LoginModule's own authentication attempt
261      * succeeded (checked by retrieving the private state saved by the
262      * <code>login</code> method), then this method cleans up any state
263      * that was originally saved.
264      *
265      * <p>
266      *
267      * @exception LoginException if the abort fails
268      *
269      * @return true if this method succeeded, or false if this
270      *          <code>LoginModule</code> should be ignored.
271      */
272     public final boolean abort()
273         throws LoginException
274     {
275         boolean ret = false;
276 
277         if (this.person != null)  {
278             if (XMLUserLoginModule.LOG.isDebugEnabled())  {
279                 XMLUserLoginModule.LOG.debug("abort of " + this.person);
280             }
281             this.subject.getPrincipals().remove(this.person);
282             for (final XMLRolePrincipal principal : this.person.getRoles())  {
283                 this.subject.getPrincipals().remove(principal);
284             }
285             for (final XMLGroupPrincipal principal : this.person.getGroups())  {
286                 this.subject.getPrincipals().remove(principal);
287             }
288             this.person = null;
289             ret = true;
290         }
291         return ret;
292     }
293 
294     /**
295      * The method logs out a Subject. All principals (the person and the related
296      * roles and groups) are removed from the subject in {@link #subject}.
297      *
298      * @return <i>true</i> if this login module is used to authentificate the
299      *         current user, otherwise <i>false</i>
300      */
301     public final boolean logout()
302     {
303         boolean ret = false;
304 
305         if (this.person != null)  {
306             if (XMLUserLoginModule.LOG.isDebugEnabled())  {
307                 XMLUserLoginModule.LOG.debug("logout of " + this.person);
308             }
309             this.subject.getPrincipals().remove(this.person);
310             for (final XMLRolePrincipal principal : this.person.getRoles())  {
311                 this.subject.getPrincipals().remove(principal);
312             }
313             for (final XMLGroupPrincipal principal : this.person.getGroups())  {
314                 this.subject.getPrincipals().remove(principal);
315             }
316             this.person = null;
317             ret = true;
318         }
319         return ret;
320     }
321 
322     /**
323      * The name of the XML is store in this instance variable. The XML file
324      * holds all allowed persons and their related roles and groups.
325      *
326      * @param _fileName name of the XML file with the user data
327      */
328     @SuppressWarnings("unchecked")
329     private void readPersons(final String _fileName)
330     {
331         try  {
332             final File file = new File(_fileName);
333 
334             final Digester digester = new Digester();
335             digester.setValidating(false);
336             digester.addObjectCreate("persons", ArrayList.class);
337             digester.addObjectCreate("persons/person", XMLPersonPrincipal.class);
338             digester.addSetNext("persons/person", "add");
339 
340             digester.addCallMethod("persons/person/name", "setName", 1);
341             digester.addCallParam("persons/person/name", 0);
342 
343             digester.addCallMethod("persons/person/password", "setPassword", 1);
344             digester.addCallParam("persons/person/password", 0);
345 
346             digester.addCallMethod("persons/person/firstName", "setFirstName", 1);
347             digester.addCallParam("persons/person/firstName", 0);
348 
349             digester.addCallMethod("persons/person/lastName", "setLastName", 1);
350             digester.addCallParam("persons/person/lastName", 0);
351 
352             digester.addCallMethod("persons/person/email", "setEmail", 1);
353             digester.addCallParam("persons/person/email", 0);
354 
355             digester.addCallMethod("persons/person/organisation", "setOrganisation", 1);
356             digester.addCallParam("persons/person/organisation", 0);
357 
358             digester.addCallMethod("persons/person/url", "setUrl", 1);
359             digester.addCallParam("persons/person/url", 0);
360 
361             digester.addCallMethod("persons/person/phone", "setPhone", 1);
362             digester.addCallParam("persons/person/phone", 0);
363 
364             digester.addCallMethod("persons/person/mobile", "setMobile", 1);
365             digester.addCallParam("persons/person/mobile", 0);
366 
367             digester.addCallMethod("persons/person/fax", "setFax", 1);
368             digester.addCallParam("persons/person/fax", 0);
369 
370             digester.addCallMethod("persons/person/role", "addRole", 1);
371             digester.addCallParam("persons/person/role", 0);
372 
373             digester.addCallMethod("persons/person/group", "addGroup", 1);
374             digester.addCallParam("persons/person/group", 0);
375 
376             final List<XMLPersonPrincipal> personList = (List<XMLPersonPrincipal>) digester.parse(file);
377             for (final XMLPersonPrincipal personTmp : personList)  {
378                 this.allPersons.put(personTmp.getName(), personTmp);
379             }
380         } catch (final IOException e)  {
381             XMLUserLoginModule.LOG.error("could not open file '" + _fileName + "'", e);
382         } catch (final SAXException e)  {
383             XMLUserLoginModule.LOG.error("could not read file '" + _fileName + "'", e);
384         }
385     }
386 }