GUACAMOLE-394: Merge add support for recording user login/logout history to database auth.
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java
index c9274dc..0f72559 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java
@@ -76,6 +76,7 @@
 import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileService;
 import org.apache.guacamole.auth.jdbc.tunnel.RestrictedGuacamoleTunnelService;
 import org.apache.guacamole.auth.jdbc.user.PasswordRecordMapper;
+import org.apache.guacamole.auth.jdbc.user.UserRecordMapper;
 import org.mybatis.guice.MyBatisModule;
 import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider;
 
@@ -126,6 +127,7 @@
         addMapperClass(SharingProfilePermissionMapper.class);
         addMapperClass(UserMapper.class);
         addMapperClass(UserPermissionMapper.class);
+        addMapperClass(UserRecordMapper.class);
         
         // Bind core implementations of guacamole-ext classes
         bind(ActiveConnectionDirectory.class);
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordModel.java
new file mode 100644
index 0000000..fbf6209
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordModel.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.jdbc.base;
+
+import java.util.Date;
+
+/**
+ * A single activity record representing an arbitrary activity performed by a
+ * user.
+ */
+public class ActivityRecordModel {
+
+    /**
+     * The ID of this object in the database, if any.
+     */
+    private Integer recordID;
+
+    /**
+     * The database ID of the user associated with this activity record.
+     */
+    private Integer userID;
+
+    /**
+     * The username of the user that performed the activity.
+     */
+    private String username;
+
+    /**
+     * The remote host associated with the user that performed the activity.
+     */
+    private String remoteHost;
+
+    /**
+     * The time the activity was initiated by the associated user.
+     */
+    private Date startDate;
+
+    /**
+     * The time the activity ended, or null if the end time is not known or
+     * the activity is still in progress.
+     */
+    private Date endDate;
+
+    /**
+     * Returns the ID of this record in the database, if it exists.
+     *
+     * @return
+     *     The ID of this record in the database, or null if this record was
+     *     not retrieved from the database.
+     */
+    public Integer getRecordID() {
+        return recordID;
+    }
+
+    /**
+     * Sets the database ID of this record to the given value.
+     *
+     * @param recordID
+     *     The ID to assign to this object.
+     */
+    public void setRecordID(Integer recordID) {
+        this.recordID = recordID;
+    }
+
+    /**
+     * Returns the database ID of the user associated with this activity
+     * record.
+     * 
+     * @return
+     *     The database ID of the user associated with this activity record.
+     */
+    public Integer getUserID() {
+        return userID;
+    }
+
+    /**
+     * Sets the database ID of the user associated with this activity record.
+     *
+     * @param userID
+     *     The database ID of the user to associate with this activity
+     *     record.
+     */
+    public void setUserID(Integer userID) {
+        this.userID = userID;
+    }
+
+    /**
+     * Returns the username of the user that performed the activity associated
+     * with this record.
+     * 
+     * @return
+     *     The username of the user that performed the activity associated with
+     *     this record.
+     */
+    public String getUsername() {
+        return username;
+    }
+
+    /**
+     * Sets the username of the user that performed the activity associated
+     * with this record.
+     *
+     * @param username
+     *     The username of the user that performed the activity associated with
+     *     this record.
+     */
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    /**
+     * Returns the remote host associated with the user that performed the
+     * activity.
+     *
+     * @return
+     *     The remote host associated with the user that performed the activity.
+     */
+    public String getRemoteHost() {
+        return remoteHost;
+    }
+
+    /**
+     * Sets the remote host associated with the user that performed the
+     * activity.
+     *
+     * @param remoteHost
+     *     The remote host associated with the user that performed the activity.
+     */
+    public void setRemoteHost(String remoteHost) {
+        this.remoteHost = remoteHost;
+    }
+
+    /**
+     * Returns the time the activity was initiated by the associated user.
+     *
+     * @return
+     *     The time the activity was initiated by the associated user.
+     */
+    public Date getStartDate() {
+        return startDate;
+    }
+
+    /**
+     * Sets the time the activity was initiated by the associated user.
+     *
+     * @param startDate
+     *     The time the activity was initiated by the associated user.
+     */
+    public void setStartDate(Date startDate) {
+        this.startDate = startDate;
+    }
+
+    /**
+     * Returns the time the activity ended, or null if the end time is not
+     * known or the activity is still in progress.
+     *
+     * @return
+     *     The time the activity ended, or null if the end time is not known or
+     *     the activity is still in progress.
+     */
+    public Date getEndDate() {
+        return endDate;
+    }
+
+    /**
+     * Sets the time the activity ended, if known.
+     *
+     * @param endDate
+     *     The time the activity ended, or null if the end time is not known or
+     *     the activity is still in progress.
+     */
+    public void setEndDate(Date endDate) {
+        this.endDate = endDate;
+    }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSearchTerm.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordSearchTerm.java
similarity index 93%
rename from extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSearchTerm.java
rename to extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordSearchTerm.java
index 844eff7..54af1a6 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSearchTerm.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordSearchTerm.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.jdbc.connection;
+package org.apache.guacamole.auth.jdbc.base;
 
 import java.util.Calendar;
 import java.util.Date;
@@ -25,11 +25,11 @@
 import java.util.regex.Pattern;
 
 /**
- * A search term for querying historical connection records. This will contain
- * a the search term in string form and, if that string appears to be a date. a
- * corresponding date range.
+ * A search term for querying historical records of arbitrary activities. This
+ * will contain a the search term in string form and, if that string appears to
+ * be a date. a corresponding date range.
  */
-public class ConnectionRecordSearchTerm {
+public class ActivityRecordSearchTerm {
     
     /**
      * A pattern that can match a year, year and month, or year and month and
@@ -180,7 +180,7 @@
     }
 
     /**
-     * Creates a new ConnectionRecordSearchTerm representing the given string.
+     * Creates a new ActivityRecordSearchTerm representing the given string.
      * If the given string appears to be a date, the start and end dates of the
      * implied date range will be automatically determined and made available
      * via getStartDate() and getEndDate() respectively.
@@ -188,7 +188,7 @@
      * @param term
      *     The string that should be searched for.
      */
-    public ConnectionRecordSearchTerm(String term) {
+    public ActivityRecordSearchTerm(String term) {
 
         // Search terms absolutely must not be null
         if (term == null)
@@ -281,10 +281,10 @@
     @Override
     public boolean equals(Object obj) {
 
-        if (obj == null || !(obj instanceof ConnectionRecordSearchTerm))
+        if (obj == null || !(obj instanceof ActivityRecordSearchTerm))
             return false;
 
-        return ((ConnectionRecordSearchTerm) obj).getTerm().equals(getTerm());
+        return ((ActivityRecordSearchTerm) obj).getTerm().equals(getTerm());
 
     }
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSortPredicate.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordSortPredicate.java
similarity index 77%
rename from extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSortPredicate.java
rename to extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordSortPredicate.java
index 69eee78..ab0d3ce 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSortPredicate.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ActivityRecordSortPredicate.java
@@ -17,18 +17,18 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.jdbc.connection;
+package org.apache.guacamole.auth.jdbc.base;
 
 import org.apache.guacamole.net.auth.ActivityRecordSet;
 
 /**
- * A sort predicate which species the property to use when sorting connection
+ * A sort predicate which species the property to use when sorting activity
  * records, along with the sort order.
  */
-public class ConnectionRecordSortPredicate {
+public class ActivityRecordSortPredicate {
 
     /**
-     * The property to use when sorting ConnectionRecords.
+     * The property to use when sorting ActivityRecords.
      */
     private final ActivityRecordSet.SortableProperty property;
 
@@ -38,26 +38,26 @@
     private final boolean descending;
     
     /**
-     * Creates a new ConnectionRecordSortPredicate with the given sort property 
+     * Creates a new ActivityRecordSortPredicate with the given sort property
      * and sort order.
      * 
      * @param property 
-     *     The property to use when sorting ConnectionRecords.
+     *     The property to use when sorting ActivityRecords.
      * 
      * @param descending 
      *     Whether the sort order is descending (true) or ascending (false).
      */
-    public ConnectionRecordSortPredicate(ActivityRecordSet.SortableProperty property,
+    public ActivityRecordSortPredicate(ActivityRecordSet.SortableProperty property,
             boolean descending) {
         this.property   = property;
         this.descending = descending;
     }
     
     /**
-     * Returns the property that should be used when sorting ConnectionRecords.
+     * Returns the property that should be used when sorting ActivityRecords.
      *
      * @return
-     *     The property that should be used when sorting ConnectionRecords.
+     *     The property that should be used when sorting ActivityRecords.
      */
     public ActivityRecordSet.SortableProperty getProperty() {
         return property;
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecord.java
new file mode 100644
index 0000000..95b1a25
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecord.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.jdbc.base;
+
+
+import java.util.Date;
+import org.apache.guacamole.net.auth.ActivityRecord;
+
+/**
+ * An ActivityRecord which is backed by a database model.
+ */
+public class ModeledActivityRecord implements ActivityRecord {
+
+    /**
+     * The model object backing this activity record.
+     */
+    private final ActivityRecordModel model;
+
+    /**
+     * Creates a new ModeledActivityRecord backed by the given model object.
+     * Changes to this record will affect the backing model object, and changes
+     * to the backing model object will affect this record.
+     * 
+     * @param model
+     *     The model object to use to back this activity record.
+     */
+    public ModeledActivityRecord(ActivityRecordModel model) {
+        this.model = model;
+    }
+
+    @Override
+    public Date getStartDate() {
+        return model.getStartDate();
+    }
+
+    @Override
+    public Date getEndDate() {
+        return model.getEndDate();
+    }
+
+    @Override
+    public String getRemoteHost() {
+        return model.getRemoteHost();
+    }
+
+    @Override
+    public String getUsername() {
+        return model.getUsername();
+    }
+
+    @Override
+    public boolean isActive() {
+        return false;
+    }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java
new file mode 100644
index 0000000..d259018
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.jdbc.base;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.ActivityRecord;
+import org.apache.guacamole.net.auth.ActivityRecordSet;
+import org.apache.guacamole.net.auth.ActivityRecordSet.SortableProperty;
+import org.apache.guacamole.net.auth.AuthenticatedUser;
+
+/**
+ * A JDBC implementation of ActivityRecordSet. Calls to asCollection() will
+ * query history records using an implementation-specific mechanism. Which
+ * records are returned will be determined by the values passed in earlier.
+ *
+ * @param <RecordType>
+ *     The type of ActivityRecord contained within this set.
+ */
+public abstract class ModeledActivityRecordSet<RecordType extends ActivityRecord>
+        extends RestrictedObject implements ActivityRecordSet<RecordType> {
+
+    /**
+     * The set of strings that each must occur somewhere within the returned 
+     * records, whether within the associated username, an associated date, or
+     * other related data. If non-empty, any record not matching each of the
+     * strings within the collection will be excluded from the results.
+     */
+    private final Set<ActivityRecordSearchTerm> requiredContents =
+            new HashSet<ActivityRecordSearchTerm>();
+    
+    /**
+     * The maximum number of history records that should be returned by a call
+     * to asCollection().
+     */
+    private int limit = Integer.MAX_VALUE;
+    
+    /**
+     * A list of predicates to apply while sorting the resulting records,
+     * describing the properties involved and the sort order for those
+     * properties.
+     */
+    private final List<ActivityRecordSortPredicate> sortPredicates =
+            new ArrayList<ActivityRecordSortPredicate>();
+
+    /**
+     * Retrieves the history records matching the given criteria. Retrieves up
+     * to <code>limit</code> history records matching the given terms and sorted
+     * by the given predicates. Only history records associated with data that
+     * the given user can read are returned.
+     *
+     * @param user
+     *     The user retrieving the history.
+     *
+     * @param requiredContents
+     *     The search terms that must be contained somewhere within each of the
+     *     returned records.
+     *
+     * @param sortPredicates
+     *     A list of predicates to sort the returned records by, in order of
+     *     priority.
+     *
+     * @param limit
+     *     The maximum number of records that should be returned.
+     *
+     * @return
+     *     A collection of all history records matching the given criteria.
+     *
+     * @throws GuacamoleException
+     *     If permission to read the history records is denied.
+     */
+    protected abstract Collection<RecordType> retrieveHistory(
+            AuthenticatedUser user,
+            Set<ActivityRecordSearchTerm> requiredContents,
+            List<ActivityRecordSortPredicate> sortPredicates,
+            int limit) throws GuacamoleException;
+
+    @Override
+    public Collection<RecordType> asCollection()
+            throws GuacamoleException {
+        return retrieveHistory(getCurrentUser(), requiredContents,
+                sortPredicates, limit);
+    }
+
+    @Override
+    public ModeledActivityRecordSet<RecordType> contains(String value)
+            throws GuacamoleException {
+        requiredContents.add(new ActivityRecordSearchTerm(value));
+        return this;
+    }
+
+    @Override
+    public ModeledActivityRecordSet<RecordType> limit(int limit) throws GuacamoleException {
+        this.limit = Math.min(this.limit, limit);
+        return this;
+    }
+
+    @Override
+    public ModeledActivityRecordSet<RecordType> sort(SortableProperty property, boolean desc)
+            throws GuacamoleException {
+        
+        sortPredicates.add(new ActivityRecordSortPredicate(
+            property,
+            desc
+        ));
+        
+        return this;
+
+    }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java
index 788daa1..da45402 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java
@@ -19,6 +19,7 @@
 
 package org.apache.guacamole.auth.jdbc.connection;
 
+import java.util.Date;
 import java.util.HashSet;
 import java.util.Set;
 import org.apache.guacamole.auth.jdbc.base.ChildObjectModel;
@@ -93,6 +94,12 @@
     private EncryptionMethod proxyEncryptionMethod;
 
     /**
+     * The date and time that this connection was last used, or null if this
+     * connection has never been used.
+     */
+    private Date lastActive;
+
+    /**
      * Creates a new, empty connection.
      */
     public ConnectionModel() {
@@ -341,6 +348,32 @@
         this.sharingProfileIdentifiers = sharingProfileIdentifiers;
     }
 
+    /**
+     * Returns the date and time that this connection was last used, or null if
+     * this connection has never been used.
+     *
+     * @return
+     *     The date and time that this connection was last used, or null if this
+     *     connection has never been used.
+     */
+    public Date getLastActive() {
+        return lastActive;
+    }
+
+    /**
+     * Sets the date and time that this connection was last used. This value is
+     * expected to be set automatically via queries, derived from connection
+     * history records. It does NOT correspond to an actual column, and values
+     * set manually through invoking this function will not persist.
+     *
+     * @param lastActive
+     *     The date and time that this connection was last used, or null if this
+     *     connection has never been used.
+     */
+    public void setLastActive(Date lastActive) {
+        this.lastActive = lastActive;
+    }
+
     @Override
     public String getIdentifier() {
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java
index aefff92..637fd0f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java
@@ -21,6 +21,8 @@
 
 import java.util.Collection;
 import java.util.List;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
 import org.apache.ibatis.annotations.Param;
 import org.apache.guacamole.auth.jdbc.user.UserModel;
 
@@ -75,8 +77,8 @@
      * @return
      *     The results of the search performed with the given parameters.
      */
-    List<ConnectionRecordModel> search(@Param("terms") Collection<ConnectionRecordSearchTerm> terms,
-            @Param("sortPredicates") List<ConnectionRecordSortPredicate> sortPredicates,
+    List<ConnectionRecordModel> search(@Param("terms") Collection<ActivityRecordSearchTerm> terms,
+            @Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
             @Param("limit") int limit);
 
     /**
@@ -104,8 +106,8 @@
      *     The results of the search performed with the given parameters.
      */
     List<ConnectionRecordModel> searchReadable(@Param("user") UserModel user,
-            @Param("terms") Collection<ConnectionRecordSearchTerm> terms,
-            @Param("sortPredicates") List<ConnectionRecordSortPredicate> sortPredicates,
+            @Param("terms") Collection<ActivityRecordSearchTerm> terms,
+            @Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
             @Param("limit") int limit);
 
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordModel.java
index f142f4e..29c5556 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordModel.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordModel.java
@@ -19,14 +19,14 @@
 
 package org.apache.guacamole.auth.jdbc.connection;
 
-import java.util.Date;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel;
 
 /**
  * A single connection record representing a past usage of a particular
  * connection. If the connection was being shared, the sharing profile used to
  * join the connection is included in the record.
  */
-public class ConnectionRecordModel {
+public class ConnectionRecordModel extends ActivityRecordModel {
 
     /**
      * The identifier of the connection associated with this connection record.
@@ -54,32 +54,6 @@
     private String sharingProfileName;
 
     /**
-     * The database ID of the user associated with this connection record.
-     */
-    private Integer userID;
-
-    /**
-     * The username of the user associated with this connection record.
-     */
-    private String username;
-
-    /**
-     * The remote host associated with this connection record.
-     */
-    private String remoteHost;
-
-    /**
-     * The time the connection was initiated by the associated user.
-     */
-    private Date startDate;
-
-    /**
-     * The time the connection ended, or null if the end time is not known or
-     * the connection is still running.
-     */
-    private Date endDate;
-
-    /**
      * Returns the identifier of the connection associated with this connection
      * record.
      *
@@ -179,109 +153,4 @@
         this.sharingProfileName = sharingProfileName;
     }
 
-    /**
-     * Returns the database ID of the user associated with this connection
-     * record.
-     * 
-     * @return
-     *     The database ID of the user associated with this connection record.
-     */
-    public Integer getUserID() {
-        return userID;
-    }
-
-    /**
-     * Sets the database ID of the user associated with this connection record.
-     *
-     * @param userID
-     *     The database ID of the user to associate with this connection
-     *     record.
-     */
-    public void setUserID(Integer userID) {
-        this.userID = userID;
-    }
-
-    /**
-     * Returns the username of the user associated with this connection record.
-     * 
-     * @return
-     *     The username of the user associated with this connection record.
-     */
-    public String getUsername() {
-        return username;
-    }
-
-    /**
-     * Sets the username of the user associated with this connection record.
-     *
-     * @param username
-     *     The username of the user to associate with this connection record.
-     */
-    public void setUsername(String username) {
-        this.username = username;
-    }
-
-    /**
-     * Returns the remote host associated with this connection record.
-     *
-     * @return
-     *     The remote host associated with this connection record.
-     */
-    public String getRemoteHost() {
-        return remoteHost;
-    }
-
-    /**
-     * Sets the remote host associated with this connection record.
-     *
-     * @param remoteHost
-     *     The remote host to associate with this connection record.
-     */
-    public void setRemoteHost(String remoteHost) {
-        this.remoteHost = remoteHost;
-    }
-
-    /**
-     * Returns the date that the associated connection was established.
-     *
-     * @return
-     *     The date the associated connection was established.
-     */
-    public Date getStartDate() {
-        return startDate;
-    }
-
-    /**
-     * Sets the date that the associated connection was established.
-     *
-     * @param startDate
-     *     The date that the associated connection was established.
-     */
-    public void setStartDate(Date startDate) {
-        this.startDate = startDate;
-    }
-
-    /**
-     * Returns the date that the associated connection ended, or null if no
-     * end date was recorded. The lack of an end date does not necessarily
-     * mean that the connection is still active.
-     *
-     * @return
-     *     The date the associated connection ended, or null if no end date was
-     *     recorded.
-     */
-    public Date getEndDate() {
-        return endDate;
-    }
-
-    /**
-     * Sets the date that the associated connection ended.
-     *
-     * @param endDate
-     *     The date that the associated connection ended.
-     */
-    public void setEndDate(Date endDate) {
-        this.endDate = endDate;
-    }
-
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java
index 7b3d629..f4574f4 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java
@@ -20,15 +20,14 @@
 package org.apache.guacamole.auth.jdbc.connection;
 
 import com.google.inject.Inject;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
-import org.apache.guacamole.net.auth.ActivityRecordSet;
-import org.apache.guacamole.net.auth.ActivityRecordSet.SortableProperty;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
+import org.apache.guacamole.auth.jdbc.base.ModeledActivityRecordSet;
+import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.ConnectionRecord;
 
 /**
@@ -36,8 +35,7 @@
  * asCollection() will query connection history records from the database. Which
  * records are returned will be determined by the values passed in earlier.
  */
-public class ConnectionRecordSet extends RestrictedObject
-        implements ActivityRecordSet<ConnectionRecord> {
+public class ConnectionRecordSet extends ModeledActivityRecordSet<ConnectionRecord> {
 
     /**
      * Service for managing connection objects.
@@ -45,60 +43,15 @@
     @Inject
     private ConnectionService connectionService;
     
-    /**
-     * The set of strings that each must occur somewhere within the returned 
-     * connection records, whether within the associated username, the name of 
-     * the associated connection, or any associated date. If non-empty, any 
-     * connection record not matching each of the strings within the collection 
-     * will be excluded from the results.
-     */
-    private final Set<ConnectionRecordSearchTerm> requiredContents = 
-            new HashSet<ConnectionRecordSearchTerm>();
-    
-    /**
-     * The maximum number of connection history records that should be returned
-     * by a call to asCollection().
-     */
-    private int limit = Integer.MAX_VALUE;
-    
-    /**
-     * A list of predicates to apply while sorting the resulting connection
-     * records, describing the properties involved and the sort order for those 
-     * properties.
-     */
-    private final List<ConnectionRecordSortPredicate> connectionRecordSortPredicates =
-            new ArrayList<ConnectionRecordSortPredicate>();
-    
     @Override
-    public Collection<ConnectionRecord> asCollection()
+    protected Collection<ConnectionRecord> retrieveHistory(
+            AuthenticatedUser user, Set<ActivityRecordSearchTerm> requiredContents,
+            List<ActivityRecordSortPredicate> sortPredicates, int limit)
             throws GuacamoleException {
+
+        // Retrieve history from database
         return connectionService.retrieveHistory(getCurrentUser(),
-                requiredContents, connectionRecordSortPredicates, limit);
-    }
-
-    @Override
-    public ConnectionRecordSet contains(String value)
-            throws GuacamoleException {
-        requiredContents.add(new ConnectionRecordSearchTerm(value));
-        return this;
-    }
-
-    @Override
-    public ConnectionRecordSet limit(int limit) throws GuacamoleException {
-        this.limit = Math.min(this.limit, limit);
-        return this;
-    }
-
-    @Override
-    public ConnectionRecordSet sort(SortableProperty property, boolean desc)
-            throws GuacamoleException {
-        
-        connectionRecordSortPredicates.add(new ConnectionRecordSortPredicate(
-            property,
-            desc
-        ));
-        
-        return this;
+                requiredContents, sortPredicates, limit);
 
     }
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java
index f256324..983f395 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java
@@ -34,6 +34,8 @@
 import org.apache.guacamole.GuacamoleClientException;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleSecurityException;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
 import org.apache.guacamole.auth.jdbc.base.ModeledChildDirectoryObjectService;
 import org.apache.guacamole.auth.jdbc.permission.ConnectionPermissionMapper;
 import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionMapper;
@@ -460,8 +462,8 @@
      *     If permission to read the connection history is denied.
      */
     public List<ConnectionRecord> retrieveHistory(ModeledAuthenticatedUser user,
-            Collection<ConnectionRecordSearchTerm> requiredContents,
-            List<ConnectionRecordSortPredicate> sortPredicates, int limit)
+            Collection<ActivityRecordSearchTerm> requiredContents,
+            List<ActivityRecordSortPredicate> sortPredicates, int limit)
             throws GuacamoleException {
 
         List<ConnectionRecordModel> searchResults;
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java
index c596b27..eb392bc 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java
@@ -235,7 +235,7 @@
 
     @Override
     public Date getLastActive() {
-        return null;
+        return getModel().getLastActive();
     }
 
     @Override
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnectionRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnectionRecord.java
index a5e83d4..9f34385 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnectionRecord.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnectionRecord.java
@@ -20,13 +20,14 @@
 package org.apache.guacamole.auth.jdbc.connection;
 
 
-import java.util.Date;
+import org.apache.guacamole.auth.jdbc.base.ModeledActivityRecord;
 import org.apache.guacamole.net.auth.ConnectionRecord;
 
 /**
  * A ConnectionRecord which is backed by a database model.
  */
-public class ModeledConnectionRecord implements ConnectionRecord {
+public class ModeledConnectionRecord extends ModeledActivityRecord
+        implements ConnectionRecord {
 
     /**
      * The model object backing this connection record.
@@ -42,6 +43,7 @@
      *     The model object to use to back this connection record.
      */
     public ModeledConnectionRecord(ConnectionRecordModel model) {
+        super(model);
         this.model = model;
     }
 
@@ -65,29 +67,4 @@
         return model.getSharingProfileName();
     }
 
-    @Override
-    public Date getStartDate() {
-        return model.getStartDate();
-    }
-
-    @Override
-    public Date getEndDate() {
-        return model.getEndDate();
-    }
-
-    @Override
-    public String getRemoteHost() {
-        return model.getRemoteHost();
-    }
-
-    @Override
-    public String getUsername() {
-        return model.getUsername();
-    }
-
-    @Override
-    public boolean isActive() {
-        return false;
-    }
-
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java
index fc43e36..5ffc458 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java
@@ -145,6 +145,12 @@
     ));
 
     /**
+     * Service for managing users.
+     */
+    @Inject
+    private UserService userService;
+
+    /**
      * Service for hashing passwords.
      */
     @Inject
@@ -795,13 +801,13 @@
     }
 
     @Override
-    public Date getLastActive() {
-        return null;
+    public Timestamp getLastActive() {
+        return getModel().getLastActive();
     }
 
     @Override
     public List<ActivityRecord> getHistory() throws GuacamoleException {
-        return Collections.<ActivityRecord>emptyList();
+        return userService.retrieveHistory(getCurrentUser(), this);
     }
 
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java
index 1b238ab..5bfcda6 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java
@@ -26,9 +26,11 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.Collection;
+import java.util.Date;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
 import org.apache.guacamole.auth.jdbc.activeconnection.ActiveConnectionDirectory;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel;
 import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordSet;
 import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
 import org.apache.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
@@ -44,7 +46,6 @@
 import org.apache.guacamole.net.auth.Directory;
 import org.apache.guacamole.net.auth.SharingProfile;
 import org.apache.guacamole.net.auth.User;
-import org.apache.guacamole.net.auth.simple.SimpleActivityRecordSet;
 
 /**
  * UserContext implementation which is driven by an arbitrary, underlying
@@ -99,7 +100,24 @@
      */
     @Inject
     private Provider<ConnectionRecordSet> connectionRecordSetProvider;
-    
+
+    /**
+     * Provider for creating user record sets.
+     */
+    @Inject
+    private Provider<UserRecordSet> userRecordSetProvider;
+
+    /**
+     * Mapper for user login records.
+     */
+    @Inject
+    private UserRecordMapper userRecordMapper;
+
+    /**
+     * The activity record associated with this user's Guacamole session.
+     */
+    private ActivityRecordModel userRecord;
+
     @Override
     public void init(ModeledAuthenticatedUser currentUser) {
 
@@ -112,6 +130,15 @@
         sharingProfileDirectory.init(currentUser);
         activeConnectionDirectory.init(currentUser);
 
+        // Create login record for user
+        userRecord = new ActivityRecordModel();
+        userRecord.setUsername(currentUser.getIdentifier());
+        userRecord.setStartDate(new Date());
+        userRecord.setRemoteHost(currentUser.getCredentials().getRemoteHostname());
+
+        // Insert record representing login
+        userRecordMapper.insert(userRecord);
+
     }
 
     @Override
@@ -167,7 +194,9 @@
     @Override
     public ActivityRecordSet<ActivityRecord> getUserHistory()
             throws GuacamoleException {
-        return new SimpleActivityRecordSet<ActivityRecord>();
+        UserRecordSet userRecordSet = userRecordSetProvider.get();
+        userRecordSet.init(getCurrentUser());
+        return userRecordSet;
     }
 
     @Override
@@ -202,7 +231,11 @@
 
     @Override
     public void invalidate() {
-        // Nothing to invalidate
+
+        // Record logout time
+        userRecord.setEndDate(new Date());
+        userRecordMapper.update(userRecord);
+
     }
 
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserModel.java
index afaeb55..a6cf997 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserModel.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserModel.java
@@ -115,6 +115,12 @@
     private String organizationalRole;
 
     /**
+     * The date and time that this user was last active, or null if this user
+     * has never logged in.
+     */
+    private Timestamp lastActive;
+
+    /**
      * Creates a new, empty user.
      */
     public UserModel() {
@@ -465,4 +471,30 @@
         this.organizationalRole = organizationalRole;
     }
 
+    /**
+     * Returns the date and time that this user was last active, or null if
+     * this user has never logged in.
+     *
+     * @return
+     *     The date and time that this user was last active, or null if this
+     *     user has never logged in.
+     */
+    public Timestamp getLastActive() {
+        return lastActive;
+    }
+
+    /**
+     * Sets the date and time that this user was last active. This value is
+     * expected to be set automatically via queries, derived from user history
+     * records. It does NOT correspond to an actual column, and values set
+     * manually through invoking this function will not persist.
+     *
+     * @param lastActive
+     *     The date and time that this user was last active, or null if this
+     *     user has never logged in.
+     */
+    public void setLastActive(Timestamp lastActive) {
+        this.lastActive = lastActive;
+    }
+
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java
new file mode 100644
index 0000000..b2177bf
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.jdbc.user;
+
+import java.util.Collection;
+import java.util.List;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * Mapper for user login activity records.
+ */
+public interface UserRecordMapper {
+
+    /**
+     * Returns a collection of all user login records associated with the user
+     * having the given username.
+     *
+     * @param username
+     *     The username of the user whose login records are to be retrieved.
+     *
+     * @return
+     *     A collection of all user login records associated with the user
+     *     having the given username. This collection will be empty if no such
+     *     user exists.
+     */
+    List<ActivityRecordModel> select(@Param("username") String username);
+
+    /**
+     * Inserts the given user login record.
+     *
+     * @param record
+     *     The user login record to insert.
+     *
+     * @return
+     *     The number of rows inserted.
+     */
+    int insert(@Param("record") ActivityRecordModel record);
+
+    /**
+     * Updates the given user login record.
+     *
+     * @param record
+     *     The user login record to update.
+     *
+     * @return
+     *     The number of rows updated.
+     */
+    int update(@Param("record") ActivityRecordModel record);
+
+    /**
+     * Searches for up to <code>limit</code> user login records that contain
+     * the given terms, sorted by the given predicates, regardless of whether
+     * the data they are associated with is is readable by any particular user.
+     * This should only be called on behalf of a system administrator. If
+     * records are needed by a non-administrative user who must have explicit
+     * read rights, use searchReadable() instead.
+     *
+     * @param terms
+     *     The search terms that must match the returned records.
+     *
+     * @param sortPredicates
+     *     A list of predicates to sort the returned records by, in order of
+     *     priority.
+     *
+     * @param limit
+     *     The maximum number of records that should be returned.
+     *
+     * @return
+     *     The results of the search performed with the given parameters.
+     */
+    List<ActivityRecordModel> search(@Param("terms") Collection<ActivityRecordSearchTerm> terms,
+            @Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
+            @Param("limit") int limit);
+
+    /**
+     * Searches for up to <code>limit</code> user login records that contain
+     * the given terms, sorted by the given predicates. Only records that are
+     * associated with data explicitly readable by the given user will be
+     * returned. If records are needed by a system administrator (who, by
+     * definition, does not need explicit read rights), use search() instead.
+     *
+     * @param user
+     *    The user whose permissions should determine whether a record is
+     *    returned.
+     *
+     * @param terms
+     *     The search terms that must match the returned records.
+     *
+     * @param sortPredicates
+     *     A list of predicates to sort the returned records by, in order of
+     *     priority.
+     *
+     * @param limit
+     *     The maximum number of records that should be returned.
+     *
+     * @return
+     *     The results of the search performed with the given parameters.
+     */
+    List<ActivityRecordModel> searchReadable(@Param("user") UserModel user,
+            @Param("terms") Collection<ActivityRecordSearchTerm> terms,
+            @Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
+            @Param("limit") int limit);
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java
new file mode 100644
index 0000000..c1b4897
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.jdbc.user;
+
+import com.google.inject.Inject;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
+import org.apache.guacamole.auth.jdbc.base.ModeledActivityRecordSet;
+import org.apache.guacamole.net.auth.ActivityRecord;
+import org.apache.guacamole.net.auth.AuthenticatedUser;
+
+/**
+ * A JDBC implementation of ActivityRecordSet for retrieving user login history.
+ * Calls to asCollection() will query user login records from the database.
+ * Which records are returned will be determined by the values passed in
+ * earlier.
+ */
+public class UserRecordSet extends ModeledActivityRecordSet<ActivityRecord> {
+
+    /**
+     * Service for managing user objects.
+     */
+    @Inject
+    private UserService userService;
+    
+    @Override
+    protected Collection<ActivityRecord> retrieveHistory(
+            AuthenticatedUser user, Set<ActivityRecordSearchTerm> requiredContents,
+            List<ActivityRecordSortPredicate> sortPredicates, int limit)
+            throws GuacamoleException {
+
+        // Retrieve history from database
+        return userService.retrieveHistory(getCurrentUser(),
+                requiredContents, sortPredicates, limit);
+
+    }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java
index 3dc025f..090963f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java
@@ -21,16 +21,24 @@
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import javax.servlet.http.HttpServletRequest;
 import org.apache.guacamole.net.auth.Credentials;
 import org.apache.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper;
 import org.apache.guacamole.auth.jdbc.base.ModeledDirectoryObjectService;
 import org.apache.guacamole.GuacamoleClientException;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleSecurityException;
 import org.apache.guacamole.GuacamoleUnsupportedException;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
+import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
+import org.apache.guacamole.auth.jdbc.base.ModeledActivityRecord;
+import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel;
 import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionMapper;
 import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionModel;
 import org.apache.guacamole.auth.jdbc.permission.UserPermissionMapper;
@@ -38,8 +46,10 @@
 import org.apache.guacamole.auth.jdbc.security.PasswordPolicyService;
 import org.apache.guacamole.form.Field;
 import org.apache.guacamole.form.PasswordField;
+import org.apache.guacamole.net.auth.ActivityRecord;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
+import org.apache.guacamole.net.auth.ConnectionRecord;
 import org.apache.guacamole.net.auth.User;
 import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
 import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
@@ -116,7 +126,13 @@
      */
     @Inject
     private UserPermissionMapper userPermissionMapper;
-    
+
+    /**
+     * Mapper for accessing user login history.
+     */
+    @Inject
+    private UserRecordMapper userRecordMapper;
+
     /**
      * Provider for creating users.
      */
@@ -460,4 +476,119 @@
 
     }
 
+    /**
+     * Returns a ActivityRecord object which is backed by the given model.
+     *
+     * @param model
+     *     The model object to use to back the returned connection record
+     *     object.
+     *
+     * @return
+     *     A connection record object which is backed by the given model.
+     */
+    protected ActivityRecord getObjectInstance(ActivityRecordModel model) {
+        return new ModeledActivityRecord(model);
+    }
+
+    /**
+     * Returns a list of ActivityRecord objects which are backed by the
+     * models in the given list.
+     *
+     * @param models
+     *     The model objects to use to back the activity record objects
+     *     within the returned list.
+     *
+     * @return
+     *     A list of activity record objects which are backed by the models
+     *     in the given list.
+     */
+    protected List<ActivityRecord> getObjectInstances(List<ActivityRecordModel> models) {
+
+        // Create new list of records by manually converting each model
+        List<ActivityRecord> objects = new ArrayList<ActivityRecord>(models.size());
+        for (ActivityRecordModel model : models)
+            objects.add(getObjectInstance(model));
+
+        return objects;
+
+    }
+
+    /**
+     * Retrieves the login history of the given user, including any active
+     * sessions.
+     *
+     * @param authenticatedUser
+     *     The user retrieving the login history.
+     *
+     * @param user
+     *     The user whose history is being retrieved.
+     *
+     * @return
+     *     The login history of the given user, including any active sessions.
+     *
+     * @throws GuacamoleException
+     *     If permission to read the login history is denied.
+     */
+    public List<ActivityRecord> retrieveHistory(ModeledAuthenticatedUser authenticatedUser,
+            ModeledUser user) throws GuacamoleException {
+
+        String username = user.getIdentifier();
+
+        // Retrieve history only if READ permission is granted
+        if (hasObjectPermission(authenticatedUser, username, ObjectPermission.Type.READ))
+            return getObjectInstances(userRecordMapper.select(username));
+
+        // The user does not have permission to read the history
+        throw new GuacamoleSecurityException("Permission denied.");
+
+    }
+
+    /**
+     * Retrieves user login history records matching the given criteria.
+     * Retrieves up to <code>limit</code> user history records matching the
+     * given terms and sorted by the given predicates. Only history records
+     * associated with data that the given user can read are returned.
+     *
+     * @param user
+     *     The user retrieving the login history.
+     *
+     * @param requiredContents
+     *     The search terms that must be contained somewhere within each of the
+     *     returned records.
+     *
+     * @param sortPredicates
+     *     A list of predicates to sort the returned records by, in order of
+     *     priority.
+     *
+     * @param limit
+     *     The maximum number of records that should be returned.
+     *
+     * @return
+     *     The login history of the given user, including any active sessions.
+     *
+     * @throws GuacamoleException
+     *     If permission to read the user login history is denied.
+     */
+    public List<ActivityRecord> retrieveHistory(ModeledAuthenticatedUser user,
+            Collection<ActivityRecordSearchTerm> requiredContents,
+            List<ActivityRecordSortPredicate> sortPredicates, int limit)
+            throws GuacamoleException {
+
+        List<ActivityRecordModel> searchResults;
+
+        // Bypass permission checks if the user is a system admin
+        if (user.getUser().isAdministrator())
+            searchResults = userRecordMapper.search(requiredContents,
+                    sortPredicates, limit);
+
+        // Otherwise only return explicitly readable history records
+        else
+            searchResults = userRecordMapper.searchReadable(user.getUser().getModel(),
+                    requiredContents, sortPredicates, limit);
+
+        return getObjectInstances(searchResults);
+
+    }
+
+
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/001-create-schema.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/001-create-schema.sql
index 1873d1c..f26d2cc 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/001-create-schema.sql
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/001-create-schema.sql
@@ -336,6 +336,7 @@
   KEY `sharing_profile_id` (`sharing_profile_id`),
   KEY `start_date` (`start_date`),
   KEY `end_date` (`end_date`),
+  KEY `connection_start_date` (`connection_id`, `start_date`),
 
   CONSTRAINT `guacamole_connection_history_ibfk_1`
     FOREIGN KEY (`user_id`)
@@ -352,6 +353,31 @@
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 --
+-- User login/logout history
+--
+
+CREATE TABLE guacamole_user_history (
+
+  `history_id`           int(11)      NOT NULL AUTO_INCREMENT,
+  `user_id`              int(11)      DEFAULT NULL,
+  `username`             varchar(128) NOT NULL,
+  `remote_host`          varchar(256) DEFAULT NULL,
+  `start_date`           datetime     NOT NULL,
+  `end_date`             datetime     DEFAULT NULL,
+
+  PRIMARY KEY (history_id),
+  KEY `user_id` (`user_id`),
+  KEY `start_date` (`start_date`),
+  KEY `end_date` (`end_date`),
+  KEY `user_start_date` (`user_id`, `start_date`),
+
+  CONSTRAINT guacamole_user_history_ibfk_1
+    FOREIGN KEY (user_id)
+    REFERENCES guacamole_user (user_id) ON DELETE SET NULL
+
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+--
 -- User password history
 --
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.14.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.14.sql
index 01be93a..ee586bf 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.14.sql
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.14.sql
@@ -37,3 +37,34 @@
 
 ALTER TABLE guacamole_connection_history
     ADD COLUMN remote_host VARCHAR(256) DEFAULT NULL;
+
+--
+-- Add covering index for connection history connection and start date
+--
+
+ALTER TABLE guacamole_connection_history ADD KEY (connection_id, start_date);
+
+--
+-- User login/logout history
+--
+
+CREATE TABLE guacamole_user_history (
+
+  `history_id`           int(11)      NOT NULL AUTO_INCREMENT,
+  `user_id`              int(11)      DEFAULT NULL,
+  `username`             varchar(128) NOT NULL,
+  `remote_host`          varchar(256) DEFAULT NULL,
+  `start_date`           datetime     NOT NULL,
+  `end_date`             datetime     DEFAULT NULL,
+
+  PRIMARY KEY (history_id),
+  KEY `user_id` (`user_id`),
+  KEY `start_date` (`start_date`),
+  KEY `end_date` (`end_date`),
+  KEY `user_start_date` (`user_id`, `start_date`),
+
+  CONSTRAINT guacamole_user_history_ibfk_1
+    FOREIGN KEY (user_id)
+    REFERENCES guacamole_user (user_id) ON DELETE SET NULL
+
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
index 97c2e54..cbffdd4 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
@@ -39,6 +39,7 @@
                 javaType="org.apache.guacamole.net.auth.GuacamoleProxyConfiguration$EncryptionMethod"/>
         <result column="connection_weight"        property="connectionWeight"      jdbcType="INTEGER"/>
         <result column="failover_only"            property="failoverOnly"          jdbcType="BOOLEAN"/>
+        <result column="last_active"              property="lastActive"            jdbcType="TIMESTAMP"/>
 
         <!-- Associated sharing profiles -->
         <collection property="sharingProfileIdentifiers" resultSet="sharingProfiles" ofType="java.lang.String"
@@ -89,8 +90,8 @@
             resultSets="connections,sharingProfiles">
 
         SELECT
-            connection_id,
-            connection_name,
+            guacamole_connection.connection_id,
+            guacamole_connection.connection_name,
             parent_id,
             protocol,
             max_connections,
@@ -99,13 +100,16 @@
             proxy_port,
             proxy_encryption_method,
             connection_weight,
-            failover_only
+            failover_only,
+            MAX(start_date) AS last_active
         FROM guacamole_connection
-        WHERE connection_id IN
+        LEFT JOIN guacamole_connection_history ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
+        WHERE guacamole_connection.connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
-            </foreach>;
+            </foreach>
+        GROUP BY guacamole_connection.connection_id;
 
         SELECT primary_connection_id, sharing_profile_id
         FROM guacamole_sharing_profile
@@ -123,7 +127,7 @@
 
         SELECT
             guacamole_connection.connection_id,
-            connection_name,
+            guacamole_connection.connection_name,
             parent_id,
             protocol,
             max_connections,
@@ -132,16 +136,19 @@
             proxy_port,
             proxy_encryption_method,
             connection_weight,
-            failover_only
+            failover_only,
+            MAX(start_date) AS last_active
         FROM guacamole_connection
         JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection.connection_id
+        LEFT JOIN guacamole_connection_history ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
         WHERE guacamole_connection.connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
-            AND user_id = #{user.objectID,jdbcType=INTEGER}
-            AND permission = 'READ';
+            AND guacamole_connection_permission.user_id = #{user.objectID,jdbcType=INTEGER}
+            AND permission = 'READ'
+        GROUP BY guacamole_connection.connection_id;
 
         SELECT primary_connection_id, guacamole_sharing_profile.sharing_profile_id
         FROM guacamole_sharing_profile
@@ -160,8 +167,8 @@
     <select id="selectOneByName" resultMap="ConnectionResultMap">
 
         SELECT
-            connection_id,
-            connection_name,
+            guacamole_connection.connection_id,
+            guacamole_connection.connection_name,
             parent_id,
             protocol,
             max_connections,
@@ -170,12 +177,15 @@
             proxy_port,
             proxy_encryption_method,
             connection_weight,
-            failover_only
+            failover_only,
+            MAX(start_date) AS last_active
         FROM guacamole_connection
+        LEFT JOIN guacamole_connection_history ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
         WHERE 
             <if test="parentIdentifier != null">parent_id = #{parentIdentifier,jdbcType=VARCHAR}</if>
             <if test="parentIdentifier == null">parent_id IS NULL</if>
-            AND connection_name = #{name,jdbcType=VARCHAR}
+            AND guacamole_connection.connection_name = #{name,jdbcType=VARCHAR}
+        GROUP BY guacamole_connection.connection_id
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
index 4ab1182..c9e4f70 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
@@ -41,6 +41,7 @@
         <result column="email_address"       property="emailAddress"       jdbcType="VARCHAR"/>
         <result column="organization"        property="organization"       jdbcType="VARCHAR"/>
         <result column="organizational_role" property="organizationalRole" jdbcType="VARCHAR"/>
+        <result column="last_active"         property="lastActive"         jdbcType="TIMESTAMP"/>
     </resultMap>
 
     <!-- Select all usernames -->
@@ -63,8 +64,8 @@
     <select id="select" resultMap="UserResultMap">
 
         SELECT
-            user_id,
-            username,
+            guacamole_user.user_id,
+            guacamole_user.username,
             password_hash,
             password_salt,
             password_date,
@@ -78,13 +79,16 @@
             full_name,
             email_address,
             organization,
-            organizational_role
+            organizational_role,
+            MAX(start_date) AS last_active
         FROM guacamole_user
-        WHERE username IN
+        LEFT JOIN guacamole_user_history ON guacamole_user_history.user_id = guacamole_user.user_id
+        WHERE guacamole_user.username IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
+        GROUP BY guacamole_user.user_id
 
     </select>
 
@@ -93,7 +97,7 @@
 
         SELECT
             guacamole_user.user_id,
-            username,
+            guacamole_user.username,
             password_hash,
             password_salt,
             password_date,
@@ -107,16 +111,19 @@
             full_name,
             email_address,
             organization,
-            organizational_role
+            organizational_role,
+            MAX(start_date) AS last_active
         FROM guacamole_user
         JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
-        WHERE username IN
+        LEFT JOIN guacamole_user_history ON guacamole_user_history.user_id = guacamole_user.user_id
+        WHERE guacamole_user.username IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND guacamole_user_permission.user_id = #{user.objectID,jdbcType=INTEGER}
             AND permission = 'READ'
+        GROUP BY guacamole_user.user_id
 
     </select>
 
@@ -124,8 +131,8 @@
     <select id="selectOne" resultMap="UserResultMap">
 
         SELECT
-            user_id,
-            username,
+            guacamole_user.user_id,
+            guacamole_user.username,
             password_hash,
             password_salt,
             password_date,
@@ -139,10 +146,13 @@
             full_name,
             email_address,
             organization,
-            organizational_role
+            organizational_role,
+            MAX(start_date) AS last_active
         FROM guacamole_user
+        LEFT JOIN guacamole_user_history ON guacamole_user_history.user_id = guacamole_user.user_id
         WHERE
-            username = #{username,jdbcType=VARCHAR}
+            guacamole_user.username = #{username,jdbcType=VARCHAR}
+        GROUP BY guacamole_user.user_id
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
new file mode 100644
index 0000000..bbae03b
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+    "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+
+<mapper namespace="org.apache.guacamole.auth.jdbc.user.UserRecordMapper" >
+
+    <!-- Result mapper for system permissions -->
+    <resultMap id="UserRecordResultMap" type="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
+        <id     column="history_id"  property="recordID"   jdbcType="INTEGER"/>
+        <result column="remote_host" property="remoteHost" jdbcType="VARCHAR"/>
+        <result column="user_id"     property="userID"     jdbcType="INTEGER"/>
+        <result column="username"    property="username"   jdbcType="VARCHAR"/>
+        <result column="start_date"  property="startDate"  jdbcType="TIMESTAMP"/>
+        <result column="end_date"    property="endDate"    jdbcType="TIMESTAMP"/>
+    </resultMap>
+
+    <!-- Select all user records from a given user -->
+    <select id="select" resultMap="UserRecordResultMap">
+
+        SELECT
+            guacamole_user_history.remote_host,
+            guacamole_user_history.user_id,
+            guacamole_user_history.username,
+            guacamole_user_history.start_date,
+            guacamole_user_history.end_date
+        FROM guacamole_user_history
+        JOIN guacamole_user ON guacamole_user_history.user_id = guacamole_user.user_id
+        WHERE
+            guacamole_user.username = #{username,jdbcType=VARCHAR}
+        ORDER BY
+            guacamole_user_history.start_date DESC,
+            guacamole_user_history.end_date DESC
+
+    </select>
+
+    <!-- Insert the given user record -->
+    <insert id="insert" useGeneratedKeys="true" keyProperty="record.recordID"
+            parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
+
+        INSERT INTO guacamole_user_history (
+            remote_host,
+            user_id,
+            username,
+            start_date,
+            end_date
+        )
+        VALUES (
+            #{record.remoteHost,jdbcType=VARCHAR},
+            (SELECT user_id FROM guacamole_user
+             WHERE username = #{record.username,jdbcType=VARCHAR}),
+            #{record.username,jdbcType=VARCHAR},
+            #{record.startDate,jdbcType=TIMESTAMP},
+            #{record.endDate,jdbcType=TIMESTAMP}
+        )
+
+    </insert>
+
+    <!-- Update the given user record -->
+    <update id="update" parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
+        UPDATE guacamole_user_history
+        SET remote_host = #{record.remoteHost,jdbcType=VARCHAR},
+            user_id     = (SELECT user_id FROM guacamole_user
+                           WHERE username = #{record.username,jdbcType=VARCHAR}),
+            username    = #{record.username,jdbcType=VARCHAR},
+            start_date  = #{record.startDate,jdbcType=TIMESTAMP},
+            end_date    = #{record.endDate,jdbcType=TIMESTAMP}
+        WHERE history_id = #{record.recordID,jdbcType=INTEGER}
+    </update>
+
+    <!-- Search for specific user records -->
+    <select id="search" resultMap="UserRecordResultMap">
+
+        SELECT
+            guacamole_user_history.remote_host,
+            guacamole_user_history.user_id,
+            guacamole_user_history.username,
+            guacamole_user_history.start_date,
+            guacamole_user_history.end_date
+        FROM guacamole_user_history
+
+        <!-- Search terms -->
+        <foreach collection="terms" item="term"
+                 open="WHERE " separator=" AND ">
+            (
+
+                guacamole_user_history.user_id IN (
+                    SELECT user_id
+                    FROM guacamole_user
+                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
+                )
+
+                <if test="term.startDate != null and term.endDate != null">
+                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                </if>
+
+            )
+        </foreach>
+
+        <!-- Bind sort property enum values for sake of readability -->
+        <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
+
+        <!-- Sort predicates -->
+        <foreach collection="sortPredicates" item="sortPredicate"
+                 open="ORDER BY " separator=", ">
+            <choose>
+                <when test="sortPredicate.property == START_DATE">guacamole_user_history.start_date</when>
+                <otherwise>1</otherwise>
+            </choose>
+            <if test="sortPredicate.descending">DESC</if>
+        </foreach>
+
+        LIMIT #{limit,jdbcType=INTEGER}
+
+    </select>
+
+    <!-- Search for specific user records -->
+    <select id="searchReadable" resultMap="UserRecordResultMap">
+
+        SELECT
+            guacamole_user_history.remote_host,
+            guacamole_user_history.user_id,
+            guacamole_user_history.username,
+            guacamole_user_history.start_date,
+            guacamole_user_history.end_date
+        FROM guacamole_user_history
+
+        <!-- Restrict to readable users -->
+        JOIN guacamole_user_permission ON
+                guacamole_user_history.user_id       = guacamole_user_permission.affected_user_id
+            AND guacamole_user_permission.user_id    = #{user.objectID,jdbcType=INTEGER}
+            AND guacamole_user_permission.permission = 'READ'
+
+        <!-- Search terms -->
+        <foreach collection="terms" item="term"
+                 open="WHERE " separator=" AND ">
+            (
+
+                guacamole_user_history.user_id IN (
+                    SELECT user_id
+                    FROM guacamole_user
+                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
+                )
+
+                <if test="term.startDate != null and term.endDate != null">
+                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                </if>
+
+            )
+        </foreach>
+
+        <!-- Bind sort property enum values for sake of readability -->
+        <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
+
+        <!-- Sort predicates -->
+        <foreach collection="sortPredicates" item="sortPredicate"
+                 open="ORDER BY " separator=", ">
+            <choose>
+                <when test="sortPredicate.property == START_DATE">guacamole_user_history.start_date</when>
+                <otherwise>1</otherwise>
+            </choose>
+            <if test="sortPredicate.descending">DESC</if>
+        </foreach>
+
+        LIMIT #{limit,jdbcType=INTEGER}
+
+    </select>
+
+</mapper>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql
index e4015d3..97780a5 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql
@@ -438,6 +438,42 @@
 CREATE INDEX guacamole_connection_history_end_date
     ON guacamole_connection_history(end_date);
 
+CREATE INDEX guacamole_connection_history_connection_id_start_date
+    ON guacamole_connection_history(connection_id, start_date);
+
+--
+-- User login/logout history
+--
+
+CREATE TABLE guacamole_user_history (
+
+  history_id           serial       NOT NULL,
+  user_id              integer      DEFAULT NULL,
+  username             varchar(128) NOT NULL,
+  remote_host          varchar(256) DEFAULT NULL,
+  start_date           timestamptz  NOT NULL,
+  end_date             timestamptz  DEFAULT NULL,
+
+  PRIMARY KEY (history_id),
+
+  CONSTRAINT guacamole_user_history_ibfk_1
+    FOREIGN KEY (user_id)
+    REFERENCES guacamole_user (user_id) ON DELETE SET NULL
+
+);
+
+CREATE INDEX guacamole_user_history_user_id
+    ON guacamole_user_history(user_id);
+
+CREATE INDEX guacamole_user_history_start_date
+    ON guacamole_user_history(start_date);
+
+CREATE INDEX guacamole_user_history_end_date
+    ON guacamole_user_history(end_date);
+
+CREATE INDEX guacamole_user_history_user_id_start_date
+    ON guacamole_user_history(user_id, start_date);
+
 --
 -- User password history
 --
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.14.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.14.sql
index 157e896..534d4dc 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.14.sql
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.14.sql
@@ -37,3 +37,43 @@
 
 ALTER TABLE guacamole_connection_history
     ADD COLUMN remote_host VARCHAR(256) DEFAULT NULL;
+
+--
+-- Add covering index for connection history connection and start date
+--
+
+CREATE INDEX guacamole_connection_history_connection_id_start_date
+    ON guacamole_connection_history(connection_id, start_date);
+
+--
+-- User login/logout history
+--
+
+CREATE TABLE guacamole_user_history (
+
+  history_id           serial       NOT NULL,
+  user_id              integer      DEFAULT NULL,
+  username             varchar(128) NOT NULL,
+  remote_host          varchar(256) DEFAULT NULL,
+  start_date           timestamptz  NOT NULL,
+  end_date             timestamptz  DEFAULT NULL,
+
+  PRIMARY KEY (history_id),
+
+  CONSTRAINT guacamole_user_history_ibfk_1
+    FOREIGN KEY (user_id)
+    REFERENCES guacamole_user (user_id) ON DELETE SET NULL
+
+);
+
+CREATE INDEX guacamole_user_history_user_id
+    ON guacamole_user_history(user_id);
+
+CREATE INDEX guacamole_user_history_start_date
+    ON guacamole_user_history(start_date);
+
+CREATE INDEX guacamole_user_history_end_date
+    ON guacamole_user_history(end_date);
+
+CREATE INDEX guacamole_user_history_user_id_start_date
+    ON guacamole_user_history(user_id, start_date);
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
index dd9265d..dc8fdd4 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
@@ -39,6 +39,7 @@
                 javaType="org.apache.guacamole.net.auth.GuacamoleProxyConfiguration$EncryptionMethod"/>
         <result column="connection_weight"        property="connectionWeight"      jdbcType="INTEGER"/>
         <result column="failover_only"            property="failoverOnly"          jdbcType="BOOLEAN"/>
+        <result column="last_active"              property="lastActive"            jdbcType="TIMESTAMP"/>
 
         <!-- Associated sharing profiles -->
         <collection property="sharingProfileIdentifiers" resultSet="sharingProfiles" ofType="java.lang.String"
@@ -89,8 +90,8 @@
             resultSets="connections,sharingProfiles">
 
         SELECT
-            connection_id,
-            connection_name,
+            guacamole_connection.connection_id,
+            guacamole_connection.connection_name,
             parent_id,
             protocol,
             max_connections,
@@ -99,13 +100,16 @@
             proxy_port,
             proxy_encryption_method,
             connection_weight,
-            failover_only
+            failover_only,
+            MAX(start_date) AS last_active
         FROM guacamole_connection
-        WHERE connection_id IN
+        LEFT JOIN guacamole_connection_history ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
+        WHERE guacamole_connection.connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
-            </foreach>;
+            </foreach>
+        GROUP BY guacamole_connection.connection_id;
 
         SELECT primary_connection_id, sharing_profile_id
         FROM guacamole_sharing_profile
@@ -123,7 +127,7 @@
 
         SELECT
             guacamole_connection.connection_id,
-            connection_name,
+            guacamole_connection.connection_name,
             parent_id,
             protocol,
             max_connections,
@@ -132,16 +136,19 @@
             proxy_port,
             proxy_encryption_method,
             connection_weight,
-            failover_only
+            failover_only,
+            MAX(start_date) AS last_active
         FROM guacamole_connection
         JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection.connection_id
+        LEFT JOIN guacamole_connection_history ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
         WHERE guacamole_connection.connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
             </foreach>
-            AND user_id = #{user.objectID,jdbcType=INTEGER}
-            AND permission = 'READ';
+            AND guacamole_connection_permission.user_id = #{user.objectID,jdbcType=INTEGER}
+            AND permission = 'READ'
+        GROUP BY guacamole_connection.connection_id;
 
         SELECT primary_connection_id, guacamole_sharing_profile.sharing_profile_id
         FROM guacamole_sharing_profile
@@ -160,8 +167,8 @@
     <select id="selectOneByName" resultMap="ConnectionResultMap">
 
         SELECT
-            connection_id,
-            connection_name,
+            guacamole_connection.connection_id,
+            guacamole_connection.connection_name,
             parent_id,
             protocol,
             max_connections,
@@ -170,12 +177,15 @@
             proxy_port,
             proxy_encryption_method,
             connection_weight,
-            failover_only
+            failover_only,
+            MAX(start_date) AS last_active
         FROM guacamole_connection
+        LEFT JOIN guacamole_connection_history ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
         WHERE 
             <if test="parentIdentifier != null">parent_id = #{parentIdentifier,jdbcType=INTEGER}::integer</if>
             <if test="parentIdentifier == null">parent_id IS NULL</if>
-            AND connection_name = #{name,jdbcType=VARCHAR}
+            AND guacamole_connection.connection_name = #{name,jdbcType=VARCHAR}
+        GROUP BY guacamole_connection.connection_id
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
index 569a8ac..c106a8f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
@@ -41,6 +41,7 @@
         <result column="email_address"       property="emailAddress"       jdbcType="VARCHAR"/>
         <result column="organization"        property="organization"       jdbcType="VARCHAR"/>
         <result column="organizational_role" property="organizationalRole" jdbcType="VARCHAR"/>
+        <result column="last_active"         property="lastActive"         jdbcType="TIMESTAMP"/>
     </resultMap>
 
     <!-- Select all usernames -->
@@ -63,8 +64,8 @@
     <select id="select" resultMap="UserResultMap">
 
         SELECT
-            user_id,
-            username,
+            guacamole_user.user_id,
+            guacamole_user.username,
             password_hash,
             password_salt,
             password_date,
@@ -78,13 +79,16 @@
             full_name,
             email_address,
             organization,
-            organizational_role
+            organizational_role,
+            MAX(start_date) AS last_active
         FROM guacamole_user
-        WHERE username IN
+        LEFT JOIN guacamole_user_history ON guacamole_user_history.user_id = guacamole_user.user_id
+        WHERE guacamole_user.username IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
+        GROUP BY guacamole_user.user_id
 
     </select>
 
@@ -93,7 +97,7 @@
 
         SELECT
             guacamole_user.user_id,
-            username,
+            guacamole_user.username,
             password_hash,
             password_salt,
             password_date,
@@ -107,16 +111,19 @@
             full_name,
             email_address,
             organization,
-            organizational_role
+            organizational_role,
+            MAX(start_date) AS last_active
         FROM guacamole_user
         JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
-        WHERE username IN
+        LEFT JOIN guacamole_user_history ON guacamole_user_history.user_id = guacamole_user.user_id
+        WHERE guacamole_user.username IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND guacamole_user_permission.user_id = #{user.objectID,jdbcType=INTEGER}
             AND permission = 'READ'
+        GROUP BY guacamole_user.user_id
 
     </select>
 
@@ -124,8 +131,8 @@
     <select id="selectOne" resultMap="UserResultMap">
 
         SELECT
-            user_id,
-            username,
+            guacamole_user.user_id,
+            guacamole_user.username,
             password_hash,
             password_salt,
             password_date,
@@ -139,10 +146,13 @@
             full_name,
             email_address,
             organization,
-            organizational_role
+            organizational_role,
+            MAX(start_date) AS last_active
         FROM guacamole_user
+        LEFT JOIN guacamole_user_history ON guacamole_user_history.user_id = guacamole_user.user_id
         WHERE
-            username = #{username,jdbcType=VARCHAR}
+            guacamole_user.username = #{username,jdbcType=VARCHAR}
+        GROUP BY guacamole_user.user_id
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
new file mode 100644
index 0000000..014b38a
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+    "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+
+<mapper namespace="org.apache.guacamole.auth.jdbc.user.UserRecordMapper" >
+
+    <!-- Result mapper for system permissions -->
+    <resultMap id="UserRecordResultMap" type="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
+        <id     column="history_id"  property="recordID"   jdbcType="INTEGER"/>
+        <result column="remote_host" property="remoteHost" jdbcType="VARCHAR"/>
+        <result column="user_id"     property="userID"     jdbcType="INTEGER"/>
+        <result column="username"    property="username"   jdbcType="VARCHAR"/>
+        <result column="start_date"  property="startDate"  jdbcType="TIMESTAMP"/>
+        <result column="end_date"    property="endDate"    jdbcType="TIMESTAMP"/>
+    </resultMap>
+
+    <!-- Select all user records from a given user -->
+    <select id="select" resultMap="UserRecordResultMap">
+
+        SELECT
+            guacamole_user_history.remote_host,
+            guacamole_user_history.user_id,
+            guacamole_user_history.username,
+            guacamole_user_history.start_date,
+            guacamole_user_history.end_date
+        FROM guacamole_user_history
+        JOIN guacamole_user ON guacamole_user_history.user_id = guacamole_user.user_id
+        WHERE
+            guacamole_user.username = #{username,jdbcType=VARCHAR}
+        ORDER BY
+            guacamole_user_history.start_date DESC,
+            guacamole_user_history.end_date DESC
+
+    </select>
+
+    <!-- Insert the given user record -->
+    <insert id="insert" useGeneratedKeys="true" keyProperty="record.recordID"
+            parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
+
+        INSERT INTO guacamole_user_history (
+            remote_host,
+            user_id,
+            username,
+            start_date,
+            end_date
+        )
+        VALUES (
+            #{record.remoteHost,jdbcType=VARCHAR},
+            (SELECT user_id FROM guacamole_user
+             WHERE username = #{record.username,jdbcType=VARCHAR}),
+            #{record.username,jdbcType=VARCHAR},
+            #{record.startDate,jdbcType=TIMESTAMP},
+            #{record.endDate,jdbcType=TIMESTAMP}
+        )
+
+    </insert>
+
+    <!-- Update the given user record -->
+    <update id="update" parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
+        UPDATE guacamole_user_history
+        SET remote_host = #{record.remoteHost,jdbcType=VARCHAR},
+            user_id     = (SELECT user_id FROM guacamole_user
+                           WHERE username = #{record.username,jdbcType=VARCHAR}),
+            username    = #{record.username,jdbcType=VARCHAR},
+            start_date  = #{record.startDate,jdbcType=TIMESTAMP},
+            end_date    = #{record.endDate,jdbcType=TIMESTAMP}
+        WHERE history_id = #{record.recordID,jdbcType=INTEGER}::integer
+    </update>
+
+    <!-- Search for specific user records -->
+    <select id="search" resultMap="UserRecordResultMap">
+
+        SELECT
+            guacamole_user_history.remote_host,
+            guacamole_user_history.user_id,
+            guacamole_user_history.username,
+            guacamole_user_history.start_date,
+            guacamole_user_history.end_date
+        FROM guacamole_user_history
+
+        <!-- Search terms -->
+        <foreach collection="terms" item="term"
+                 open="WHERE " separator=" AND ">
+            (
+
+                guacamole_user_history.user_id IN (
+                    SELECT user_id
+                    FROM guacamole_user
+                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
+                )
+
+                <if test="term.startDate != null and term.endDate != null">
+                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                </if>
+
+            )
+        </foreach>
+
+        <!-- Bind sort property enum values for sake of readability -->
+        <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
+
+        <!-- Sort predicates -->
+        <foreach collection="sortPredicates" item="sortPredicate"
+                 open="ORDER BY " separator=", ">
+            <choose>
+                <when test="sortPredicate.property == START_DATE">guacamole_user_history.start_date</when>
+                <otherwise>1</otherwise>
+            </choose>
+            <if test="sortPredicate.descending">DESC</if>
+        </foreach>
+
+        LIMIT #{limit,jdbcType=INTEGER}
+
+    </select>
+
+    <!-- Search for specific user records -->
+    <select id="searchReadable" resultMap="UserRecordResultMap">
+
+        SELECT
+            guacamole_user_history.remote_host,
+            guacamole_user_history.user_id,
+            guacamole_user_history.username,
+            guacamole_user_history.start_date,
+            guacamole_user_history.end_date
+        FROM guacamole_user_history
+
+        <!-- Restrict to readable users -->
+        JOIN guacamole_user_permission ON
+                guacamole_user_history.user_id       = guacamole_user_permission.affected_user_id
+            AND guacamole_user_permission.user_id    = #{user.objectID,jdbcType=INTEGER}
+            AND guacamole_user_permission.permission = 'READ'
+
+        <!-- Search terms -->
+        <foreach collection="terms" item="term"
+                 open="WHERE " separator=" AND ">
+            (
+
+                guacamole_user_history.user_id IN (
+                    SELECT user_id
+                    FROM guacamole_user
+                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
+                )
+
+                <if test="term.startDate != null and term.endDate != null">
+                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                </if>
+
+            )
+        </foreach>
+
+        <!-- Bind sort property enum values for sake of readability -->
+        <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
+
+        <!-- Sort predicates -->
+        <foreach collection="sortPredicates" item="sortPredicate"
+                 open="ORDER BY " separator=", ">
+            <choose>
+                <when test="sortPredicate.property == START_DATE">guacamole_user_history.start_date</when>
+                <otherwise>1</otherwise>
+            </choose>
+            <if test="sortPredicate.descending">DESC</if>
+        </foreach>
+
+        LIMIT #{limit,jdbcType=INTEGER}
+
+    </select>
+
+</mapper>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/schema/001-create-schema.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/schema/001-create-schema.sql
index 91ba377..060503a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/schema/001-create-schema.sql
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/schema/001-create-schema.sql
@@ -502,6 +502,43 @@
 
 CREATE NONCLUSTERED INDEX [IX_guacamole_connection_history_end_date]
     ON [guacamole_connection_history] ([end_date]);
+
+CREATE NONCLUSTERED INDEX [IX_guacamole_connection_history_connection_id_start_date]
+    ON [guacamole_connection_history] ([connection_id], [start_date]);
+GO
+
+--
+-- User login/logout history
+--
+
+CREATE TABLE [guacamole_user_history] (
+
+    [history_id]           [int] IDENTITY(1,1) NOT NULL,
+    [user_id]              [int]               DEFAULT NULL,
+    [username]             [nvarchar](128)     NOT NULL,
+    [remote_host]          [nvarchar](256)     DEFAULT NULL,
+    [start_date]           [datetime]          NOT NULL,
+    [end_date]             [datetime]          DEFAULT NULL,
+
+    PRIMARY KEY (history_id),
+
+    CONSTRAINT FK_guacamole_user_history_user_id
+        FOREIGN KEY (user_id)
+        REFERENCES guacamole_user (user_id) ON DELETE SET NULL
+
+);
+
+CREATE NONCLUSTERED INDEX [IX_guacamole_user_history_user_id]
+    ON [guacamole_user_history] ([user_id]);
+
+CREATE NONCLUSTERED INDEX [IX_guacamole_user_history_start_date]
+    ON [guacamole_user_history] ([start_date]);
+
+CREATE NONCLUSTERED INDEX [IX_guacamole_user_history_end_date]
+    ON [guacamole_user_history] ([end_date]);
+
+CREATE NONCLUSTERED INDEX [IX_guacamole_user_history_user_id_start_date]
+    ON [guacamole_user_history] ([user_id], [start_date]);
 GO
 
 --
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
index 3e6819f..19c3912 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
@@ -39,6 +39,7 @@
                 javaType="org.apache.guacamole.net.auth.GuacamoleProxyConfiguration$EncryptionMethod"/>
         <result column="connection_weight"        property="connectionWeight"      jdbcType="INTEGER"/>
         <result column="failover_only"            property="failoverOnly"          jdbcType="BOOLEAN"/>
+        <result column="last_active"              property="lastActive"            jdbcType="TIMESTAMP"/>
 
         <!-- Associated sharing profiles -->
         <collection property="sharingProfileIdentifiers" resultSet="sharingProfiles" ofType="java.lang.String"
@@ -89,8 +90,8 @@
             resultSets="connections,sharingProfiles">
 
         SELECT
-            connection_id,
-            connection_name,
+            [guacamole_connection].connection_id,
+            [guacamole_connection].connection_name,
             parent_id,
             protocol,
             max_connections,
@@ -99,9 +100,14 @@
             proxy_port,
             proxy_encryption_method,
             connection_weight,
-            failover_only
+            failover_only,
+            (
+                SELECT MAX(start_date)
+                FROM [guacamole_connection_history]
+                WHERE [guacamole_connection_history].connection_id = [guacamole_connection].connection_id
+            ) AS last_active
         FROM [guacamole_connection]
-        WHERE connection_id IN
+        WHERE [guacamole_connection].connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
@@ -123,7 +129,7 @@
 
         SELECT
             [guacamole_connection].connection_id,
-            connection_name,
+            [guacamole_connection].connection_name,
             parent_id,
             protocol,
             max_connections,
@@ -132,7 +138,12 @@
             proxy_port,
             proxy_encryption_method,
             connection_weight,
-            failover_only
+            failover_only,
+            (
+                SELECT MAX(start_date)
+                FROM [guacamole_connection_history]
+                WHERE [guacamole_connection_history].connection_id = [guacamole_connection].connection_id
+            ) AS last_active
         FROM [guacamole_connection]
         JOIN [guacamole_connection_permission] ON [guacamole_connection_permission].connection_id = [guacamole_connection].connection_id
         WHERE [guacamole_connection].connection_id IN
@@ -140,7 +151,7 @@
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
             </foreach>
-            AND user_id = #{user.objectID,jdbcType=INTEGER}
+            AND [guacamole_connection_permission].user_id = #{user.objectID,jdbcType=INTEGER}
             AND permission = 'READ';
 
         SELECT primary_connection_id, [guacamole_sharing_profile].sharing_profile_id
@@ -160,8 +171,8 @@
     <select id="selectOneByName" resultMap="ConnectionResultMap">
 
         SELECT
-            connection_id,
-            connection_name,
+            [guacamole_connection].connection_id,
+            [guacamole_connection].connection_name,
             parent_id,
             protocol,
             max_connections,
@@ -170,12 +181,17 @@
             proxy_port,
             proxy_encryption_method,
             connection_weight,
-            failover_only
+            failover_only,
+            (
+                SELECT MAX(start_date)
+                FROM [guacamole_connection_history]
+                WHERE [guacamole_connection_history].connection_id = [guacamole_connection].connection_id
+            ) AS last_active
         FROM [guacamole_connection]
         WHERE 
             <if test="parentIdentifier != null">parent_id = #{parentIdentifier,jdbcType=INTEGER}</if>
             <if test="parentIdentifier == null">parent_id IS NULL</if>
-            AND connection_name = #{name,jdbcType=VARCHAR}
+            AND [guacamole_connection].connection_name = #{name,jdbcType=VARCHAR}
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
index 6df6cf2..24db013 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
@@ -41,6 +41,7 @@
         <result column="email_address"       property="emailAddress"       jdbcType="VARCHAR"/>
         <result column="organization"        property="organization"       jdbcType="VARCHAR"/>
         <result column="organizational_role" property="organizationalRole" jdbcType="VARCHAR"/>
+        <result column="last_active"         property="lastActive"         jdbcType="TIMESTAMP"/>
     </resultMap>
 
     <!-- Select all usernames -->
@@ -63,8 +64,8 @@
     <select id="select" resultMap="UserResultMap">
 
         SELECT
-            user_id,
-            username,
+            [guacamole_user].user_id,
+            [guacamole_user].username,
             password_hash,
             password_salt,
             password_date,
@@ -78,13 +79,18 @@
             full_name,
             email_address,
             organization,
-            organizational_role
+            organizational_role,
+            (
+                SELECT MAX(start_date)
+                FROM [guacamole_user_history]
+                WHERE [guacamole_user_history].user_id = [guacamole_user].user_id
+            ) AS last_active
         FROM [guacamole_user]
-        WHERE username IN
+        WHERE [guacamole_user].username IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
-            </foreach>
+            </foreach>;
 
     </select>
 
@@ -93,7 +99,7 @@
 
         SELECT
             [guacamole_user].user_id,
-            username,
+            [guacamole_user].username,
             password_hash,
             password_salt,
             password_date,
@@ -107,10 +113,15 @@
             full_name,
             email_address,
             organization,
-            organizational_role
+            organizational_role,
+            (
+                SELECT MAX(start_date)
+                FROM [guacamole_user_history]
+                WHERE [guacamole_user_history].user_id = [guacamole_user].user_id
+            ) AS last_active
         FROM [guacamole_user]
         JOIN [guacamole_user_permission] ON affected_user_id = [guacamole_user].user_id
-        WHERE username IN
+        WHERE [guacamole_user].username IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
@@ -124,8 +135,8 @@
     <select id="selectOne" resultMap="UserResultMap">
 
         SELECT
-            user_id,
-            username,
+            [guacamole_user].user_id,
+            [guacamole_user].username,
             password_hash,
             password_salt,
             password_date,
@@ -139,10 +150,16 @@
             full_name,
             email_address,
             organization,
-            organizational_role
+            organizational_role,
+            (
+                SELECT MAX(start_date)
+                FROM [guacamole_user_history]
+                WHERE [guacamole_user_history].user_id = [guacamole_user].user_id
+            ) AS last_active
         FROM [guacamole_user]
+        LEFT JOIN [guacamole_user_history] ON [guacamole_user_history].user_id = [guacamole_user].user_id
         WHERE
-            username = #{username,jdbcType=VARCHAR}
+            [guacamole_user].username = #{username,jdbcType=VARCHAR}
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
new file mode 100644
index 0000000..0143dda
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+    "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+
+<mapper namespace="org.apache.guacamole.auth.jdbc.user.UserRecordMapper" >
+
+    <!-- Result mapper for system permissions -->
+    <resultMap id="UserRecordResultMap" type="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
+        <id     column="history_id"  property="recordID"   jdbcType="INTEGER"/>
+        <result column="remote_host" property="remoteHost" jdbcType="VARCHAR"/>
+        <result column="user_id"     property="userID"     jdbcType="INTEGER"/>
+        <result column="username"    property="username"   jdbcType="VARCHAR"/>
+        <result column="start_date"  property="startDate"  jdbcType="TIMESTAMP"/>
+        <result column="end_date"    property="endDate"    jdbcType="TIMESTAMP"/>
+    </resultMap>
+
+    <!-- Select all user records from a given user -->
+    <select id="select" resultMap="UserRecordResultMap">
+
+        SELECT
+            [guacamole_user_history].remote_host,
+            [guacamole_user_history].user_id,
+            [guacamole_user_history].username,
+            [guacamole_user_history].start_date,
+            [guacamole_user_history].end_date
+        FROM [guacamole_user_history]
+        JOIN [guacamole_user] ON [guacamole_user_history].user_id = [guacamole_user].user_id
+        WHERE
+            [guacamole_user].username = #{username,jdbcType=VARCHAR}
+        ORDER BY
+            [guacamole_user_history].start_date DESC,
+            [guacamole_user_history].end_date DESC
+
+    </select>
+
+    <!-- Insert the given user record -->
+    <insert id="insert" useGeneratedKeys="true" keyProperty="record.recordID"
+            parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
+
+        INSERT INTO [guacamole_user_history] (
+            remote_host,
+            user_id,
+            username,
+            start_date,
+            end_date
+        )
+        VALUES (
+            #{record.remoteHost,jdbcType=VARCHAR},
+            (SELECT user_id FROM [guacamole_user]
+             WHERE username = #{record.username,jdbcType=VARCHAR}),
+            #{record.username,jdbcType=VARCHAR},
+            #{record.startDate,jdbcType=TIMESTAMP},
+            #{record.endDate,jdbcType=TIMESTAMP}
+        )
+
+    </insert>
+
+    <!-- Update the given user record -->
+    <update id="update" parameterType="org.apache.guacamole.auth.jdbc.base.ActivityRecordModel">
+        UPDATE [guacamole_user_history]
+        SET remote_host = #{record.remoteHost,jdbcType=VARCHAR},
+            user_id     = (SELECT user_id FROM [guacamole_user]
+                           WHERE username = #{record.username,jdbcType=VARCHAR}),
+            username    = #{record.username,jdbcType=VARCHAR},
+            start_date  = #{record.startDate,jdbcType=TIMESTAMP},
+            end_date    = #{record.endDate,jdbcType=TIMESTAMP}
+        WHERE history_id = #{record.recordID,jdbcType=INTEGER}
+    </update>
+
+    <!-- Search for specific user records -->
+    <select id="search" resultMap="UserRecordResultMap">
+
+        SELECT
+            [guacamole_user_history].remote_host,
+            [guacamole_user_history].user_id,
+            [guacamole_user_history].username,
+            [guacamole_user_history].start_date,
+            [guacamole_user_history].end_date
+        FROM [guacamole_user_history]
+
+        <!-- Search terms -->
+        <foreach collection="terms" item="term"
+                 open="WHERE " separator=" AND ">
+            (
+
+                [guacamole_user_history].user_id IN (
+                    SELECT user_id
+                    FROM [guacamole_user]
+                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
+                )
+
+                <if test="term.startDate != null and term.endDate != null">
+                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                </if>
+
+            )
+        </foreach>
+
+        <!-- Bind sort property enum values for sake of readability -->
+        <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
+
+        <!-- Sort predicates -->
+        <foreach collection="sortPredicates" item="sortPredicate"
+                 open="ORDER BY " separator=", ">
+            <choose>
+                <when test="sortPredicate.property == START_DATE">[guacamole_user_history].start_date</when>
+                <otherwise>1</otherwise>
+            </choose>
+            <if test="sortPredicate.descending">DESC</if>
+        </foreach>
+
+        LIMIT #{limit,jdbcType=INTEGER}
+
+    </select>
+
+    <!-- Search for specific user records -->
+    <select id="searchReadable" resultMap="UserRecordResultMap">
+
+        SELECT
+            [guacamole_user_history].remote_host,
+            [guacamole_user_history].user_id,
+            [guacamole_user_history].username,
+            [guacamole_user_history].start_date,
+            [guacamole_user_history].end_date
+        FROM [guacamole_user_history]
+
+        <!-- Restrict to readable users -->
+        JOIN [guacamole_user_permission] ON
+                [guacamole_user_history].user_id       = [guacamole_user_permission].affected_user_id
+            AND [guacamole_user_permission].user_id    = #{user.objectID,jdbcType=INTEGER}
+            AND [guacamole_user_permission].permission = 'READ'
+
+        <!-- Search terms -->
+        <foreach collection="terms" item="term"
+                 open="WHERE " separator=" AND ">
+            (
+
+                [guacamole_user_history].user_id IN (
+                    SELECT user_id
+                    FROM [guacamole_user]
+                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
+                )
+
+                <if test="term.startDate != null and term.endDate != null">
+                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                </if>
+
+            )
+        </foreach>
+
+        <!-- Bind sort property enum values for sake of readability -->
+        <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
+
+        <!-- Sort predicates -->
+        <foreach collection="sortPredicates" item="sortPredicate"
+                 open="ORDER BY " separator=", ">
+            <choose>
+                <when test="sortPredicate.property == START_DATE">[guacamole_user_history].start_date</when>
+                <otherwise>1</otherwise>
+            </choose>
+            <if test="sortPredicate.descending">DESC</if>
+        </foreach>
+
+        LIMIT #{limit,jdbcType=INTEGER}
+
+    </select>
+
+</mapper>