1    /*
2     * Copyright 2009 :torweg free software group
3     *
4     * This program is free software: you can redistribute it and/or modify
5     * it under the terms of the GNU General Public License as published by
6     * the Free Software Foundation, either version 3 of the License, or
7     * (at your option) any later version.
8     * 
9     * This program is distributed in the hope that it will be useful,
10    * but WITHOUT ANY WARRANTY; without even the implied warranty of
11    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    * GNU General Public License for more details.
13    * 
14    * You should have received a copy of the GNU General Public License
15    * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16    *
17    */
18   package org.torweg.pulse.accesscontrol;
19   
20   import java.io.File;
21   import java.io.FileNotFoundException;
22   import java.math.BigInteger;
23   import java.security.NoSuchAlgorithmException;
24   import java.security.SecureRandom;
25   import java.util.HashSet;
26   import java.util.Random;
27   import java.util.Set;
28   
29   import javax.persistence.Transient;
30   import javax.xml.bind.annotation.XmlAccessOrder;
31   import javax.xml.bind.annotation.XmlAccessType;
32   import javax.xml.bind.annotation.XmlAccessorOrder;
33   import javax.xml.bind.annotation.XmlAccessorType;
34   import javax.xml.bind.annotation.XmlElement;
35   import javax.xml.bind.annotation.XmlElementWrapper;
36   import javax.xml.bind.annotation.XmlRootElement;
37   
38   import org.hibernate.LazyInitializationException;
39   import org.hibernate.Session;
40   import org.hibernate.Transaction;
41   import org.hibernate.criterion.Restrictions;
42   import org.jdom.Element;
43   import org.slf4j.Logger;
44   import org.slf4j.LoggerFactory;
45   import org.torweg.pulse.configuration.PoorMansCache;
46   import org.torweg.pulse.invocation.lifecycle.Lifecycle;
47   import org.torweg.pulse.service.PulseException;
48   import org.torweg.pulse.service.request.ServiceSession;
49   import org.torweg.pulse.util.HibernateDataSource;
50   import org.torweg.pulse.util.StringUtils;
51   
52   /**
53    * abstract base for {@code User} containing all utility methods.
54    * 
55    * @author Thomas Weber, Daniel Dietz
56    * @version $Revision: 1822 $
57    */
58   @XmlRootElement(name = "abstract-user-base")
59   @XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
60   @XmlAccessorType(XmlAccessType.FIELD)
61   public abstract class AbstractUserBase extends AbstractAccessControlObject {
62   
63       /**
64        * serialVersionUID.
65        */
66       private static final long serialVersionUID = -7020963404983358073L;
67   
68       /**
69        * the logger.
70        */
71       private static final Logger LOGGER = LoggerFactory
72               .getLogger(AbstractUserBase.class);
73   
74       /**
75        * The characters for auto-generated passwords.
76        */
77       protected static final String PASSWORD_CHARACTERS = "0123456789"
78               + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
79               + "~+*#'?/\\{}[]()!$%&=-_.,;:";
80   
81       /**
82        * the id of the everybody role.
83        */
84       @XmlElement(name = "everybody-id")
85       private static Long everybodyId;
86   
87       /**
88        * the command matchers of the user.
89        */
90       @Transient
91       // getter JAXB-annotated
92       private final Set<CommandMatcher> commandMatchers = new HashSet<CommandMatcher>();
93   
94       /**
95        * name of the everybody role.
96        */
97       public static final String EVERYBODY = "~Everybody";
98   
99       /**
100       * initialises the {@code CommandMatcher}s of the given {@code User}.
101       * 
102       * @param user
103       *            the user
104       * 
105       */
106      private static void initUserCommandMatchers(final User user) {
107          for (Role r : user.getRoles()) {
108              processGroups(user, r.getGroups());
109              processPermissions(user, r.getPermissions());
110          }
111          processGroups(user, user.getGroups());
112          processPermissions(user, user.getPermissions());
113      }
114  
115      /**
116       * processes a set of {@code Group}s and adds all {@code CommandMatcher}s to
117       * the {@code User}'s set of {@code CommandMatcher}s.
118       * 
119       * @param user
120       *            the user
121       * 
122       * @param gs
123       *            the groups to be processed
124       */
125      private static void processGroups(final User user, final Set<Group> gs) {
126          for (Group g : gs) {
127              for (Permission p : g.getPermissions()) {
128                  user.getCommandMatchers().addAll(p.getCommandMatchers());
129              }
130          }
131      }
132  
133      /**
134       * processes a set of {@code Permission}s and adds all
135       * {@code CommandMatcher}s to the {@code User}'s set of
136       * {@code CommandMatcher}s.
137       * 
138       * @param user
139       *            the user
140       * 
141       * @param ps
142       *            the permissions to be processed
143       */
144      private static void processPermissions(final User user,
145              final Set<Permission> ps) {
146          for (Permission p : ps) {
147              user.getCommandMatchers().addAll(p.getCommandMatchers());
148          }
149      }
150  
151      /**
152       * parses the user attributes from {@code WEB-INF/conf/user-attributes.xml}.
153       * <p>
154       * TODO: remove when database based version of attributes is ready.
155       * </p>
156       * 
157       * @param user
158       *            the user
159       */
160      private static void parseUserAttributes(final User user) {
161          try {
162              Element userAttributes = PoorMansCache.getJDOMResource(
163                      new File("WEB-INF" + System.getProperty("file.separator")
164                              + "conf" + System.getProperty("file.separator")
165                              + "user-attributes.xml")).getRootElement();
166              for (Object o : userAttributes.getChildren("user")) {
167                  Element userElement = (Element) o;
168                  if (!userElement.getAttributeValue("name").equals(
169                          user.getName())) {
170                      continue;
171                  }
172                  for (Object attr : userElement.getChildren("attribute")) {
173                      user.addAttribute(parseAttribute((Element) attr));
174                  }
175              }
176          } catch (FileNotFoundException e) {
177              LOGGER.warn("no user-attributes.xml found!");
178          }
179      }
180  
181      /**
182       * TODO: remove when database based version of attributes is ready.
183       * 
184       * @param attr
185       *            .
186       * @return the parsed user attributes
187       */
188      private static UserAttribute parseAttribute(final Element attr) {
189          UserAttribute attribute = new UserAttribute(
190                  attr.getAttributeValue("name"));
191          for (Object v : attr.getChildren("value")) {
192              Element value = (Element) v;
193              attribute.addValue(value.getTextNormalize());
194          }
195          for (Object a : attr.getChildren("attribute")) {
196              attribute.addAttribute(parseAttribute((Element) a));
197          }
198          return attribute;
199      }
200  
201      /**
202       * adds the current session roles ({@link ServiceSession#getSessionRoles()})
203       * to the given {@code User}.
204       * @param user
205       *            the given user
206       * @param session
207       *            the current session
208       */
209      private static void addSessionRoles(final User user,
210              final ServiceSession session) {
211          if (session != null && session.getSessionRoles() != null
212                  && !session.getSessionRoles().isEmpty()) {
213              for (Role role : session.getSessionRoles()) {
214                  if (!user.addSessionRole(role)) {
215                      LOGGER.warn(
216                              "Could not add session based role {}@{} to current user.",
217                              role.getName(), role.getId());
218                  }
219      
220              }
221          }
222      }
223  
224      /**
225       * @return Returns the commandMatchers.
226       */
227      protected final Set<CommandMatcher> getCommandMatchers() {
228          return this.commandMatchers;
229      }
230  
231      /**
232       * For JAXB only.
233       * 
234       * @return this.getCommandMatchers()
235       */
236      @XmlElementWrapper(name = "command-matchers")
237      @XmlElement(name = "command-matcher")
238      @SuppressWarnings("unused")
239      @Deprecated
240      private HashSet<CommandMatcher> getCommandMatchersJAXB() { // NOPMD
241          try {
242              return new HashSet<CommandMatcher>(getCommandMatchers());
243          } catch (LazyInitializationException e) {
244              LOGGER.debug("ignored: {}", e.getLocalizedMessage());
245              return null;
246          }
247      }
248  
249      /**
250       * returns the id of the <tt>~Everybody</tt> role.
251       * 
252       * @return the id
253       */
254      protected final Long getEveryBodyId() {
255          return Long.valueOf(everybodyId.longValue());
256      }
257  
258      /**
259       * creates a {@code User}, if a {@code User} id is found in the
260       * {@code ServiceSession}. If not, the {@code Everybody} is returned.
261       * 
262       * @param session
263       *            the {@code ServiceSession}
264       * 
265       * @return the initialised {@code User}
266       */
267      public static final User getUser(final ServiceSession session) {
268          Long id = null;
269          if (session != null) {
270              id = (Long) session.getAttribute(User.class.getCanonicalName());
271          }
272          User user = null;
273          Session sess = null;
274          Transaction trans = null;
275          try {
276              sess = Lifecycle.getHibernateDataSource().createNewSession();
277              trans = sess.beginTransaction();
278  
279              /* add role "~Everybody" */
280              Role everybody = getEverybodyRole(sess);
281  
282              /* if we have a logged in user, add its permissions as well */
283              if (id != null) {
284                  user = (User) sess.get(User.class, id);
285                  if (user != null && user.isActive()) {
286                      initUserCommandMatchers(user);
287                  } else {
288                      session.removeAttribute(User.class.getCanonicalName());
289                  }
290              }
291  
292              /*
293               * if the user is still null, add a dummy user to the context for
294               * access checks
295               */
296              if (user == null) {
297                  user = new User.Everybody(everybody);
298              }
299  
300              processGroups(user, everybody.getGroups());
301              processPermissions(user, everybody.getPermissions());
302  
303              trans.commit();
304          } catch (Exception exception) {
305              if (trans != null) {
306                  trans.rollback();
307              }
308              throw new PulseException(exception);
309          } finally {
310              sess.close();
311          }
312  
313          // TODO: change when database based version of attributes is ready
314          if (user != null) {
315              parseUserAttributes(user);
316          }
317  
318          // add session-roles
319          addSessionRoles(user, session);
320  
321          return user;
322      }
323  
324      /**
325       * Returns the {@code User} named usrName or the {@code Everybody} , if no
326       * such user exists.
327       * 
328       * @param usrName
329       *            the name of the {@code User}
330       * @return the {@code User} named usrName or {@code Everybody}, if no such
331       *         {@code User} exists.
332       */
333      public static final User getUser(final String usrName) {
334          Session sess = null;
335          Transaction trans = null;
336          try {
337              sess = Lifecycle.getHibernateDataSource().createNewSession();
338              trans = sess.beginTransaction();
339              User user = (User) sess
340                      .createQuery("from User as u where u.name=?")
341                      .setString(0, usrName).setCacheable(true).uniqueResult();
342              if (user != null) {
343                  initUserCommandMatchers(user);
344              }
345  
346              /* add role "~Everybody" */
347              Role everybody = getEverybodyRole(sess);
348  
349              if ((user == null) || (user.isExpunged())) {
350                  user = new User.Everybody(everybody);
351              }
352  
353              processGroups(user, everybody.getGroups());
354              processPermissions(user, everybody.getPermissions());
355  
356              trans.commit();
357              return user;
358          } catch (Exception exception) {
359              if (trans != null) {
360                  trans.rollback();
361              }
362              throw new PulseException(exception);
363          } finally {
364              sess.close();
365          }
366      }
367  
368      /**
369       * Returns the superuser or creates one, if none exists.
370       * 
371       * <p>
372       * The superuser will get a random password, which will be logged at info
373       * level. Make sure to change the password after your first login!
374       * </p>
375       * 
376       * @param dataSrc
377       *            the {@code HibernateDataSource} to be used
378       * 
379       * @return the superuser
380       */
381      public static final User getSuperUser(final HibernateDataSource dataSrc) {
382          User superUsr = null;
383          String passwd = null;
384          Session sess = null;
385          Transaction trans = null;
386          try {
387              sess = dataSrc.createNewSession();
388              trans = sess.beginTransaction();
389              superUsr = (User) sess
390                      .createQuery("from User as u where u.superuser = ?")
391                      .setBoolean(0, true).uniqueResult();
392              if (superUsr == null) {
393                  passwd = generatePassword();
394                  superUsr = new User("root", "user@domain", passwd);
395                  superUsr.setSuperuser(true);
396                  superUsr.setWebdavEnabled(true);
397                  sess.save(superUsr);
398              }
399              trans.commit();
400          } catch (Exception exception) {
401              if (trans != null) {
402                  trans.rollback();
403              }
404              throw new PulseException(exception);
405          } finally {
406              sess.close();
407          }
408          if (passwd != null) {
409              LOGGER.info("Created new superuser root identified by {}", passwd);
410          }
411          return superUsr;
412      }
413  
414      /**
415       * retrieves the Everybody role.
416       * 
417       * @param sess
418       *            the session to load the role with
419       * @return the <tt>~Everybody</tt> role
420       */
421      public static final Role getEverybodyRole(final Session sess) {
422          Role everybody;
423          if (everybodyId != null) {
424              everybody = (Role) sess.get(Role.class, everybodyId);
425          } else {
426              everybody = (Role) sess.createCriteria(Group.class)
427                      .add(Restrictions.eq("name", User.EVERYBODY))
428                      .uniqueResult();
429          }
430          return everybody;
431      }
432  
433      /**
434       * initialises the "{@code ~EVERYBODY}" {@code Group}.
435       * <p>
436       * If the group does not exist already, it is created.
437       * </p>
438       * 
439       * @param ds
440       *            the {@code HibernateDataSource} to be used
441       */
442      public static final void initEverybodyRole(final HibernateDataSource ds) {
443          Session s = ds.createNewSession();
444          Transaction tx = s.beginTransaction();
445          try {
446              Role everybody = (Role) s.createCriteria(Role.class)
447                      .add(Restrictions.eq("name", User.EVERYBODY))
448                      .uniqueResult();
449              if (everybody == null) {
450                  everybody = new Role(User.EVERYBODY);
451                  s.save(everybody);
452                  LOGGER.info("Created Role '{}' as the default role.",
453                          User.EVERYBODY);
454              }
455              tx.commit();
456              everybodyId = everybody.getId();
457          } catch (Exception e) {
458              tx.rollback();
459              throw new PulseException("Error: " + e.getLocalizedMessage(), e);
460          } finally {
461              s.close();
462          }
463      }
464  
465      /**
466       * Returns a generated password of 12 characters.
467       * 
468       * @return a generated password of 12 characters
469       */
470      public static final String generatePassword() {
471          Random random;
472          try {
473              // create a secure random generator
474              random = SecureRandom.getInstance("SHA1PRNG");
475          } catch (NoSuchAlgorithmException e) {
476              LOGGER.error(
477                      "Error creating SecureRandom: " + e.getLocalizedMessage(),
478                      e);
479              random = new Random();
480              random.setSeed(System.currentTimeMillis());
481          }
482  
483          StringBuilder password = new StringBuilder();
484          for (int i = 0; i < 15; i++) {
485              password.append(User.PASSWORD_CHARACTERS.charAt((int) (random
486                      .nextDouble() * User.PASSWORD_CHARACTERS.length())));
487          }
488          return password.toString();
489      }
490  
491      /**
492       * generates a base-62 encoded 16 byte token using
493       * {@link Lifecycle#getRandom()}.
494       * 
495       * @return the token
496       */
497      public static final String generateToken() {
498          byte[] bytes = new byte[16];
499          Lifecycle.getRandom().nextBytes(bytes);
500          BigInteger token = new BigInteger(bytes);
501          StringBuilder tokenBuilder = new StringBuilder(
502                  StringUtils.toBase62String(token.abs()));
503          for (int i = tokenBuilder.length(); i < 22; i++) {
504              tokenBuilder.insert(0, '0');
505          }
506          return tokenBuilder.toString();
507      }
508  
509  }
510