Merge pull request #276 from glyptodon/GUAC-1193

GUAC-1193: Implement searchable connection history interface
This commit is contained in:
James Muehlner
2015-10-15 15:58:40 -07:00
36 changed files with 2192 additions and 57 deletions

View File

@@ -22,8 +22,10 @@
package org.glyptodon.guacamole.auth.jdbc.connection;
import java.util.Collection;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.glyptodon.guacamole.auth.jdbc.user.UserModel;
/**
* Mapper for connection record objects.
@@ -56,5 +58,59 @@ public interface ConnectionRecordMapper {
* The number of rows inserted.
*/
int insert(@Param("record") ConnectionRecordModel record);
/**
* Searches for up to <code>limit</code> connection 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<ConnectionRecordModel> search(@Param("terms") Collection<ConnectionRecordSearchTerm> terms,
@Param("sortPredicates") List<ConnectionRecordSortPredicate> sortPredicates,
@Param("limit") int limit);
/**
* Searches for up to <code>limit</code> connection 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<ConnectionRecordModel> searchReadable(@Param("user") UserModel user,
@Param("terms") Collection<ConnectionRecordSearchTerm> terms,
@Param("sortPredicates") List<ConnectionRecordSortPredicate> sortPredicates,
@Param("limit") int limit);
}

View File

@@ -37,6 +37,11 @@ public class ConnectionRecordModel {
*/
private String connectionIdentifier;
/**
* The name of the connection associated with this connection record.
*/
private String connectionName;
/**
* The database ID of the user associated with this connection record.
*/
@@ -82,6 +87,32 @@ public class ConnectionRecordModel {
this.connectionIdentifier = connectionIdentifier;
}
/**
* Returns the name of the connection associated with this connection
* record.
*
* @return
* The name of the connection associated with this connection
* record.
*/
public String getConnectionName() {
return connectionName;
}
/**
* Sets the name of the connection associated with this connection
* record.
*
* @param connectionName
* The name of the connection to associate with this connection
* record.
*/
public void setConnectionName(String connectionName) {
this.connectionName = connectionName;
}
/**
* Returns the database ID of the user associated with this connection
* record.

View File

@@ -0,0 +1,296 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.auth.jdbc.connection;
import java.util.Calendar;
import java.util.Date;
import java.util.regex.Matcher;
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.
*
* @author James Muehlner
*/
public class ConnectionRecordSearchTerm {
/**
* A pattern that can match a year, year and month, or year and month and
* day.
*/
private static final Pattern DATE_PATTERN =
Pattern.compile("(\\d+)(?:-(\\d+)?(?:-(\\d+)?)?)?");
/**
* The index of the group within <code>DATE_PATTERN</code> containing the
* year number.
*/
private static final int YEAR_GROUP = 1;
/**
* The index of the group within <code>DATE_PATTERN</code> containing the
* month number, if any.
*/
private static final int MONTH_GROUP = 2;
/**
* The index of the group within <code>DATE_PATTERN</code> containing the
* day number, if any.
*/
private static final int DAY_GROUP = 3;
/**
* The start of the date range for records that should be retrieved, if the
* provided search term appears to be a date.
*/
private final Date startDate;
/**
* The end of the date range for records that should be retrieved, if the
* provided search term appears to be a date.
*/
private final Date endDate;
/**
* The string that should be searched for.
*/
private final String term;
/**
* Parse the given string as an integer, returning the provided default
* value if the string is null.
*
* @param str
* The string to parse as an integer.
*
* @param defaultValue
* The value to return if <code>str</code> is null.
*
* @return
* The parsed value, or the provided default value if <code>str</code>
* is null.
*/
private static int parseInt(String str, int defaultValue) {
if (str == null)
return defaultValue;
return Integer.parseInt(str);
}
/**
* Returns a new calendar representing the last millisecond of the same
* year as <code>calendar</code>.
*
* @param calendar
* The calendar defining the year whose end (last millisecond) is to be
* returned.
*
* @return
* A new calendar representing the last millisecond of the same year as
* <code>calendar</code>.
*/
private static Calendar getEndOfYear(Calendar calendar) {
// Get first day of next year
Calendar endOfYear = Calendar.getInstance();
endOfYear.clear();
endOfYear.set(Calendar.YEAR, calendar.get(Calendar.YEAR) + 1);
// Transform into the last millisecond of the given year
endOfYear.add(Calendar.MILLISECOND, -1);
return endOfYear;
}
/**
* Returns a new calendar representing the last millisecond of the same
* month and year as <code>calendar</code>.
*
* @param calendar
* The calendar defining the month and year whose end (last millisecond)
* is to be returned.
*
* @return
* A new calendar representing the last millisecond of the same month
* and year as <code>calendar</code>.
*/
private static Calendar getEndOfMonth(Calendar calendar) {
// Copy given calender only up to given month
Calendar endOfMonth = Calendar.getInstance();
endOfMonth.clear();
endOfMonth.set(Calendar.YEAR, calendar.get(Calendar.YEAR));
endOfMonth.set(Calendar.MONTH, calendar.get(Calendar.MONTH));
// Advance to the last millisecond of the given month
endOfMonth.add(Calendar.MONTH, 1);
endOfMonth.add(Calendar.MILLISECOND, -1);
return endOfMonth;
}
/**
* Returns a new calendar representing the last millisecond of the same
* year, month, and day as <code>calendar</code>.
*
* @param calendar
* The calendar defining the year, month, and day whose end
* (last millisecond) is to be returned.
*
* @return
* A new calendar representing the last millisecond of the same year,
* month, and day as <code>calendar</code>.
*/
private static Calendar getEndOfDay(Calendar calendar) {
// Copy given calender only up to given month
Calendar endOfMonth = Calendar.getInstance();
endOfMonth.clear();
endOfMonth.set(Calendar.YEAR, calendar.get(Calendar.YEAR));
endOfMonth.set(Calendar.MONTH, calendar.get(Calendar.MONTH));
endOfMonth.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH));
// Advance to the last millisecond of the given day
endOfMonth.add(Calendar.DAY_OF_MONTH, 1);
endOfMonth.add(Calendar.MILLISECOND, -1);
return endOfMonth;
}
/**
* Creates a new ConnectionRecordSearchTerm 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.
*
* @param term
* The string that should be searched for.
*/
public ConnectionRecordSearchTerm(String term) {
// Search terms absolutely must not be null
if (term == null)
throw new NullPointerException("Search terms may not be null");
this.term = term;
// Parse start/end of date range if term appears to be a date
Matcher matcher = DATE_PATTERN.matcher(term);
if (matcher.matches()) {
// Retrieve date components from term
String year = matcher.group(YEAR_GROUP);
String month = matcher.group(MONTH_GROUP);
String day = matcher.group(DAY_GROUP);
// Parse start date from term
Calendar startCalendar = Calendar.getInstance();
startCalendar.clear();
startCalendar.set(
Integer.parseInt(year),
parseInt(month, 1) - 1,
parseInt(day, 1)
);
Calendar endCalendar;
// Derive end date from start date
if (month == null) {
endCalendar = getEndOfYear(startCalendar);
}
else if (day == null) {
endCalendar = getEndOfMonth(startCalendar);
}
else {
endCalendar = getEndOfDay(startCalendar);
}
// Convert results back into dates
this.startDate = startCalendar.getTime();
this.endDate = endCalendar.getTime();
}
// The search term doesn't look like a date
else {
this.startDate = null;
this.endDate = null;
}
}
/**
* Returns the start of the date range for records that should be retrieved,
* if the provided search term appears to be a date.
*
* @return
* The start of the date range.
*/
public Date getStartDate() {
return startDate;
}
/**
* Returns the end of the date range for records that should be retrieved,
* if the provided search term appears to be a date.
*
* @return
* The end of the date range.
*/
public Date getEndDate() {
return endDate;
}
/**
* Returns the string that should be searched for.
*
* @return
* The search term.
*/
public String getTerm() {
return term;
}
@Override
public int hashCode() {
return term.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof ConnectionRecordSearchTerm))
return false;
return ((ConnectionRecordSearchTerm) obj).getTerm().equals(getTerm());
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.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.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject;
import org.glyptodon.guacamole.net.auth.ConnectionRecord;
/**
* A JDBC implementation of ConnectionRecordSet. Calls to asCollection() will
* query connection history records from the database. Which records are
* returned will be determined by the values passed in earlier.
*
* @author James Muehlner
*/
public class ConnectionRecordSet extends RestrictedObject
implements org.glyptodon.guacamole.net.auth.ConnectionRecordSet {
/**
* Service for managing connection objects.
*/
@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()
throws GuacamoleException {
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;
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.auth.jdbc.connection;
import org.glyptodon.guacamole.net.auth.ConnectionRecordSet;
/**
* A sort predicate which species the property to use when sorting connection
* records, along with the sort order.
*
* @author James Muehlner
*/
public class ConnectionRecordSortPredicate {
/**
* The property to use when sorting ConnectionRecords.
*/
private final ConnectionRecordSet.SortableProperty property;
/**
* Whether the sort order is descending (true) or ascending (false).
*/
private final boolean descending;
/**
* Creates a new ConnectionRecordSortPredicate with the given sort property
* and sort order.
*
* @param property
* The property to use when sorting ConnectionRecords.
*
* @param descending
* Whether the sort order is descending (true) or ascending (false).
*/
public ConnectionRecordSortPredicate(ConnectionRecordSet.SortableProperty property,
boolean descending) {
this.property = property;
this.descending = descending;
}
/**
* Returns the property that should be used when sorting ConnectionRecords.
*
* @return
* The property that should be used when sorting ConnectionRecords.
*/
public ConnectionRecordSet.SortableProperty getProperty() {
return property;
}
/**
* Returns whether the sort order is descending.
*
* @return
* true if the sort order is descending, false if the sort order is
* ascending.
*/
public boolean isDescending() {
return descending;
}
}

View File

@@ -343,6 +343,43 @@ public class ConnectionService extends ModeledGroupedDirectoryObjectService<Mode
}
/**
* Returns a connection records 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 ConnectionRecord getObjectInstance(ConnectionRecordModel model) {
return new ModeledConnectionRecord(model);
}
/**
* Returns a list of connection records objects which are backed by the
* models in the given list.
*
* @param models
* The model objects to use to back the connection record objects
* within the returned list.
*
* @return
* A list of connection record objects which are backed by the models
* in the given list.
*/
protected List<ConnectionRecord> getObjectInstances(List<ConnectionRecordModel> models) {
// Create new list of records by manually converting each model
List<ConnectionRecord> objects = new ArrayList<ConnectionRecord>(models.size());
for (ConnectionRecordModel model : models)
objects.add(getObjectInstance(model));
return objects;
}
/**
* Retrieves the connection history of the given connection, including any
* active connections.
@@ -364,7 +401,7 @@ public class ConnectionService extends ModeledGroupedDirectoryObjectService<Mode
ModeledConnection connection) throws GuacamoleException {
String identifier = connection.getIdentifier();
// Retrieve history only if READ permission is granted
if (hasObjectPermission(user, identifier, ObjectPermission.Type.READ)) {
@@ -377,18 +414,66 @@ public class ConnectionService extends ModeledGroupedDirectoryObjectService<Mode
// Add past connections from model objects
for (ConnectionRecordModel model : models)
records.add(new ModeledConnectionRecord(model));
records.add(getObjectInstance(model));
// Return converted history list
return records;
}
// The user does not have permission to read the history
throw new GuacamoleSecurityException("Permission denied.");
}
/**
* Retrieves the connection history records matching the given criteria.
* Retrieves up to <code>limit</code> connection 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 connection 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 connection history of the given connection, including any
* active connections.
*
* @throws GuacamoleException
* If permission to read the connection history is denied.
*/
public List<ConnectionRecord> retrieveHistory(AuthenticatedUser user,
Collection<ConnectionRecordSearchTerm> requiredContents,
List<ConnectionRecordSortPredicate> sortPredicates, int limit)
throws GuacamoleException {
List<ConnectionRecordModel> searchResults;
// Bypass permission checks if the user is a system admin
if (user.getUser().isAdministrator())
searchResults = connectionRecordMapper.search(requiredContents,
sortPredicates, limit);
// Otherwise only return explicitly readable history records
else
searchResults = connectionRecordMapper.searchReadable(user.getUser().getModel(),
requiredContents, sortPredicates, limit);
return getObjectInstances(searchResults);
}
/**
* Connects to the given connection as the given user, using the given
* client information. If the user does not have permission to read the

View File

@@ -51,6 +51,16 @@ public class ModeledConnectionRecord implements ConnectionRecord {
this.model = model;
}
@Override
public String getConnectionIdentifier() {
return model.getConnectionIdentifier();
}
@Override
public String getConnectionName() {
return model.getConnectionName();
}
@Override
public Date getStartDate() {
return model.getStartDate();

View File

@@ -164,6 +164,16 @@ public class ActiveConnectionRecord implements ConnectionRecord {
return balancingGroup != null;
}
@Override
public String getConnectionIdentifier() {
return connection.getIdentifier();
}
@Override
public String getConnectionName() {
return connection.getName();
}
@Override
public Date getStartDate() {
return startDate;

View File

@@ -32,6 +32,7 @@ import java.util.Collection;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject;
import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionDirectory;
import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionRecordSet;
import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection;
import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
import org.glyptodon.guacamole.form.Form;
@@ -92,6 +93,12 @@ public class UserContext extends RestrictedObject
@Inject
private Provider<RootConnectionGroup> rootGroupProvider;
/**
* Provider for creating connection record sets.
*/
@Inject
private Provider<ConnectionRecordSet> connectionRecordSetProvider;
@Override
public void init(AuthenticatedUser currentUser) {
@@ -136,6 +143,14 @@ public class UserContext extends RestrictedObject
return activeConnectionDirectory;
}
@Override
public ConnectionRecordSet getConnectionHistory()
throws GuacamoleException {
ConnectionRecordSet connectionRecordSet = connectionRecordSetProvider.get();
connectionRecordSet.init(getCurrentUser());
return connectionRecordSet;
}
@Override
public ConnectionGroup getRootConnectionGroup() throws GuacamoleException {

View File

@@ -28,29 +28,32 @@
<!-- Result mapper for system permissions -->
<resultMap id="ConnectionRecordResultMap" type="org.glyptodon.guacamole.auth.jdbc.connection.ConnectionRecordModel">
<result column="connection_id" property="connectionIdentifier" jdbcType="INTEGER"/>
<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"/>
<result column="connection_id" property="connectionIdentifier" jdbcType="INTEGER"/>
<result column="connection_name" property="connectionName" 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 connection records from a given connection -->
<select id="select" resultMap="ConnectionRecordResultMap">
SELECT
connection_id,
guacamole_connection_history.user_id,
username,
start_date,
end_date
guacamole_connection.connection_id,
guacamole_connection.connection_name,
guacamole_user.user_id,
guacamole_user.username,
guacamole_connection_history.start_date,
guacamole_connection_history.end_date
FROM guacamole_connection_history
JOIN guacamole_connection ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
JOIN guacamole_user ON guacamole_connection_history.user_id = guacamole_user.user_id
WHERE
connection_id = #{identifier,jdbcType=VARCHAR}
guacamole_connection.connection_id = #{identifier,jdbcType=VARCHAR}
ORDER BY
start_date DESC,
end_date DESC
guacamole_connection_history.start_date DESC,
guacamole_connection_history.end_date DESC
</select>
@@ -72,4 +75,144 @@
</insert>
<!-- Search for specific connection records -->
<select id="search" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.connection_id,
guacamole_connection.connection_name,
guacamole_connection_history.user_id,
guacamole_user.username,
guacamole_connection_history.start_date,
guacamole_connection_history.end_date
FROM guacamole_connection_history
JOIN guacamole_connection ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
JOIN guacamole_user ON guacamole_connection_history.user_id = guacamole_user.user_id
<!-- Search terms -->
<foreach collection="terms" item="term"
open="WHERE " separator=" AND ">
(
guacamole_connection_history.user_id IN (
SELECT user_id
FROM guacamole_user
WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
)
OR guacamole_connection_history.connection_id IN (
SELECT connection_id
FROM guacamole_connection
WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
)
<if test="term.startDate != null and term.endDate != null">
OR (
(start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
AND (end_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
)
</if>
)
</foreach>
<!-- Bind sort property enum values for sake of readability -->
<bind name="CONNECTION_NAME" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@CONNECTION_NAME"/>
<bind name="USER_IDENTIFIER" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@USER_IDENTIFIER"/>
<bind name="START_DATE" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@START_DATE"/>
<bind name="END_DATE" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@END_DATE"/>
<!-- Sort predicates -->
<foreach collection="sortPredicates" item="sortPredicate"
open="ORDER BY " separator=", ">
<choose>
<when test="sortPredicate.property == CONNECTION_NAME">guacamole_connection.connection_name</when>
<when test="sortPredicate.property == USER_IDENTIFIER">guacamole_user.username</when>
<when test="sortPredicate.property == START_DATE">guacamole_connection_history.start_date</when>
<when test="sortPredicate.property == END_DATE">guacamole_connection_history.end_date</when>
<otherwise>1</otherwise>
</choose>
<if test="sortPredicate.descending">DESC</if>
</foreach>
LIMIT #{limit,jdbcType=INTEGER}
</select>
<!-- Search for specific connection records -->
<select id="searchReadable" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.connection_id,
guacamole_connection.connection_name,
guacamole_connection_history.user_id,
guacamole_user.username,
guacamole_connection_history.start_date,
guacamole_connection_history.end_date
FROM guacamole_connection_history
JOIN guacamole_connection ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
JOIN guacamole_user ON guacamole_connection_history.user_id = guacamole_user.user_id
<!-- Restrict to readable connections -->
JOIN guacamole_connection_permission ON
guacamole_connection_history.connection_id = guacamole_connection_permission.connection_id
AND guacamole_connection_permission.user_id = #{user.objectID,jdbcType=INTEGER}
AND guacamole_connection_permission.permission = 'READ'
<!-- Restrict to readable users -->
JOIN guacamole_user_permission ON
guacamole_connection_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_connection_history.user_id IN (
SELECT user_id
FROM guacamole_user
WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
)
OR guacamole_connection_history.connection_id IN (
SELECT connection_id
FROM guacamole_connection
WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
)
<if test="term.startDate != null and term.endDate != null">
OR (
(start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
AND (end_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
)
</if>
)
</foreach>
<!-- Bind sort property enum values for sake of readability -->
<bind name="CONNECTION_NAME" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@CONNECTION_NAME"/>
<bind name="USER_IDENTIFIER" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@USER_IDENTIFIER"/>
<bind name="START_DATE" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@START_DATE"/>
<bind name="END_DATE" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@END_DATE"/>
<!-- Sort predicates -->
<foreach collection="sortPredicates" item="sortPredicate"
open="ORDER BY " separator=", ">
<choose>
<when test="sortPredicate.property == CONNECTION_NAME">guacamole_connection.connection_name</when>
<when test="sortPredicate.property == USER_IDENTIFIER">guacamole_user.username</when>
<when test="sortPredicate.property == START_DATE">guacamole_connection_history.start_date</when>
<when test="sortPredicate.property == END_DATE">guacamole_connection_history.end_date</when>
<otherwise>1</otherwise>
</choose>
<if test="sortPredicate.descending">DESC</if>
</foreach>
LIMIT #{limit,jdbcType=INTEGER}
</select>
</mapper>

View File

@@ -28,29 +28,32 @@
<!-- Result mapper for system permissions -->
<resultMap id="ConnectionRecordResultMap" type="org.glyptodon.guacamole.auth.jdbc.connection.ConnectionRecordModel">
<result column="connection_id" property="connectionIdentifier" jdbcType="INTEGER"/>
<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"/>
<result column="connection_id" property="connectionIdentifier" jdbcType="INTEGER"/>
<result column="connection_name" property="connectionName" 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 connection records from a given connection -->
<select id="select" resultMap="ConnectionRecordResultMap">
SELECT
connection_id,
guacamole_connection_history.user_id,
username,
start_date,
end_date
guacamole_connection.connection_id,
guacamole_connection.connection_name,
guacamole_user.user_id,
guacamole_user.username,
guacamole_connection_history.start_date,
guacamole_connection_history.end_date
FROM guacamole_connection_history
JOIN guacamole_connection ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
JOIN guacamole_user ON guacamole_connection_history.user_id = guacamole_user.user_id
WHERE
connection_id = #{identifier,jdbcType=INTEGER}::integer
guacamole_connection.connection_id = #{identifier,jdbcType=INTEGER}::integer
ORDER BY
start_date DESC,
end_date DESC
guacamole_connection_history.start_date DESC,
guacamole_connection_history.end_date DESC
</select>
@@ -72,4 +75,144 @@
</insert>
<!-- Search for specific connection records -->
<select id="search" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.connection_id,
guacamole_connection.connection_name,
guacamole_connection_history.user_id,
guacamole_user.username,
guacamole_connection_history.start_date,
guacamole_connection_history.end_date
FROM guacamole_connection_history
JOIN guacamole_connection ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
JOIN guacamole_user ON guacamole_connection_history.user_id = guacamole_user.user_id
<!-- Search terms -->
<foreach collection="terms" item="term"
open="WHERE " separator=" AND ">
(
guacamole_connection_history.user_id IN (
SELECT user_id
FROM guacamole_user
WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
)
OR guacamole_connection_history.connection_id IN (
SELECT connection_id
FROM guacamole_connection
WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
)
<if test="term.startDate != null and term.endDate != null">
OR (
(start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
AND (end_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
)
</if>
)
</foreach>
<!-- Bind sort property enum values for sake of readability -->
<bind name="CONNECTION_NAME" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@CONNECTION_NAME"/>
<bind name="USER_IDENTIFIER" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@USER_IDENTIFIER"/>
<bind name="START_DATE" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@START_DATE"/>
<bind name="END_DATE" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@END_DATE"/>
<!-- Sort predicates -->
<foreach collection="sortPredicates" item="sortPredicate"
open="ORDER BY " separator=", ">
<choose>
<when test="sortPredicate.property == CONNECTION_NAME">guacamole_connection.connection_name</when>
<when test="sortPredicate.property == USER_IDENTIFIER">guacamole_user.username</when>
<when test="sortPredicate.property == START_DATE">guacamole_connection_history.start_date</when>
<when test="sortPredicate.property == END_DATE">guacamole_connection_history.end_date</when>
<otherwise>1</otherwise>
</choose>
<if test="sortPredicate.descending">DESC</if>
</foreach>
LIMIT #{limit,jdbcType=INTEGER}
</select>
<!-- Search for specific connection records -->
<select id="searchReadable" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.connection_id,
guacamole_connection.connection_name,
guacamole_connection_history.user_id,
guacamole_user.username,
guacamole_connection_history.start_date,
guacamole_connection_history.end_date
FROM guacamole_connection_history
JOIN guacamole_connection ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
JOIN guacamole_user ON guacamole_connection_history.user_id = guacamole_user.user_id
<!-- Restrict to readable connections -->
JOIN guacamole_connection_permission ON
guacamole_connection_history.connection_id = guacamole_connection_permission.connection_id
AND guacamole_connection_permission.user_id = #{user.objectID,jdbcType=INTEGER}
AND guacamole_connection_permission.permission = 'READ'
<!-- Restrict to readable users -->
JOIN guacamole_user_permission ON
guacamole_connection_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_connection_history.user_id IN (
SELECT user_id
FROM guacamole_user
WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
)
OR guacamole_connection_history.connection_id IN (
SELECT connection_id
FROM guacamole_connection
WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
)
<if test="term.startDate != null and term.endDate != null">
OR (
(start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
AND (end_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
)
</if>
)
</foreach>
<!-- Bind sort property enum values for sake of readability -->
<bind name="CONNECTION_NAME" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@CONNECTION_NAME"/>
<bind name="USER_IDENTIFIER" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@USER_IDENTIFIER"/>
<bind name="START_DATE" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@START_DATE"/>
<bind name="END_DATE" value="@org.glyptodon.guacamole.net.auth.ConnectionRecordSet$SortableProperty@END_DATE"/>
<!-- Sort predicates -->
<foreach collection="sortPredicates" item="sortPredicate"
open="ORDER BY " separator=", ">
<choose>
<when test="sortPredicate.property == CONNECTION_NAME">guacamole_connection.connection_name</when>
<when test="sortPredicate.property == USER_IDENTIFIER">guacamole_user.username</when>
<when test="sortPredicate.property == START_DATE">guacamole_connection_history.start_date</when>
<when test="sortPredicate.property == END_DATE">guacamole_connection_history.end_date</when>
<otherwise>1</otherwise>
</choose>
<if test="sortPredicate.descending">DESC</if>
</foreach>
LIMIT #{limit,jdbcType=INTEGER}
</select>
</mapper>

View File

@@ -35,10 +35,12 @@ import org.glyptodon.guacamole.net.auth.AuthenticatedUser;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Connection;
import org.glyptodon.guacamole.net.auth.ConnectionGroup;
import org.glyptodon.guacamole.net.auth.ConnectionRecordSet;
import org.glyptodon.guacamole.net.auth.Directory;
import org.glyptodon.guacamole.net.auth.User;
import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionGroup;
import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionGroupDirectory;
import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionRecordSet;
import org.glyptodon.guacamole.net.auth.simple.SimpleDirectory;
import org.glyptodon.guacamole.net.auth.simple.SimpleUser;
import org.slf4j.Logger;
@@ -194,6 +196,12 @@ public class UserContext implements org.glyptodon.guacamole.net.auth.UserContext
return new SimpleDirectory<ActiveConnection>();
}
@Override
public ConnectionRecordSet getConnectionHistory()
throws GuacamoleException {
return new SimpleConnectionRecordSet();
}
@Override
public Collection<Form> getUserAttributes() {
return Collections.<Form>emptyList();

View File

@@ -32,6 +32,25 @@ import java.util.Date;
*/
public interface ConnectionRecord {
/**
* Returns the identifier of the connection associated with this
* connection record.
*
* @return
* The identifier of the connection associated with this connection
* record.
*/
public String getConnectionIdentifier();
/**
* Returns the name of the connection associated with this connection
* record.
*
* @return
* The name of the connection associated with this connection record.
*/
public String getConnectionName();
/**
* Returns the date and time the connection began.
*

View File

@@ -0,0 +1,149 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.auth;
import java.util.Collection;
import org.glyptodon.guacamole.GuacamoleException;
/**
* The set of all available connection records, or a subset of those records.
*
* @author James Muehlner
* @author Michael Jumper
*/
public interface ConnectionRecordSet {
/**
* All properties of connection records which can be used as sorting
* criteria.
*/
enum SortableProperty {
/**
* The name (not identifier) of the connection associated with the
* connection record.
*/
CONNECTION_NAME,
/**
* The identifier (username) of the user that used the connection
* associated with the connection record.
*/
USER_IDENTIFIER,
/**
* The date and time when the connection associated with the
* connection record began.
*/
START_DATE,
/**
* The date and time when the connection associated with the
* connection record ended.
*/
END_DATE
};
/**
* Returns all connection records within this set as a standard Collection.
*
* @return
* A collection containing all connection records within this set.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection records within
* this set.
*/
Collection<ConnectionRecord> asCollection() throws GuacamoleException;
/**
* Returns the subset of connection records to only those where the
* connection name, user identifier, or any associated date field contain
* the given value. This function may also affect the contents of the
* current ConnectionRecordSet. The contents of the current
* ConnectionRecordSet should NOT be relied upon after this function is
* called.
*
* @param value
* The value which all connection records within the resulting subset
* should contain within their associated connection name or user
* identifier.
*
* @return
* The subset of connection history records which contain the specified
* value within their associated connection name or user identifier.
*
* @throws GuacamoleException
* If an error occurs while restricting the current subset.
*/
ConnectionRecordSet contains(String value) throws GuacamoleException;
/**
* Returns the subset of connection history records containing only the
* first <code>limit</code> records. If the subset has fewer than
* <code>limit</code> records, then this function has no effect. This
* function may also affect the contents of the current
* ConnectionRecordSet. The contents of the current ConnectionRecordSet
* should NOT be relied upon after this function is called.
*
* @param limit
* The maximum number of records that the new subset should contain.
*
* @return
* The subset of connection history records that containing only the
* first <code>limit</code> records.
*
* @throws GuacamoleException
* If an error occurs while limiting the current subset.
*/
ConnectionRecordSet limit(int limit) throws GuacamoleException;
/**
* Returns a ConnectionRecordSet containing identically the records within
* this set, sorted according to the specified criteria. The sort operation
* performed is guaranteed to be stable with respect to any past call to
* sort(). This function may also affect the contents of the current
* ConnectionRecordSet. The contents of the current ConnectionRecordSet
* should NOT be relied upon after this function is called.
*
* @param property
* The property by which the connection records within the resulting
* set should be sorted.
*
* @param desc
* Whether the records should be sorted according to the specified
* property in descending order. If false, records will be sorted
* according to the specified property in ascending order.
*
* @return
* The ConnnectionRecordSet, sorted according to the specified
* criteria.
*
* @throws GuacamoleException
* If an error occurs while sorting the current subset.
*/
ConnectionRecordSet sort(SortableProperty property, boolean desc)
throws GuacamoleException;
}

View File

@@ -109,6 +109,19 @@ public interface UserContext {
Directory<ActiveConnection> getActiveConnectionDirectory()
throws GuacamoleException;
/**
* Retrieves all connection records visible to current user. The resulting
* set of connection records can be further filtered and ordered using the
* methods defined on ConnectionRecordSet.
*
* @return
* A set of all connection records visible to the current user.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection records.
*/
ConnectionRecordSet getConnectionHistory() throws GuacamoleException;
/**
* Retrieves a connection group which can be used to view and manipulate
* connections, but only as allowed by the permissions given to the user of

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.auth.simple;
import java.util.Collection;
import java.util.Collections;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.ConnectionRecord;
import org.glyptodon.guacamole.net.auth.ConnectionRecordSet;
/**
* An immutable and empty ConnectionRecordSet.
*
* @author Michael Jumper
*/
public class SimpleConnectionRecordSet implements ConnectionRecordSet {
@Override
public Collection<ConnectionRecord> asCollection()
throws GuacamoleException {
return Collections.<ConnectionRecord>emptyList();
}
@Override
public ConnectionRecordSet contains(String value)
throws GuacamoleException {
return this;
}
@Override
public ConnectionRecordSet limit(int limit)
throws GuacamoleException {
return this;
}
@Override
public ConnectionRecordSet sort(SortableProperty property, boolean desc)
throws GuacamoleException {
return this;
}
}

View File

@@ -33,6 +33,7 @@ import org.glyptodon.guacamole.net.auth.ActiveConnection;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Connection;
import org.glyptodon.guacamole.net.auth.ConnectionGroup;
import org.glyptodon.guacamole.net.auth.ConnectionRecordSet;
import org.glyptodon.guacamole.net.auth.Directory;
import org.glyptodon.guacamole.net.auth.User;
import org.glyptodon.guacamole.net.auth.UserContext;
@@ -200,6 +201,12 @@ public class SimpleUserContext implements UserContext {
return new SimpleDirectory<ActiveConnection>();
}
@Override
public ConnectionRecordSet getConnectionHistory()
throws GuacamoleException {
return new SimpleConnectionRecordSet();
}
@Override
public Collection<Form> getUserAttributes() {
return Collections.<Form>emptyList();

View File

@@ -31,6 +31,7 @@ import org.glyptodon.guacamole.net.basic.rest.auth.TokenRESTService;
import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionRESTService;
import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupRESTService;
import org.glyptodon.guacamole.net.basic.rest.activeconnection.ActiveConnectionRESTService;
import org.glyptodon.guacamole.net.basic.rest.history.HistoryRESTService;
import org.glyptodon.guacamole.net.basic.rest.language.LanguageRESTService;
import org.glyptodon.guacamole.net.basic.rest.schema.SchemaRESTService;
import org.glyptodon.guacamole.net.basic.rest.user.UserRESTService;
@@ -59,6 +60,7 @@ public class RESTServletModule extends ServletModule {
bind(ActiveConnectionRESTService.class);
bind(ConnectionGroupRESTService.class);
bind(ConnectionRESTService.class);
bind(HistoryRESTService.class);
bind(LanguageRESTService.class);
bind(SchemaRESTService.class);
bind(TokenRESTService.class);

View File

@@ -51,6 +51,7 @@ import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet;
import org.glyptodon.guacamole.net.basic.GuacamoleSession;
import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService;
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
import org.glyptodon.guacamole.net.basic.rest.history.APIConnectionRecord;
import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -20,18 +20,28 @@
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.rest.connection;
package org.glyptodon.guacamole.net.basic.rest.history;
import java.util.Date;
import org.glyptodon.guacamole.net.auth.ConnectionRecord;
/**
* A connection record which may be exposed through the REST endpoints.
*
*
* @author Michael Jumper
*/
public class APIConnectionRecord {
/**
* The identifier of the connection associated with this record.
*/
private final String connectionIdentifier;
/**
* The identifier of the connection associated with this record.
*/
private final String connectionName;
/**
* The date and time the connection began.
*/
@@ -47,7 +57,7 @@ public class APIConnectionRecord {
* The host from which the connection originated, if known.
*/
private final String remoteHost;
/**
* The name of the user who used or is using the connection.
*/
@@ -66,11 +76,34 @@ public class APIConnectionRecord {
* The record to copy data from.
*/
public APIConnectionRecord(ConnectionRecord record) {
this.startDate = record.getStartDate();
this.endDate = record.getEndDate();
this.remoteHost = record.getRemoteHost();
this.username = record.getUsername();
this.active = record.isActive();
this.connectionIdentifier = record.getConnectionIdentifier();
this.connectionName = record.getConnectionName();
this.startDate = record.getStartDate();
this.endDate = record.getEndDate();
this.remoteHost = record.getRemoteHost();
this.username = record.getUsername();
this.active = record.isActive();
}
/**
* Returns the identifier of the connection associated with this
* record.
*
* @return
* The identifier of the connection associated with this record.
*/
public String getConnectionIdentifier() {
return connectionIdentifier;
}
/**
* Returns the name of the connection associated with this record.
*
* @return
* The name of the connection associated with this record.
*/
public String getConnectionName() {
return connectionName;
}
/**
@@ -126,5 +159,5 @@ public class APIConnectionRecord {
public boolean isActive() {
return active;
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.rest.history;
import org.glyptodon.guacamole.net.auth.ConnectionRecordSet;
import org.glyptodon.guacamole.net.basic.rest.APIError;
import org.glyptodon.guacamole.net.basic.rest.APIException;
/**
* A sort predicate which species the property to use when sorting connection
* records, along with the sort order.
*
* @author Michael Jumper
*/
public class APIConnectionRecordSortPredicate {
/**
* The prefix which will be included before the name of a sortable property
* to indicate that the sort order is descending, not ascending.
*/
public static final String DESCENDING_PREFIX = "-";
/**
* All possible property name strings and their corresponding
* ConnectionRecordSet.SortableProperty values.
*/
public enum SortableProperty {
/**
* The name (not identifier) of the connection associated with the
* connection record.
*/
connectionName(ConnectionRecordSet.SortableProperty.CONNECTION_NAME),
/**
* The username (identifier) of the user associated with the connection
* record.
*/
username(ConnectionRecordSet.SortableProperty.USER_IDENTIFIER),
/**
* The date that the connection associated with the connection record
* began (connected).
*/
startDate(ConnectionRecordSet.SortableProperty.START_DATE),
/**
* The date that the connection associated with the connection record
* ended (disconnected).
*/
endDate(ConnectionRecordSet.SortableProperty.END_DATE);
/**
* The ConnectionRecordSet.SortableProperty that this property name
* string represents.
*/
public final ConnectionRecordSet.SortableProperty recordProperty;
/**
* Creates a new SortableProperty which associates the property name
* string (identical to its own name) with the given
* ConnectionRecordSet.SortableProperty value.
*
* @param recordProperty
* The ConnectionRecordSet.SortableProperty value to associate with
* the new SortableProperty.
*/
SortableProperty(ConnectionRecordSet.SortableProperty recordProperty) {
this.recordProperty = recordProperty;
}
}
/**
* The property to use when sorting ConnectionRecords.
*/
private ConnectionRecordSet.SortableProperty property;
/**
* Whether the requested sort order is descending (true) or ascending
* (false).
*/
private boolean descending;
/**
* Parses the given string value, determining the requested sort property
* and ordering. Possible values consist of any valid property name, and
* may include an optional prefix to denote descending sort order. Each
* possible property name is enumerated by the SortableValue enum.
*
* @param value
* The sort predicate string to parse, which must consist ONLY of a
* valid property name, possibly preceded by the DESCENDING_PREFIX.
*
* @throws APIException
* If the provided sort predicate string is invalid.
*/
public APIConnectionRecordSortPredicate(String value)
throws APIException {
// Parse whether sort order is descending
if (value.startsWith(DESCENDING_PREFIX)) {
descending = true;
value = value.substring(DESCENDING_PREFIX.length());
}
// Parse sorting property into ConnectionRecordSet.SortableProperty
try {
this.property = SortableProperty.valueOf(value).recordProperty;
}
// Bail out if sort property is not valid
catch (IllegalArgumentException e) {
throw new APIException(
APIError.Type.BAD_REQUEST,
String.format("Invalid sort property: \"%s\"", value)
);
}
}
/**
* Returns the SortableProperty defined by ConnectionRecordSet which
* represents the property requested.
*
* @return
* The ConnectionRecordSet.SortableProperty which refers to the same
* property as the string originally provided when this
* APIConnectionRecordSortPredicate was created.
*/
public ConnectionRecordSet.SortableProperty getProperty() {
return property;
}
/**
* Returns whether the requested sort order is descending.
*
* @return
* true if the sort order is descending, false if the sort order is
* ascending.
*/
public boolean isDescending() {
return descending;
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.rest.history;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.ConnectionRecord;
import org.glyptodon.guacamole.net.auth.ConnectionRecordSet;
import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.basic.GuacamoleSession;
import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService;
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A REST Service for retrieving and managing the history records of Guacamole
* objects.
*
* @author Michael Jumper
*/
@Path("/data/{dataSource}/history")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class HistoryRESTService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(HistoryRESTService.class);
/**
* The maximum number of history records to return in any one response.
*/
private static final int MAXIMUM_HISTORY_SIZE = 10000;
/**
* A service for authenticating users from auth tokens.
*/
@Inject
private AuthenticationService authenticationService;
/**
* Service for convenient retrieval of objects.
*/
@Inject
private ObjectRetrievalService retrievalService;
/**
* Retrieves the usage history for all connections, restricted by optional
* filter parameters.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection whose history is to be
* retrieved.
*
* @param requiredContents
* 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.
*
* @param sortPredicates
* A list of predicates to apply while sorting the resulting connection
* records, describing the properties involved and the sort order for
* those properties.
*
* @return
* A list of connection records, describing the start and end times of
* various usages of this connection.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection history.
*/
@GET
@Path("/connections")
public List<APIConnectionRecord> getConnectionHistory(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@QueryParam("contains") List<String> requiredContents,
@QueryParam("order") List<APIConnectionRecordSortPredicate> sortPredicates)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Retrieve overall connection history
ConnectionRecordSet history = userContext.getConnectionHistory();
// Restrict to records which contain the specified strings
for (String required : requiredContents) {
if (!required.isEmpty())
history = history.contains(required);
}
// Sort according to specified ordering
for (APIConnectionRecordSortPredicate predicate : sortPredicates)
history = history.sort(predicate.getProperty(), predicate.isDescending());
// Limit to maximum result size
history = history.limit(MAXIMUM_HISTORY_SIZE);
// Convert record set to collection of API connection records
List<APIConnectionRecord> apiRecords = new ArrayList<APIConnectionRecord>();
for (ConnectionRecord record : history.asCollection())
apiRecords.add(new APIConnectionRecord(record));
// Return the converted history
return apiRecords;
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to retrieval or maintenance of history records using the
* Guacamole REST API.
*/
package org.glyptodon.guacamole.net.basic.rest.history;

View File

@@ -173,6 +173,7 @@ angular.module('navigation').factory('userPageService', ['$injector',
var canManageUsers = [];
var canManageConnections = [];
var canViewConnectionRecords = [];
var canManageSessions = [];
// Inspect the contents of each provided permission set
@@ -234,12 +235,13 @@ angular.module('navigation').factory('userPageService', ['$injector',
)
canManageConnections.push(dataSource);
// Determine whether the current user needs access to the session management UI
// Determine whether the current user needs access to the session management UI or view connection history
if (
// A user must be a system administrator to manage sessions
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
)
canManageSessions.push(dataSource);
canViewConnectionRecords.push(dataSource);
});
@@ -250,7 +252,18 @@ angular.module('navigation').factory('userPageService', ['$injector',
url : '/settings/sessions'
}));
}
// If user can manage connections, add links for connection management pages
angular.forEach(canViewConnectionRecords, function addConnectionHistoryLink(dataSource) {
pages.push(new PageDefinition({
name : [
'USER_MENU.ACTION_VIEW_HISTORY',
translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME'
],
url : '/settings/' + encodeURIComponent(dataSource) + '/history'
}));
});
// If user can manage users, add link to user management page
if (canManageUsers.length) {
pages.push(new PageDefinition({

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Service for operating on history records via the REST API.
*/
angular.module('rest').factory('historyService', ['$injector',
function historyService($injector) {
// Required services
var $http = $injector.get('$http');
var authenticationService = $injector.get('authenticationService');
var service = {};
/**
* Makes a request to the REST API to get the usage history of all
* accessible connections, returning a promise that provides the
* corresponding array of @link{ConnectionHistoryEntry} objects if
* successful.
*
* @param {String} dataSource
* The unique identifier of the data source containing the connection
* history records to be retrieved. This identifier corresponds to an
* AuthenticationProvider within the Guacamole web application.
*
* @param {String[]} [requiredContents]
* The set of arbitrary strings to filter with. A ConnectionHistoryEntry
* must contain each of these values within the associated username,
* connection name, start date, or end date to appear in the result. If
* null, no filtering will be performed.
*
* @param {String[]} [sortPredicates]
* The set of predicates to sort against. The resulting array of
* ConnectionHistoryEntry objects will be sorted according to the
* properties and sort orders defined by each predicate. If null, the
* order of the resulting entries is undefined. Valid values are listed
* within ConnectionHistoryEntry.SortPredicate.
*
* @returns {Promise.<ConnectionHistoryEntry[]>}
* A promise which will resolve with an array of
* @link{ConnectionHistoryEntry} objects upon success.
*/
service.getConnectionHistory = function getConnectionHistory(dataSource,
requiredContents, sortPredicates) {
// Build HTTP parameters set
var httpParameters = {
token : authenticationService.getCurrentToken()
};
// Filter according to contents if restrictions are specified
if (requiredContents)
httpParameters.contains = requiredContents;
// Sort according to provided predicates, if any
if (sortPredicates)
httpParameters.order = sortPredicates;
// Retrieve connection history
return $http({
method : 'GET',
url : 'api/data/' + encodeURIComponent(dataSource) + '/history/connections',
params : httpParameters
});
};
return service;
}]);

View File

@@ -41,6 +41,20 @@ angular.module('rest').factory('ConnectionHistoryEntry', [function defineConnect
// Use empty object by default
template = template || {};
/**
* The identifier of the connection associated with this history entry.
*
* @type String
*/
this.connectionIdentifier = template.connectionIdentifier;
/**
* The name of the connection associated with this history entry.
*
* @type String
*/
this.connectionName = template.connectionName;
/**
* The time that usage began, in seconds since 1970-01-01 00:00:00 UTC.
*
@@ -87,6 +101,41 @@ angular.module('rest').factory('ConnectionHistoryEntry', [function defineConnect
};
/**
* All possible predicates for sorting ConnectionHistoryEntry objects using
* the REST API. By default, each predicate indicates ascending order. To
* indicate descending order, add "-" to the beginning of the predicate.
*
* @type Object.<String, String>
*/
ConnectionHistoryEntry.SortPredicate = {
/**
* The name of the connection associated with the history entry (not
* the connection identifier).
*/
CONNECTION_NAME : 'connectionName',
/**
* The username of the user associated with the history entry (the user
* identifier).
*/
USER_IDENTIFIER : 'username',
/**
* The date and time that the connection associated with the history
* entry began (connected).
*/
START_DATE : 'startDate',
/**
* The date and time that the connection associated with the history
* entry ended (disconnected).
*/
END_DATE : 'endDate'
};
return ConnectionHistoryEntry;
}]);

View File

@@ -0,0 +1,171 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A directive for viewing connection history records.
*/
angular.module('settings').directive('guacSettingsConnectionHistory', [function guacSettingsConnectionHistory() {
return {
// Element only
restrict: 'E',
replace: true,
scope: {
},
templateUrl: 'app/settings/templates/settingsConnectionHistory.html',
controller: ['$scope', '$injector', function settingsConnectionHistoryController($scope, $injector) {
// Get required types
var FilterToken = $injector.get('FilterToken');
var SortOrder = $injector.get('SortOrder');
// Get required services
var $routeParams = $injector.get('$routeParams');
var $translate = $injector.get('$translate');
var historyService = $injector.get('historyService');
/**
* The identifier of the currently-selected data source.
*
* @type String
*/
$scope.dataSource = $routeParams.dataSource;
/**
* All matching connection history records, or null if these
* records have not yet been retrieved.
*
* @type ConnectionHistoryEntry[]
*/
$scope.historyRecords = null;
/**
* The search terms to use when filtering the history records.
*
* @type String
*/
$scope.searchString = '';
/**
* The date format for use for start/end dates.
*
* @type String
*/
$scope.dateFormat = null;
/**
* SortOrder instance which stores the sort order of the history
* records.
*
* @type SortOrder
*/
$scope.order = new SortOrder([
'username',
'startDate',
'endDate',
'connectionName'
]);
// Get session date format
$translate('SETTINGS_CONNECTION_HISTORY.FORMAT_DATE')
.then(function dateFormatReceived(retrievedDateFormat) {
// Store received date format
$scope.dateFormat = retrievedDateFormat;
});
/**
* Returns true if the connection history records have been loaded,
* indicating that information needed to render the page is fully
* loaded.
*
* @returns {Boolean}
* true if the history records have been loaded, false
* otherwise.
*
*/
$scope.isLoaded = function isLoaded() {
return $scope.historyRecords !== null
&& $scope.dateFormat !== null;
};
/**
* Query the API for the connection record history, filtered by
* searchString, and ordered by order.
*/
$scope.search = function search() {
// Clear current results
$scope.historyRecords = null;
// Tokenize search string
var tokens = FilterToken.tokenize($scope.searchString);
// Transform tokens into list of required string contents
var requiredContents = [];
angular.forEach(tokens, function addRequiredContents(token) {
// Transform depending on token type
switch (token.type) {
// For string literals, use parsed token value
case 'LITERAL':
requiredContents.push(token.value);
// Ignore whitespace
case 'WHITESPACE':
break;
// For all other token types, use the relevant portion
// of the original search string
default:
requiredContents.push(token.consumed);
}
});
// Fetch history records
historyService.getConnectionHistory(
$scope.dataSource,
requiredContents,
$scope.order.predicate
)
.success(function historyRetrieved(historyRecords) {
// Store retrieved permissions
$scope.historyRecords = historyRecords;
});
};
// Initialize search results
$scope.search();
}]
};
}]);

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
.settings.connectionHistory .filter {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: row;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: horizontal;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: horizontal;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: row;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: row;
}
.settings.connectionHistory .filter .search-button {
margin-top: 0;
margin-bottom: 0;
}
.settings.connectionHistory .history-list {
width: 100%;
}

View File

@@ -33,9 +33,10 @@ THE SOFTWARE.
</div>
<!-- Selected tab -->
<guac-settings-users ng-if="activeTab === 'users'"></guac-settings-users>
<guac-settings-connections ng-if="activeTab === 'connections'"></guac-settings-connections>
<guac-settings-sessions ng-if="activeTab === 'sessions'"></guac-settings-sessions>
<guac-settings-preferences ng-if="activeTab === 'preferences'"></guac-settings-preferences>
<guac-settings-users ng-if="activeTab === 'users'"></guac-settings-users>
<guac-settings-connections ng-if="activeTab === 'connections'"></guac-settings-connections>
<guac-settings-connection-history ng-if="activeTab === 'history'"></guac-settings-connection-history>
<guac-settings-sessions ng-if="activeTab === 'sessions'"></guac-settings-sessions>
<guac-settings-preferences ng-if="activeTab === 'preferences'"></guac-settings-preferences>
</div>

View File

@@ -0,0 +1,74 @@
<div class="settings section connectionHistory">
<!--
Copyright 2015 Glyptodon LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<!-- Connection history -->
<p>{{'SETTINGS_CONNECTION_HISTORY.HELP_CONNECTION_HISTORY' | translate}}</p>
<!-- Search controls -->
<form class="filter" ng-submit="search()">
<input class="search-string" type="text" placeholder="{{'SETTINGS_CONNECTION_HISTORY.FIELD_PLACEHOLDER_FILTER' | translate}}" ng-model="searchString" />
<input class="search-button" type="submit" value="{{'SETTINGS_CONNECTION_HISTORY.ACTION_SEARCH' | translate}}" />
</form>
<!-- Search results -->
<div class="results" ng-class="{loading: !isLoaded()}">
<!-- List of matching history records -->
<table class="sorted history-list">
<thead>
<tr>
<th guac-sort-order="order" guac-sort-property="'username'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME' | translate}}
</th>
<th guac-sort-order="order" guac-sort-property="'startDate'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE' | translate}}
</th>
<th guac-sort-order="order" guac-sort-property="'endDate'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_ENDDATE' | translate}}
</th>
<th guac-sort-order="order" guac-sort-property="'connectionName'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}}
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="historyRecord in historyRecordPage" class="history">
<td>{{historyRecord.username}}</td>
<td>{{historyRecord.startDate | date : dateFormat}}</td>
<td>{{historyRecord.endDate | date : dateFormat}}</td>
<td>{{historyRecord.connectionName}}</td>
</tr>
</tbody>
</table>
<!-- Text displayed if no history exists -->
<p class="placeholder" ng-hide="historyRecordPage.length">
{{'SETTINGS_CONNECTION_HISTORY.INFO_NO_HISTORY' | translate}}
</p>
<!-- Pager for history list -->
<guac-pager page="historyRecordPage" page-size="25"
items="historyRecords | orderBy : order.predicate"></guac-pager>
</div>
</div>

View File

@@ -30,6 +30,8 @@
"FIELD_HEADER_PASSWORD" : "Passwort:",
"FIELD_HEADER_PASSWORD_AGAIN" : "Wiederhole Passwort:",
"FIELD_PLACEHOLDER_FILTER" : "Filter",
"FORMAT_DATE_TIME_PRECISE" : "dd-MM-yyyy HH:mm:ss",
"INFO_ACTIVE_USER_COUNT" : "In Benutzung durch {USERS} Benutzer.",
@@ -456,6 +458,12 @@
},
"SETTINGS_CONNECTION_HISTORY" : {
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER"
},
"SETTINGS_PREFERENCES" : {
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
@@ -518,7 +526,7 @@
"DIALOG_HEADER_CONFIRM_DELETE" : "Beende Sitzung",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_PLACEHOLDER_FILTER" : "Filter",
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",

View File

@@ -20,7 +20,9 @@
"ACTION_NAVIGATE_BACK" : "Back",
"ACTION_NAVIGATE_HOME" : "Home",
"ACTION_SAVE" : "Save",
"ACTION_SEARCH" : "Search",
"ACTION_UPDATE_PASSWORD" : "Update Password",
"ACTION_VIEW_HISTORY" : "History",
"DIALOG_HEADER_ERROR" : "Error",
@@ -30,6 +32,8 @@
"FIELD_HEADER_PASSWORD" : "Password:",
"FIELD_HEADER_PASSWORD_AGAIN" : "Re-enter Password:",
"FIELD_PLACEHOLDER_FILTER" : "Filter",
"FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss",
"INFO_ACTIVE_USER_COUNT" : "Currently in use by {USERS} {USERS, plural, one{user} other{users}}.",
@@ -473,6 +477,25 @@
},
"SETTINGS_CONNECTION_HISTORY" : {
"ACTION_SEARCH" : "@:APP.ACTION_SEARCH",
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
"HELP_CONNECTION_HISTORY" : "History records for past connections are listed here and can be sorted by clicking the column headers. To search for specific records, enter a filter string and click \"Search\". Only records which match the provided filter string will be listed.",
"INFO_NO_HISTORY" : "No matching records",
"TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name",
"TABLE_HEADER_SESSION_ENDDATE" : "End time",
"TABLE_HEADER_SESSION_STARTDATE" : "Start time",
"TABLE_HEADER_SESSION_USERNAME" : "Username"
},
"SETTINGS_CONNECTIONS" : {
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
@@ -551,7 +574,7 @@
"DIALOG_HEADER_CONFIRM_DELETE" : "Kill Sessions",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_PLACEHOLDER_FILTER" : "Filter",
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
@@ -561,10 +584,10 @@
"SECTION_HEADER_SESSIONS" : "Active Sessions",
"TABLE_HEADER_SESSION_USERNAME" : "Username",
"TABLE_HEADER_SESSION_STARTDATE" : "Active since",
"TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host",
"TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name",
"TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host",
"TABLE_HEADER_SESSION_STARTDATE" : "Active since",
"TABLE_HEADER_SESSION_USERNAME" : "Username",
"TEXT_CONFIRM_DELETE" : "Are you sure you want to kill all selected sessions? The users using these sessions will be immediately disconnected."
@@ -578,7 +601,8 @@
"ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS",
"ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS",
"ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS",
"ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME"
"ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME",
"ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY"
}

View File

@@ -30,6 +30,8 @@
"FIELD_HEADER_PASSWORD" : "Mot de passe:",
"FIELD_HEADER_PASSWORD_AGAIN" : "Répéter mot de passe:",
"FIELD_PLACEHOLDER_FILTER" : "Filtre",
"FORMAT_DATE_TIME_PRECISE" : "dd-MM-yyyy HH:mm:ss",
"INFO_ACTIVE_USER_COUNT" : "Actuellement utilisé par {USERS} {USERS, plural, one{utilisateur} other{utilisateurs}}.",
@@ -456,6 +458,12 @@
},
"SETTINGS_CONNECTION_HISTORY" : {
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER"
},
"SETTINGS_PREFERENCES" : {
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
@@ -518,7 +526,7 @@
"DIALOG_HEADER_CONFIRM_DELETE" : "Fermer Sessions",
"DIALOG_HEADER_ERROR" : "Erreur",
"FIELD_PLACEHOLDER_FILTER" : "Filtre",
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",

View File

@@ -30,6 +30,8 @@
"FIELD_HEADER_PASSWORD" : "Password:",
"FIELD_HEADER_PASSWORD_AGAIN" : "Re-inserisci la password:",
"FIELD_PLACEHOLDER_FILTER" : "Filtro",
"FORMAT_DATE_TIME_PRECISE" : "dd-MM-yyyy HH:mm:ss",
"INFO_ACTIVE_USER_COUNT" : "Ora utilizzato da {USERS} {USERS, plural, one{user} other{users}}.",
@@ -456,6 +458,12 @@
},
"SETTINGS_CONNECTION_HISTORY" : {
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER"
},
"SETTINGS_PREFERENCES" : {
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
@@ -518,7 +526,7 @@
"DIALOG_HEADER_CONFIRM_DELETE" : "Termina Sessione",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_PLACEHOLDER_FILTER" : "Filtro",
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",

View File

@@ -30,6 +30,8 @@
"FIELD_HEADER_PASSWORD" : "Wachtwoord:",
"FIELD_HEADER_PASSWORD_AGAIN" : "Nogmaals uw wachtwoord:",
"FIELD_PLACEHOLDER_FILTER" : "Filter",
"FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss",
"INFO_ACTIVE_USER_COUNT" : "Op dit moment in gebruik door {USERS} {USERS, plural, one{gebruiker} other{gebruikers}}.",
@@ -447,6 +449,12 @@
},
"SETTINGS_CONNECTION_HISTORY" : {
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER"
},
"SETTINGS_PREFERENCES" : {
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
@@ -509,7 +517,7 @@
"DIALOG_HEADER_CONFIRM_DELETE" : "Beeindig Sessie",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_PLACEHOLDER_FILTER" : "Filter",
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",

View File

@@ -29,6 +29,8 @@
"FIELD_HEADER_PASSWORD" : "Пароль:",
"FIELD_HEADER_PASSWORD_AGAIN" : "Повтор пароля:",
"FIELD_PLACEHOLDER_FILTER" : "Фильтр",
"FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss",
"INFO_ACTIVE_USER_COUNT" : "Подключено пользователей {USERS}.",
@@ -424,6 +426,12 @@
},
"SETTINGS_CONNECTION_HISTORY" : {
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER"
},
"SETTINGS_PREFERENCES" : {
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
@@ -486,7 +494,7 @@
"DIALOG_HEADER_CONFIRM_DELETE" : "Завершение сессий",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_PLACEHOLDER_FILTER" : "Фильтр",
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",