001/**
002 * Copyright 2014 Tampere University of Technology, Pori Department
003 * 
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 *   http://www.apache.org/licenses/LICENSE-2.0
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package service.tut.pori.users;
017
018import java.util.EnumSet;
019
020import javax.xml.bind.annotation.XmlAccessType;
021import javax.xml.bind.annotation.XmlAccessorType;
022import javax.xml.bind.annotation.XmlElement;
023import javax.xml.bind.annotation.XmlRootElement;
024
025import org.apache.commons.codec.DecoderException;
026import org.apache.commons.codec.EncoderException;
027import org.apache.commons.codec.net.URLCodec;
028import org.apache.commons.lang3.ArrayUtils;
029import org.apache.commons.lang3.StringUtils;
030import org.apache.commons.lang3.tuple.Pair;
031import org.apache.log4j.Logger;
032import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
033
034import core.tut.pori.context.EventHandler;
035import core.tut.pori.context.ServiceInitializer;
036import core.tut.pori.http.ResponseData;
037import core.tut.pori.users.ExternalAccountConnection;
038import core.tut.pori.users.ExternalAccountConnection.UserServiceType;
039import core.tut.pori.users.UserAuthority;
040import core.tut.pori.users.UserEvent.EventType;
041import core.tut.pori.users.UserIdentity;
042
043/**
044 * User Core methods.
045 * 
046 * This class emits events of type {@link service.tut.pori.users.UserServiceEvent} for user account modifications with one of the listed {@link core.tut.pori.users.UserEvent.EventType} :
047 * <ul>
048 *  <li>{@link core.tut.pori.users.UserEvent.EventType#USER_CREATED} for newly created user accounts.</li>
049 *  <li>{@link core.tut.pori.users.UserEvent.EventType#USER_REMOVED} for removed user accounts.</li>
050 *  <li>{@link core.tut.pori.users.UserEvent.EventType#USER_AUTHORIZATION_REVOKED} for removed external account connection. The external connection type will can be retrieved from the service type getter ({@link service.tut.pori.users.UserServiceEvent#getUserServiceType()})</li>
051 * </ul>
052 */
053public class UserCore {
054  private static final Logger LOGGER = Logger.getLogger(UserCore.class);
055  
056  /**
057   * The status of registration process.
058   *
059   */
060  public enum RegistrationStatus{
061    /** registeration completed successfully */
062    OK,
063    /** given username was invalid or reserved */
064    BAD_USERNAME,
065    /** given password was invalid (too short or contained invalid characters */
066    BAD_PASSWORD,
067    /** required data was not given */
068    NULL_DATA, 
069    /** Registeration attempt was forbidden. */
070    FORBIDDEN;
071    
072    /**
073     * 
074     * @return this status as a string
075     */
076    public String toStatusString(){
077      return name();
078    }
079  } // enum RegistrationStatus
080  
081  /**
082   * 
083   */
084  private UserCore(){
085    // nothing needed
086  }
087  
088  /**
089   * @param serviceTypes
090   * @param userId
091   * @throws IllegalArgumentException on bad values
092   */
093  public static void deleteExternalAccountConnections(EnumSet<UserServiceType> serviceTypes, UserIdentity userId) throws IllegalArgumentException {
094    if(!UserIdentity.isValid(userId)){
095      throw new IllegalArgumentException("Invalid user identity.");
096    }
097    
098    if(serviceTypes == null || serviceTypes.isEmpty()){
099      LOGGER.warn("Ignored empty service type list.");
100      return;
101    }
102    
103    UserDAO userDao = ServiceInitializer.getDAOHandler().getSQLDAO(UserDAO.class);
104    EventHandler eventHandler = ServiceInitializer.getEventHandler();
105    for(UserServiceType t : serviceTypes){
106      if(userDao.deleteExternalAccountConnection(t, userId)){
107        eventHandler.publishEvent(new UserServiceEvent(EventType.USER_AUTHORIZATION_REVOKED, t, UserCore.class, userId));
108      }else{
109        LOGGER.warn("Could not remove requested user service connection "+t.toUserServiceTypeString()+" for user, id: "+userId.getUserId());
110      }
111    }
112  }
113  
114  /**
115   * 
116   * @param username
117   * @return user identity for the username or null if not found
118   */
119  public static UserIdentity getUserIdentity(String username){
120    return ServiceInitializer.getDAOHandler().getSQLDAO(UserDAO.class).getUser(username);
121  }
122  
123  /**
124   * 
125   * @param userId
126   * @return user identity for the given id or null if not found
127   */
128  public static UserIdentity getUserIdentity(Long userId){
129    return ServiceInitializer.getDAOHandler().getSQLDAO(UserDAO.class).getUser(userId);
130  }
131  
132  /**
133   * 
134   * @param serviceTypes optional service type filters
135   * @param userId
136   * @return list of connections for the given user or null if none was found
137   */
138  public static ExternalAccountConnectionList getExternalAccountConnections(EnumSet<UserServiceType> serviceTypes, UserIdentity userId){
139    return ServiceInitializer.getDAOHandler().getSQLDAO(UserDAO.class).getExternalAccountConnections(serviceTypes, userId);
140  }
141  
142  /**
143   *  Register a new user, checking for valid system registration password, if one is set in the system properties.
144   * 
145   * @param registration
146   * @return status
147   */
148  public static RegistrationStatus register(Registration registration) {
149    String registerPassword = ServiceInitializer.getPropertyHandler().getSystemProperties(UserServiceProperties.class).getRegisterPassword();
150    if(!StringUtils.isBlank(registerPassword) && !registerPassword.equals(registration.getRegisterPassword())){
151      LOGGER.warn("The given registeration password was invalid.");
152      return RegistrationStatus.FORBIDDEN;
153    }
154    
155    return createUser(registration);
156  }
157  
158  /**
159   * 
160   * @param connection
161   * @return UserIdentity with the id value set or null if none is found
162   */
163  public static UserIdentity getUserId(ExternalAccountConnection connection){
164    return ServiceInitializer.getDAOHandler().getSQLDAO(UserDAO.class).getUserId(connection);
165  }
166  
167  /**
168   * 
169   * @param connection
170   * @param userId
171   * @throws IllegalArgumentException
172   */
173  public static void insertExternalAccountConnection(ExternalAccountConnection connection, UserIdentity userId) throws IllegalArgumentException{
174    if(!UserIdentity.isValid(userId)){
175      throw new IllegalArgumentException("Bad userId.");
176    }
177    ServiceInitializer.getDAOHandler().getSQLDAO(UserDAO.class).insertExternalAccountConnection(connection, userId);
178  }
179  
180  /**
181   * Crate user based on the registration information. On success this will publish event notification for newly created user.
182   * 
183   * @param registration
184   * @return status
185   */
186  public static RegistrationStatus createUser(Registration registration){
187    RegistrationStatus status = Registration.isValid(registration);
188    if(status != RegistrationStatus.OK){
189      LOGGER.debug("Invalid registration.");
190      return status;
191    }
192    
193    UserIdentity userId = new UserIdentity(registration.getEncryptedPassword(), null, registration.getUsername());
194    userId.addAuthority(UserAuthority.AUTHORITY_ROLE_USER); // add with role user
195    if(ServiceInitializer.getDAOHandler().getSQLDAO(UserDAO.class).addUser(userId)){
196      registration.setRegisteredUserId(userId);
197      ServiceInitializer.getEventHandler().publishEvent(new UserServiceEvent(EventType.USER_CREATED, null, UserCore.class, userId));
198      return RegistrationStatus.OK;
199    }else{
200      LOGGER.debug("Failed to add new user: reserved username.");
201      return RegistrationStatus.BAD_USERNAME;
202    }
203  }
204  
205  /**
206   * Remove the user from the system. Successful call will publish event notification for removed user.
207   * 
208   * @param userId
209   * @throws IllegalArgumentException
210   */
211  public static void unregister(UserIdentity userId) throws IllegalArgumentException{
212    if(ServiceInitializer.getDAOHandler().getSQLDAO(UserDAO.class).removeUser(userId)){
213      ServiceInitializer.getSessionHandler().removeSessionInformation(userId); // remove all user's sessions
214      ServiceInitializer.getEventHandler().publishEvent(new UserServiceEvent(EventType.USER_REMOVED, null, UserCore.class, userId));
215    }else{
216      throw new IllegalArgumentException("Failed to remove user, id: "+userId.getUserId());
217    }
218  }
219  
220  /**
221   * 
222   * @param authenticatedUser
223   * @param userIdFilter optional filter for retrieving a list of users, if null, the details of the authenticatedUser will be returned
224   * @return user details for the requested userId or null if not available
225   * @throws IllegalArgumentException
226   */
227  public static UserIdentityList getUserDetails(UserIdentity authenticatedUser, long[] userIdFilter) throws IllegalArgumentException{
228    if(!UserIdentity.isValid(authenticatedUser)){
229      throw new IllegalArgumentException("Bad authenticated user.");
230    }
231    Long authId = authenticatedUser.getUserId();
232    UserDAO userDAO = ServiceInitializer.getDAOHandler().getSQLDAO(UserDAO.class);
233    authenticatedUser = userDAO.getUser(authId); // populate the details
234    if(!UserIdentity.isValid(authenticatedUser)){
235      LOGGER.warn("Could not resolve user identity for user, id: "+authId);
236      throw new IllegalArgumentException("Bad authenticated user.");
237    }
238    
239    if(ArrayUtils.isEmpty(userIdFilter) || (userIdFilter.length == 1 && userIdFilter[0] == authId)){ // if no filters have been given or the only filter is the authenticated user
240      UserIdentityList list = new UserIdentityList();
241      list.addUserId(authenticatedUser);
242      return list;
243    }
244    
245    if(!authenticatedUser.getAuthorities().contains(UserAuthority.AUTHORITY_ROLE_ADMIN)){
246      LOGGER.warn("User, id: "+authId+" tried to access user details, but does not have the required role: "+UserAuthority.AUTHORITY_ROLE_ADMIN.getAuthority());
247      return null;
248    }else{
249      return userDAO.getUsers(userIdFilter);
250    }
251  }
252  
253  /**
254   * Combines nonce and redirectUri together for redirecting things after FB has responded
255   * @param nonce
256   * @param redirectUri
257   * @return the nonce and redirection in a combined, URL encoded form
258   */
259  public static String urlEncodedCombinedNonce(String nonce, String redirectUri){
260    String retval = null;
261    try {
262      if(StringUtils.isEmpty(redirectUri)){
263        retval = new URLCodec().encode(nonce);
264      }else{
265        retval = new URLCodec().encode(nonce+Definitions.NONCE_SEPARATOR+redirectUri);
266      }
267    } catch (EncoderException ex) {
268      LOGGER.error(ex, ex);
269    }
270    return retval;
271  }
272  
273  /**
274   * 
275   * @param nonce
276   * @return pair of "nonce","redirectUri" or null if NONCE_SEPARATOR ".-." is not found
277   */
278  public static Pair<String, String> getNonceAndRedirectUri(String nonce){
279    Pair<String, String> nonceAndUrl = null;
280    try {
281      String decoded = new URLCodec().decode(nonce);
282      String[] splitted = StringUtils.split(decoded, Definitions.NONCE_SEPARATOR, 2);
283      if(splitted.length > 1){
284        nonceAndUrl = Pair.of(splitted[0], splitted[1]);
285      }else{
286        return null;
287      }
288    } catch (DecoderException ex) {
289      LOGGER.error(ex, ex);
290    }
291    return nonceAndUrl;
292  }
293  
294  /**
295   * User registration details.
296   *
297   */
298  @XmlRootElement(name=Definitions.ELEMENT_REGISTRATION)
299  @XmlAccessorType(XmlAccessType.NONE)
300  public static class Registration extends ResponseData{
301    @XmlElement(name=core.tut.pori.users.Definitions.ELEMENT_USERNAME)
302    private String _username = null;
303    private String _password = null;
304    private String _encryptedPassword = null;
305    private BCryptPasswordEncoder _encoder = null;
306    @XmlElement(name=Definitions.ELEMENT_REGISTER_PASSWORD)
307    private String _registerPassword = null;
308    private UserIdentity _registeredUserId = null; // contains the registered user details after successful registration or null if not available
309    
310    /**
311     * @return the username
312     */
313    public String getUsername() {
314      return _username;
315    }
316    
317    /**
318     * @param username the username to set
319     */
320    public void setUsername(String username) {
321      _username = username;
322    }
323    
324    /**
325     * @return the password
326     */
327    @XmlElement(name=Definitions.ELEMENT_PASSWORD)
328    public String getPassword() {
329      return _password;
330    }
331    
332    /**
333     * 
334     * @return encrypted password
335     */
336    public String getEncryptedPassword(){
337      if(_password == null){
338        LOGGER.debug("No password.");
339        return null;
340      }
341      if(_encryptedPassword == null){
342        if(_encoder == null){
343          _encoder = new BCryptPasswordEncoder();
344        }
345        _encryptedPassword = _encoder.encode(_password);
346      }
347      return _encryptedPassword;
348    }
349    
350    /**
351     * @param password the password to set
352     */
353    public void setPassword(String password) {
354      _encryptedPassword = null; // may have changed
355      _password = password;
356    }
357    
358    /**
359     * for sub-classing, use the static
360     * 
361     * @return true if the registration object is valid
362     */
363    protected RegistrationStatus isValid(){
364      if(StringUtils.isBlank(_username)){
365        LOGGER.debug("No username.");
366        return RegistrationStatus.BAD_USERNAME;
367      }else if(StringUtils.isBlank(_password)){
368        LOGGER.debug("No password.");
369        return RegistrationStatus.BAD_PASSWORD;
370      }else{
371        return RegistrationStatus.OK;
372      }
373    }
374    
375    /**
376     * 
377     * @param registration can be null
378     * @return true if the passed registration object is valid
379     */
380    public static RegistrationStatus isValid(Registration registration){
381      if(registration == null){
382        return RegistrationStatus.NULL_DATA;
383      }else{
384        return registration.isValid();
385      }
386    }
387
388    /**
389     * Returns the registered user details after successful registration
390     * 
391     * @return the registeredUserId
392     */
393    public UserIdentity getRegisteredUserId() {
394      return _registeredUserId;
395    }
396
397    /**
398     * @param registeredUserId the registeredUserId to set
399     */
400    protected void setRegisteredUserId(UserIdentity registeredUserId) {
401      _registeredUserId = registeredUserId;
402    }
403
404    /**
405     * @return the registerPassword
406     */
407    public String getRegisterPassword() {
408      return _registerPassword;
409    }
410
411    /**
412     * @param registerPassword the registerPassword to set
413     */
414    public void setRegisterPassword(String registerPassword) {
415      _registerPassword = registerPassword;
416    }
417  } // class Registration
418}