1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements.  See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership.  The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License.  You may obtain a copy of the License at
9  *
10  *   http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied.  See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19 
20 package org.apache.guacamole.auth.jdbc.user;
21 
22 import com.google.inject.Inject;
23 import com.google.inject.Provider;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.List;
29 import javax.servlet.http.HttpServletRequest;
30 import org.apache.guacamole.net.auth.Credentials;
31 import org.apache.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper;
32 import org.apache.guacamole.auth.jdbc.base.ModeledDirectoryObjectService;
33 import org.apache.guacamole.GuacamoleClientException;
34 import org.apache.guacamole.GuacamoleException;
35 import org.apache.guacamole.GuacamoleUnsupportedException;
36 import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel;
37 import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
38 import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
39 import org.apache.guacamole.auth.jdbc.base.EntityMapper;
40 import org.apache.guacamole.auth.jdbc.base.ModeledActivityRecord;
41 import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionMapper;
42 import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionModel;
43 import org.apache.guacamole.auth.jdbc.permission.UserPermissionMapper;
44 import org.apache.guacamole.auth.jdbc.security.PasswordEncryptionService;
45 import org.apache.guacamole.auth.jdbc.security.PasswordPolicyService;
46 import org.apache.guacamole.form.Field;
47 import org.apache.guacamole.form.PasswordField;
48 import org.apache.guacamole.language.TranslatableGuacamoleClientException;
49 import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException;
50 import org.apache.guacamole.net.auth.ActivityRecord;
51 import org.apache.guacamole.net.auth.AuthenticatedUser;
52 import org.apache.guacamole.net.auth.AuthenticationProvider;
53 import org.apache.guacamole.net.auth.User;
54 import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
55 import org.apache.guacamole.net.auth.permission.ObjectPermission;
56 import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
57 import org.apache.guacamole.net.auth.permission.SystemPermission;
58 import org.apache.guacamole.net.auth.permission.SystemPermissionSet;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61 
62 /**
63  * Service which provides convenience methods for creating, retrieving, and
64  * manipulating users.
65  */
66 public class UserService extends ModeledDirectoryObjectService<ModeledUser, User, UserModel> {
67 
68     /**
69      * Logger for this class.
70      */
71     private static final Logger logger = LoggerFactory.getLogger(UserService.class);
72 
73     /**
74      * All user permissions which are implicitly granted to the new user upon
75      * creation.
76      */
77     private static final ObjectPermission.Type[] IMPLICIT_USER_PERMISSIONS = {
78         ObjectPermission.Type.READ
79     };
80 
81     /**
82      * The name of the HTTP password parameter to expect if the user is
83      * changing their expired password upon login.
84      */
85     private static final String NEW_PASSWORD_PARAMETER = "new-password";
86 
87     /**
88      * The password field to provide the user when their password is expired
89      * and must be changed.
90      */
91     private static final Field NEW_PASSWORD = new PasswordField(NEW_PASSWORD_PARAMETER);
92 
93     /**
94      * The name of the HTTP password confirmation parameter to expect if the
95      * user is changing their expired password upon login.
96      */
97     private static final String CONFIRM_NEW_PASSWORD_PARAMETER = "confirm-new-password";
98 
99     /**
100      * The password confirmation field to provide the user when their password
101      * is expired and must be changed.
102      */
103     private static final Field CONFIRM_NEW_PASSWORD = new PasswordField(CONFIRM_NEW_PASSWORD_PARAMETER);
104 
105     /**
106      * Information describing the expected credentials if a user's password is
107      * expired. If a user's password is expired, it must be changed during the
108      * login process.
109      */
110     private static final CredentialsInfo EXPIRED_PASSWORD = new CredentialsInfo(Arrays.asList(
111         CredentialsInfo.USERNAME,
112         CredentialsInfo.PASSWORD,
113         NEW_PASSWORD,
114         CONFIRM_NEW_PASSWORD
115     ));
116 
117     /**
118      * Mapper for creating/deleting entities.
119      */
120     @Inject
121     private EntityMapper entityMapper;
122 
123     /**
124      * Mapper for accessing users.
125      */
126     @Inject
127     private UserMapper userMapper;
128 
129     /**
130      * Mapper for manipulating user permissions.
131      */
132     @Inject
133     private UserPermissionMapper userPermissionMapper;
134 
135     /**
136      * Mapper for accessing user login history.
137      */
138     @Inject
139     private UserRecordMapper userRecordMapper;
140 
141     /**
142      * Provider for creating users.
143      */
144     @Inject
145     private Provider<ModeledUser> userProvider;
146 
147     /**
148      * Service for hashing passwords.
149      */
150     @Inject
151     private PasswordEncryptionService encryptionService;
152 
153     /**
154      * Service for enforcing password complexity policies.
155      */
156     @Inject
157     private PasswordPolicyService passwordPolicyService;
158 
159     @Override
getObjectMapper()160     protected ModeledDirectoryObjectMapper<UserModel> getObjectMapper() {
161         return userMapper;
162     }
163 
164     @Override
getPermissionMapper()165     protected ObjectPermissionMapper getPermissionMapper() {
166         return userPermissionMapper;
167     }
168 
169     @Override
getObjectInstance(ModeledAuthenticatedUser currentUser, UserModel model)170     protected ModeledUser getObjectInstance(ModeledAuthenticatedUser currentUser,
171             UserModel model) throws GuacamoleException {
172 
173         boolean exposeRestrictedAttributes;
174 
175         // Expose restricted attributes if the user does not yet exist
176         if (model.getObjectID() == null)
177             exposeRestrictedAttributes = true;
178 
179         // Otherwise, if the user permissions are available, expose restricted
180         // attributes only if the user has ADMINISTER permission
181         else if (currentUser != null)
182             exposeRestrictedAttributes = hasObjectPermission(currentUser,
183                     model.getIdentifier(), ObjectPermission.Type.ADMINISTER);
184 
185         // If user permissions are not available, do not expose anything
186         else
187             exposeRestrictedAttributes = false;
188 
189         // Produce ModeledUser exposing only those attributes for which the
190         // current user has permission
191         ModeledUser user = userProvider.get();
192         user.init(currentUser, model, exposeRestrictedAttributes);
193         return user;
194 
195     }
196 
197     @Override
getModelInstance(ModeledAuthenticatedUser currentUser, final User object)198     protected UserModel getModelInstance(ModeledAuthenticatedUser currentUser,
199             final User object) throws GuacamoleException {
200 
201         // Create new ModeledUser backed by blank model
202         UserModel model = new UserModel();
203         ModeledUser user = getObjectInstance(currentUser, model);
204 
205         // Set model contents through ModeledUser, copying the provided user
206         user.setIdentifier(object.getIdentifier());
207         user.setPassword(object.getPassword());
208         user.setAttributes(object.getAttributes());
209 
210         return model;
211 
212     }
213 
214     @Override
hasCreatePermission(ModeledAuthenticatedUser user)215     protected boolean hasCreatePermission(ModeledAuthenticatedUser user)
216             throws GuacamoleException {
217 
218         // Return whether user has explicit user creation permission
219         SystemPermissionSet permissionSet = user.getUser().getEffectivePermissions().getSystemPermissions();
220         return permissionSet.hasPermission(SystemPermission.Type.CREATE_USER);
221 
222     }
223 
224     @Override
getEffectivePermissionSet(ModeledAuthenticatedUser user)225     protected ObjectPermissionSet getEffectivePermissionSet(ModeledAuthenticatedUser user)
226             throws GuacamoleException {
227 
228         // Return permissions related to users
229         return user.getUser().getEffectivePermissions().getUserPermissions();
230 
231     }
232 
233     @Override
beforeCreate(ModeledAuthenticatedUser user, User object, UserModel model)234     protected void beforeCreate(ModeledAuthenticatedUser user, User object,
235             UserModel model) throws GuacamoleException {
236 
237         super.beforeCreate(user, object, model);
238 
239         // Username must not be blank
240         if (model.getIdentifier() == null || model.getIdentifier().trim().isEmpty())
241             throw new GuacamoleClientException("The username must not be blank.");
242 
243         // Do not create duplicate users
244         Collection<UserModel> existing = userMapper.select(Collections.singleton(model.getIdentifier()));
245         if (!existing.isEmpty())
246             throw new GuacamoleClientException("User \"" + model.getIdentifier() + "\" already exists.");
247 
248         // Verify new password does not violate defined policies (if specified)
249         if (object.getPassword() != null)
250             passwordPolicyService.verifyPassword(object.getIdentifier(), object.getPassword());
251 
252         // Create base entity object, implicitly populating underlying entity ID
253         entityMapper.insert(model);
254 
255     }
256 
257     @Override
beforeUpdate(ModeledAuthenticatedUser user, ModeledUser object, UserModel model)258     protected void beforeUpdate(ModeledAuthenticatedUser user,
259             ModeledUser object, UserModel model) throws GuacamoleException {
260 
261         super.beforeUpdate(user, object, model);
262 
263         // Refuse to update if the user is a skeleton and does not actually
264         // exist in the database (this will happen if the user is authenticated
265         // via a non-database authentication provider)
266         if (object.isSkeleton()) {
267             logger.info("Data cannot be stored for user \"{}\" as they do not "
268                     + "have an account within the database. If this is "
269                     + "unexpected, consider allowing automatic creation of "
270                     + "user accounts.", object.getIdentifier());
271             throw new GuacamoleUnsupportedException("User does not exist "
272                     + "within the database and cannot be updated.");
273         }
274 
275         // Username must not be blank
276         if (model.getIdentifier() == null || model.getIdentifier().trim().isEmpty())
277             throw new GuacamoleClientException("The username must not be blank.");
278 
279         // Check whether such a user is already present
280         UserModel existing = userMapper.selectOne(model.getIdentifier());
281         if (existing != null) {
282 
283             // Do not rename to existing user
284             if (!existing.getObjectID().equals(model.getObjectID()))
285                 throw new GuacamoleClientException("User \"" + model.getIdentifier() + "\" already exists.");
286 
287         }
288 
289         // Verify new password does not violate defined policies (if specified)
290         if (object.getPassword() != null) {
291 
292             // Enforce password age only for non-privileged users
293             if (!user.isPrivileged())
294                 passwordPolicyService.verifyPasswordAge(object);
295 
296             // Always verify password complexity
297             passwordPolicyService.verifyPassword(object.getIdentifier(), object.getPassword());
298 
299             // Store previous password in history
300             passwordPolicyService.recordPassword(object);
301 
302         }
303 
304     }
305 
306     @Override
307     protected Collection<ObjectPermissionModel>
getImplicitPermissions(ModeledAuthenticatedUser user, UserModel model)308         getImplicitPermissions(ModeledAuthenticatedUser user, UserModel model) {
309 
310         // Get original set of implicit permissions and make a copy
311         Collection<ObjectPermissionModel> implicitPermissions =
312                 new ArrayList<>(super.getImplicitPermissions(user, model));
313 
314         // Grant implicit permissions to the new user
315         for (ObjectPermission.Type permissionType : IMPLICIT_USER_PERMISSIONS) {
316 
317             ObjectPermissionModel permissionModel = new ObjectPermissionModel();
318             permissionModel.setEntityID(model.getEntityID());
319             permissionModel.setType(permissionType);
320             permissionModel.setObjectIdentifier(model.getIdentifier());
321 
322             // Add new permission to implicit permission set
323             implicitPermissions.add(permissionModel);
324 
325         }
326 
327         return Collections.unmodifiableCollection(implicitPermissions);
328     }
329 
330     @Override
beforeDelete(ModeledAuthenticatedUser user, String identifier)331     protected void beforeDelete(ModeledAuthenticatedUser user, String identifier) throws GuacamoleException {
332 
333         super.beforeDelete(user, identifier);
334 
335         // Do not allow users to delete themselves
336         if (identifier.equals(user.getUser().getIdentifier()))
337             throw new GuacamoleUnsupportedException("Deleting your own user is not allowed.");
338 
339     }
340 
341     @Override
isValidIdentifier(String identifier)342     protected boolean isValidIdentifier(String identifier) {
343 
344         // All strings are valid user identifiers
345         return true;
346 
347     }
348 
349     /**
350      * Retrieves the user corresponding to the given credentials from the
351      * database. Note that this function will not enforce any additional
352      * account restrictions, including explicitly disabled accounts,
353      * scheduling, and password expiration. It is the responsibility of the
354      * caller to enforce such restrictions, if desired.
355      *
356      * @param authenticationProvider
357      *     The AuthenticationProvider on behalf of which the user is being
358      *     retrieved.
359      *
360      * @param credentials
361      *     The credentials to use when locating the user.
362      *
363      * @return
364      *     An AuthenticatedUser containing the existing ModeledUser object if
365      *     the credentials given are valid, null otherwise.
366      *
367      * @throws GuacamoleException
368      *     If the provided credentials to not conform to expectations.
369      */
retrieveAuthenticatedUser(AuthenticationProvider authenticationProvider, Credentials credentials)370     public ModeledAuthenticatedUser retrieveAuthenticatedUser(AuthenticationProvider authenticationProvider,
371             Credentials credentials) throws GuacamoleException {
372 
373         // Get username and password
374         String username = credentials.getUsername();
375         String password = credentials.getPassword();
376 
377         // Retrieve corresponding user model, if such a user exists
378         UserModel userModel = userMapper.selectOne(username);
379         if (userModel == null)
380             return null;
381 
382         // Verify provided password is correct
383         byte[] hash = encryptionService.createPasswordHash(password, userModel.getPasswordSalt());
384         if (!Arrays.equals(hash, userModel.getPasswordHash()))
385             return null;
386 
387         // Create corresponding user object, set up cyclic reference
388         ModeledUser user = getObjectInstance(null, userModel);
389         user.setCurrentUser(new ModeledAuthenticatedUser(authenticationProvider, user, credentials));
390 
391         // Return now-authenticated user
392         return user.getCurrentUser();
393 
394     }
395 
396     /**
397      * Retrieves the user corresponding to the given AuthenticatedUser from the
398      * database.
399      *
400      * @param authenticationProvider
401      *     The AuthenticationProvider on behalf of which the user is being
402      *     retrieved.
403      *
404      * @param authenticatedUser
405      *     The AuthenticatedUser to retrieve the corresponding ModeledUser of.
406      *
407      * @return
408      *     The ModeledUser which corresponds to the given AuthenticatedUser, or
409      *     null if no such user exists.
410      *
411      * @throws GuacamoleException
412      *     If a ModeledUser object for the user corresponding to the given
413      *     AuthenticatedUser cannot be created.
414      */
retrieveUser(AuthenticationProvider authenticationProvider, AuthenticatedUser authenticatedUser)415     public ModeledUser retrieveUser(AuthenticationProvider authenticationProvider,
416             AuthenticatedUser authenticatedUser) throws GuacamoleException {
417 
418         // If we already queried this user, return that rather than querying again
419         if (authenticatedUser instanceof ModeledAuthenticatedUser)
420             return ((ModeledAuthenticatedUser) authenticatedUser).getUser();
421 
422         // Retrieve corresponding user model, if such a user exists
423         UserModel userModel = userMapper.selectOne(authenticatedUser.getIdentifier());
424         if (userModel == null)
425             return null;
426 
427         // Create corresponding user object, set up cyclic reference
428         ModeledUser user = getObjectInstance(null, userModel);
429         user.setCurrentUser(new ModeledAuthenticatedUser(authenticatedUser,
430                 authenticationProvider, user));
431 
432         // Return already-authenticated user
433         return user;
434 
435     }
436 
437     /**
438      * Generates an empty (skeleton) user corresponding to the given
439      * AuthenticatedUser.  The user will not be stored in the database, and
440      * will only be available in-memory during the time the session is
441      * active.
442      *
443      * @param authenticationProvider
444      *     The AuthenticationProvider on behalf of which the user is being
445      *     retrieved.
446      *
447      * @param authenticatedUser
448      *     The AuthenticatedUser to generate the skeleton account for.
449      *
450      * @return
451      *     The empty ModeledUser which corresponds to the given
452      *     AuthenticatedUser.
453      *
454      * @throws GuacamoleException
455      *     If a ModeledUser object for the user corresponding to the given
456      *     AuthenticatedUser cannot be created.
457      */
retrieveSkeletonUser(AuthenticationProvider authenticationProvider, AuthenticatedUser authenticatedUser)458     public ModeledUser retrieveSkeletonUser(AuthenticationProvider authenticationProvider,
459             AuthenticatedUser authenticatedUser) throws GuacamoleException {
460 
461         // Set up an empty user model
462         ModeledUser user = getObjectInstance(null,
463                 new UserModel(authenticatedUser.getIdentifier()));
464 
465         // Create user object, and configure cyclic reference
466         user.setCurrentUser(new ModeledAuthenticatedUser(authenticatedUser,
467                 authenticationProvider, user));
468 
469         // Return the new user.
470         return user;
471 
472     }
473 
474     /**
475      * Resets the password of the given user to the new password specified via
476      * the "new-password" and "confirm-new-password" parameters from the
477      * provided credentials. If these parameters are missing or invalid,
478      * additional credentials will be requested.
479      *
480      * @param user
481      *     The user whose password should be reset.
482      *
483      * @param credentials
484      *     The credentials from which the parameters required for password
485      *     reset should be retrieved.
486      *
487      * @throws GuacamoleException
488      *     If the password reset parameters within the given credentials are
489      *     invalid or missing.
490      */
resetExpiredPassword(ModeledUser user, Credentials credentials)491     public void resetExpiredPassword(ModeledUser user, Credentials credentials)
492             throws GuacamoleException {
493 
494         UserModel userModel = user.getModel();
495 
496         // Get username
497         String username = user.getIdentifier();
498 
499         // Pull new password from HTTP request
500         HttpServletRequest request = credentials.getRequest();
501         String newPassword = request.getParameter(NEW_PASSWORD_PARAMETER);
502         String confirmNewPassword = request.getParameter(CONFIRM_NEW_PASSWORD_PARAMETER);
503 
504         // Require new password if account is expired
505         if (newPassword == null || confirmNewPassword == null) {
506             logger.info("The password of user \"{}\" has expired and must be reset.", username);
507             throw new TranslatableGuacamoleInsufficientCredentialsException("Password has expired",
508                     "LOGIN.INFO_PASSWORD_EXPIRED", EXPIRED_PASSWORD);
509         }
510 
511         // New password must be different from old password
512         if (newPassword.equals(credentials.getPassword()))
513             throw new TranslatableGuacamoleClientException("New passwords may "
514                     + "not be identical to the current password if password "
515                     + "reset is required.", "LOGIN.ERROR_PASSWORD_SAME");
516 
517         // New password must not be blank
518         if (newPassword.isEmpty())
519             throw new TranslatableGuacamoleClientException("Passwords may not "
520                     + "be blank.", "LOGIN.ERROR_PASSWORD_BLANK");
521 
522         // Confirm that the password was entered correctly twice
523         if (!newPassword.equals(confirmNewPassword))
524             throw new TranslatableGuacamoleClientException("New password does "
525                     + "not match.", "LOGIN.ERROR_PASSWORD_MISMATCH");
526 
527         // Verify new password does not violate defined policies
528         passwordPolicyService.verifyPassword(username, newPassword);
529 
530         // Change password and reset expiration flag
531         userModel.setExpired(false);
532         user.setPassword(newPassword);
533         userMapper.update(userModel);
534         logger.info("Expired password of user \"{}\" has been reset.", username);
535 
536     }
537 
538     /**
539      * Returns a ActivityRecord object which is backed by the given model.
540      *
541      * @param model
542      *     The model object to use to back the returned connection record
543      *     object.
544      *
545      * @return
546      *     A connection record object which is backed by the given model.
547      */
getObjectInstance(ActivityRecordModel model)548     protected ActivityRecord getObjectInstance(ActivityRecordModel model) {
549         return new ModeledActivityRecord(model);
550     }
551 
552     /**
553      * Returns a list of ActivityRecord objects which are backed by the
554      * models in the given list.
555      *
556      * @param models
557      *     The model objects to use to back the activity record objects
558      *     within the returned list.
559      *
560      * @return
561      *     A list of activity record objects which are backed by the models
562      *     in the given list.
563      */
getObjectInstances(List<ActivityRecordModel> models)564     protected List<ActivityRecord> getObjectInstances(List<ActivityRecordModel> models) {
565 
566         // Create new list of records by manually converting each model
567         List<ActivityRecord> objects = new ArrayList<ActivityRecord>(models.size());
568         for (ActivityRecordModel model : models)
569             objects.add(getObjectInstance(model));
570 
571         return objects;
572 
573     }
574 
575     /**
576      * Retrieves the login history of the given user, including any active
577      * sessions.
578      *
579      * @param authenticatedUser
580      *     The user retrieving the login history.
581      *
582      * @param user
583      *     The user whose history is being retrieved.
584      *
585      * @return
586      *     The login history of the given user, including any active sessions.
587      *
588      * @throws GuacamoleException
589      *     If permission to read the login history is denied.
590      */
retrieveHistory(ModeledAuthenticatedUser authenticatedUser, ModeledUser user)591     public List<ActivityRecord> retrieveHistory(ModeledAuthenticatedUser authenticatedUser,
592             ModeledUser user) throws GuacamoleException {
593 
594         String username = user.getIdentifier();
595 
596         return retrieveHistory(username, authenticatedUser, Collections.emptyList(),
597                 Collections.emptyList(), Integer.MAX_VALUE);
598 
599     }
600 
601     /**
602      * Retrieves user login history records matching the given criteria.
603      * Retrieves up to <code>limit</code> user history records matching the
604      * given terms and sorted by the given predicates. Only history records
605      * associated with data that the given user can read are returned.
606      *
607      * @param username
608      *     The optional username to which history records should be limited, or
609      *     null if all readable records should be retrieved.
610      *
611      * @param user
612      *     The user retrieving the login history.
613      *
614      * @param requiredContents
615      *     The search terms that must be contained somewhere within each of the
616      *     returned records.
617      *
618      * @param sortPredicates
619      *     A list of predicates to sort the returned records by, in order of
620      *     priority.
621      *
622      * @param limit
623      *     The maximum number of records that should be returned.
624      *
625      * @return
626      *     The login history of the given user, including any active sessions.
627      *
628      * @throws GuacamoleException
629      *     If permission to read the user login history is denied.
630      */
retrieveHistory(String username, ModeledAuthenticatedUser user, Collection<ActivityRecordSearchTerm> requiredContents, List<ActivityRecordSortPredicate> sortPredicates, int limit)631     public List<ActivityRecord> retrieveHistory(String username,
632             ModeledAuthenticatedUser user,
633             Collection<ActivityRecordSearchTerm> requiredContents,
634             List<ActivityRecordSortPredicate> sortPredicates, int limit)
635             throws GuacamoleException {
636 
637         List<ActivityRecordModel> searchResults;
638 
639         // Bypass permission checks if the user is privileged
640         if (user.isPrivileged())
641             searchResults = userRecordMapper.search(username, requiredContents,
642                     sortPredicates, limit);
643 
644         // Otherwise only return explicitly readable history records
645         else
646             searchResults = userRecordMapper.searchReadable(username,
647                     user.getUser().getModel(),
648                     requiredContents, sortPredicates, limit, user.getEffectiveUserGroups());
649 
650         return getObjectInstances(searchResults);
651 
652     }
653 
654     /**
655      * Retrieves user login history records matching the given criteria.
656      * Retrieves up to <code>limit</code> user history records matching the
657      * given terms and sorted by the given predicates. Only history records
658      * associated with data that the given user can read are returned.
659      *
660      * @param user
661      *     The user retrieving the login history.
662      *
663      * @param requiredContents
664      *     The search terms that must be contained somewhere within each of the
665      *     returned records.
666      *
667      * @param sortPredicates
668      *     A list of predicates to sort the returned records by, in order of
669      *     priority.
670      *
671      * @param limit
672      *     The maximum number of records that should be returned.
673      *
674      * @return
675      *     The login history of the given user, including any active sessions.
676      *
677      * @throws GuacamoleException
678      *     If permission to read the user login history is denied.
679      */
retrieveHistory(ModeledAuthenticatedUser user, Collection<ActivityRecordSearchTerm> requiredContents, List<ActivityRecordSortPredicate> sortPredicates, int limit)680     public List<ActivityRecord> retrieveHistory(ModeledAuthenticatedUser user,
681             Collection<ActivityRecordSearchTerm> requiredContents,
682             List<ActivityRecordSortPredicate> sortPredicates, int limit)
683             throws GuacamoleException {
684 
685         return retrieveHistory(null, user, requiredContents, sortPredicates, limit);
686 
687     }
688 
689 }
690