GUAC-1193: Implement JDBC ConnectionRecordSet. Add MySQL mapping.

This commit is contained in:
James Muehlner
2015-10-06 23:06:21 -07:00
parent ae9a39edb9
commit a631aa803b
6 changed files with 591 additions and 2 deletions

View File

@@ -23,6 +23,7 @@
package org.glyptodon.guacamole.auth.jdbc.connection; package org.glyptodon.guacamole.auth.jdbc.connection;
import java.util.List; import java.util.List;
import java.util.Set;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
/** /**
@@ -57,4 +58,25 @@ public interface ConnectionRecordMapper {
*/ */
int insert(@Param("record") ConnectionRecordModel record); 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.
*
* @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") Set<ConnectionRecordSearchTerm> terms,
@Param("sortPredicates") List<ConnectionRecordSortPredicate> sortPredicates,
@Param("limit") int limit);
} }

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, 0),
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,123 @@
/*
* 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.Collections;
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 {
/**
* Mapper for accessing connection history.
*/
@Inject
private ConnectionRecordMapper connectionRecordMapper;
/**
* 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 {
// Perform the search against the database
List<ConnectionRecordModel> searchResults =
connectionRecordMapper.search(requiredContents,
connectionRecordSortPredicates, limit);
List<ConnectionRecord> modeledSearchResults =
new ArrayList<ConnectionRecord>();
// Convert raw DB records into ConnectionRecords
for(ConnectionRecordModel model : searchResults) {
modeledSearchResults.add(new ModeledConnectionRecord(model));
}
return modeledSearchResults;
}
@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

@@ -32,6 +32,7 @@ import java.util.Collection;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject;
import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionDirectory; 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.connection.ModeledConnection;
import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.form.Form;
@@ -39,7 +40,6 @@ import org.glyptodon.guacamole.net.auth.ActiveConnection;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.Connection;
import org.glyptodon.guacamole.net.auth.ConnectionGroup; 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.Directory;
import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.User;
import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionRecordSet; import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionRecordSet;
@@ -94,6 +94,12 @@ public class UserContext extends RestrictedObject
@Inject @Inject
private Provider<RootConnectionGroup> rootGroupProvider; private Provider<RootConnectionGroup> rootGroupProvider;
/**
* Provider for creating connection record sets.
*/
@Inject
private Provider<ConnectionRecordSet> connectionRecordSetProvider;
@Override @Override
public void init(AuthenticatedUser currentUser) { public void init(AuthenticatedUser currentUser) {
@@ -141,7 +147,9 @@ public class UserContext extends RestrictedObject
@Override @Override
public ConnectionRecordSet getConnectionHistory() public ConnectionRecordSet getConnectionHistory()
throws GuacamoleException { throws GuacamoleException {
return new SimpleConnectionRecordSet(); ConnectionRecordSet connectionRecordSet = connectionRecordSetProvider.get();
connectionRecordSet.init(getCurrentUser());
return connectionRecordSet;
} }
@Override @Override

View File

@@ -72,4 +72,62 @@
</insert> </insert>
<!-- Select all connection records from a given connection -->
<select id="search" resultMap="ConnectionRecordResultMap">
SELECT
guacamole_connection_history.connection_id,
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 ">
<bind name="termPattern" value="'%' + term.term + '%'" />
(
guacamole_connection_history.user_id IN (
SELECT user_id
FROM guacamole_user
WHERE username LIKE #{termPattern,jdbcType=VARCHAR}
)
OR guacamole_connection_history.connection_id IN (
SELECT connection_id
FROM guacamole_connection
WHERE connection_name LIKE #{termPattern,jdbcType=VARCHAR}
)
<if test="term.startDate != null and term.endDate != null">
OR (
(start_date BETWEEN #{term.startDate,jdbcType=DATE} AND #{term.endDate,jdbcType=DATE})
AND (end_date BETWEEN #{term.startDate,jdbcType=DATE} AND #{term.endDate,jdbcType=DATE})
)
</if>
)
</foreach>
<!-- 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> </mapper>