KEYCLOAK-512 Pagination support for sessions

This commit is contained in:
Stian Thorgersen 2014-07-14 11:49:59 +01:00
parent 6bb6dc0aaf
commit 3f68180ee7
11 changed files with 167 additions and 14 deletions

View file

@ -31,6 +31,10 @@ public interface MongoStore {
<T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context);
<T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, DBObject sort, MongoStoreInvocationContext context, int firstResult, int maxResults);
<T extends MongoIdentifiableEntity> int countEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context);
boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context);
boolean removeEntity(Class<? extends MongoIdentifiableEntity> type, String id, MongoStoreInvocationContext context);

View file

@ -280,6 +280,29 @@ public class MongoStoreImpl implements MongoStore {
return convertCursor(type, cursor, context);
}
@Override
public <T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, DBObject sort, MongoStoreInvocationContext context, int firstResult, int maxResults) {
// First we should execute all pending tasks before searching DB
context.beforeDBSearch(type);
DBCollection dbCollection = getDBCollectionForType(type);
DBCursor cursor = dbCollection.find(query);
cursor.skip(firstResult);
cursor.limit(maxResults);
if (sort != null) {
cursor.sort(sort);
}
return convertCursor(type, cursor, context);
}
public <T extends MongoIdentifiableEntity> int countEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context) {
context.beforeDBSearch(type);
DBCollection dbCollection = getDBCollectionForType(type);
DBCursor cursor = dbCollection.find(query);
return cursor.size();
}
@Override
public boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {

View file

@ -44,20 +44,48 @@ module.controller('ApplicationCredentialsCtrl', function($scope, $location, real
});
module.controller('ApplicationSessionsCtrl', function($scope, realm, sessionCount, application,
ApplicationUserSessions,
$location, Dialog, Notifications) {
ApplicationUserSessions) {
$scope.realm = realm;
$scope.count = sessionCount.count;
$scope.sessions = [];
$scope.application = application;
$scope.page = 0;
$scope.query = {
realm : realm.realm,
application: $scope.application.name,
max : 5,
first : 0
}
$scope.firstPage = function() {
$scope.query.first = 0;
if ($scope.query.first < 0) {
$scope.query.first = 0;
}
$scope.loadUsers();
}
$scope.previousPage = function() {
$scope.query.first -= parseInt($scope.query.max);
if ($scope.query.first < 0) {
$scope.query.first = 0;
}
$scope.loadUsers();
}
$scope.nextPage = function() {
$scope.query.first += parseInt($scope.query.max);
$scope.loadUsers();
}
$scope.toDate = function(val) {
return new Date(val);
};
$scope.loadUsers = function() {
ApplicationUserSessions.query({ realm : realm.realm, application: $scope.application.name }, function(updated) {
$scope.count = updated.length;
ApplicationUserSessions.query($scope.query, function(updated) {
$scope.sessions = updated;
})
};

View file

@ -28,19 +28,30 @@
</form>
<table class="table table-striped table-bordered" data-ng-show="count > 0">
<thead>
<tr>
<tr data-ng-hide="sessions">
<th class="kc-table-actions" colspan="3">
<div class="pull-right">
<a class="btn btn-primary" ng-click="loadUsers()">Show Users</a>
<a class="btn btn-primary" ng-click="loadUsers()">Show Sessions</a>
</div>
</th>
</tr>
<tr>
<tr data-ng-show="sessions">
<th>User</th>
<th>From IP</th>
<th>Session Start</th>
</tr>
</thead>
<tfoot data-ng-show="sessions">
<tr>
<td colspan="7">
<div class="table-nav">
<button data-ng-click="firstPage()" class="first" ng-disabled="query.first == 0">First page</button>
<button data-ng-click="previousPage()" class="prev" ng-disabled="query.first == 0">Previous page</button>
<button data-ng-click="nextPage()" class="next" ng-disabled="sessions.length < query.max">Next page</button>
</div>
</td>
</tr>
</tfoot>
<tbody>
<tr data-ng-repeat="session in sessions">
<td><a href="#/realms/{{realm.realm}}/users/{{session.user}}">{{session.user}}</a></td>

View file

@ -16,6 +16,7 @@ public interface UserSessionProvider extends Provider {
UserSessionModel getUserSession(RealmModel realm, String id);
List<UserSessionModel> getUserSessions(RealmModel realm, UserModel user);
List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client);
List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults);
int getActiveUserSessions(RealmModel realm, ClientModel client);
void removeUserSession(RealmModel realm, UserSessionModel session);
void removeUserSessions(RealmModel realm, UserModel user);

View file

@ -97,10 +97,16 @@ public class JpaUserSessionProvider implements UserSessionProvider {
@Override
public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client) {
return getUserSessions(realm, client, 0, Integer.MAX_VALUE);
}
public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
List<UserSessionModel> list = new LinkedList<UserSessionModel>();
TypedQuery<UserSessionEntity> query = em.createNamedQuery("getUserSessionByClient", UserSessionEntity.class)
.setParameter("realmId", realm.getId())
.setParameter("clientId", client.getClientId());
.setParameter("clientId", client.getClientId())
.setFirstResult(firstResult)
.setMaxResults(maxResults);
for (UserSessionEntity entity : query.getResultList()) {
list.add(new UserSessionAdapter(session, em, realm, entity));
}

View file

@ -18,8 +18,8 @@ import java.util.Collection;
*/
@Entity
@NamedQueries({
@NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId"),
@NamedQuery(name = "getUserSessionByClient", query = "select s from UserSessionEntity s join s.clients c where s.realmId = :realmId and c.clientId = :clientId"),
@NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId order by s.started, s.id"),
@NamedQuery(name = "getUserSessionByClient", query = "select s from UserSessionEntity s join s.clients c where s.realmId = :realmId and c.clientId = :clientId order by s.started, s.id"),
@NamedQuery(name = "getActiveUserSessionByClient", query = "select count(s) from UserSessionEntity s join s.clients c where s.realmId = :realmId and c.clientId = :clientId"),
@NamedQuery(name = "removeUserSessionByRealm", query = "delete from UserSessionEntity s where s.realmId = :realmId"),
@NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId"),

View file

@ -15,6 +15,8 @@ import org.keycloak.models.sessions.mem.entities.UsernameLoginFailureKey;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.util.Time;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
@ -80,9 +82,21 @@ public class MemUserSessionProvider implements UserSessionProvider {
clientSessions.add(new UserSessionAdapter(session, realm, s));
}
}
Collections.sort(clientSessions, new UserSessionSort());
return clientSessions;
}
@Override
public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
List<UserSessionModel> userSessions = getUserSessions(realm, client);
if (firstResult > userSessions.size()) {
return Collections.emptyList();
}
int toIndex = (firstResult + maxResults) < userSessions.size() ? firstResult + maxResults : userSessions.size();
return userSessions.subList(firstResult, toIndex);
}
@Override
public int getActiveUserSessions(RealmModel realm, ClientModel client) {
int count = 0;
@ -195,4 +209,17 @@ public class MemUserSessionProvider implements UserSessionProvider {
public void close() {
}
private class UserSessionSort implements Comparator<UserSessionModel> {
@Override
public int compare(UserSessionModel o1, UserSessionModel o2) {
int r = o1.getStarted() - o2.getStarted();
if (r == 0) {
return o1.getId().compareTo(o2.getId());
} else {
return r;
}
}
}
}

View file

@ -84,9 +84,26 @@ public class MongoUserSessionProvider implements UserSessionProvider {
return result;
}
public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
DBObject query = new QueryBuilder()
.and("associatedClientIds").is(client.getId())
.get();
DBObject sort = new BasicDBObject("started", 1).append("id", 1);
List<MongoUserSessionEntity> sessions = mongoStore.loadEntities(MongoUserSessionEntity.class, query, sort, invocationContext, firstResult, maxResults);
List<UserSessionModel> result = new LinkedList<UserSessionModel>();
for (MongoUserSessionEntity session : sessions) {
result.add(new UserSessionAdapter(session, realm, invocationContext));
}
return result;
}
@Override
public int getActiveUserSessions(RealmModel realm, ClientModel client) {
return getUserSessions(realm, client).size();
DBObject query = new QueryBuilder()
.and("associatedClientIds").is(client.getId())
.get();
return mongoStore.countEntities(MongoUserSessionEntity.class, query, invocationContext);
}
@Override

View file

@ -340,10 +340,12 @@ public class ApplicationResource {
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<UserSessionRepresentation> getUserSessions() {
public List<UserSessionRepresentation> getUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) {
auth.requireView();
firstResult = firstResult != null ? firstResult : 0;
maxResults = maxResults != null ? maxResults : Integer.MAX_VALUE;
List<UserSessionRepresentation> sessions = new ArrayList<UserSessionRepresentation>();
for (UserSessionModel userSession : session.sessions().getUserSessions(application.getRealm(), application)) {
for (UserSessionModel userSession : session.sessions().getUserSessions(application.getRealm(), application, firstResult, maxResults)) {
UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession);
sessions.add(rep);
}

View file

@ -4,6 +4,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -112,6 +113,40 @@ public class UserSessionProviderTest {
assertSessions(session.sessions().getUserSessions(realm, realm.findClient("third-party")), sessions[0]);
}
@Test
public void testGetByClientPaginated() {
for (int i = 0; i < 25; i++) {
UserSessionModel userSession = session.sessions().createUserSession(realm, realm.getUser("user1"), "127.0.0." + i);
userSession.setStarted(Time.currentTime() + i);
userSession.associateClient(realm.findClient("test-app"));
}
resetSession();
assertPaginatedSession(realm, realm.findClient("test-app"), 0, 1, 1);
assertPaginatedSession(realm, realm.findClient("test-app"), 0, 10, 10);
assertPaginatedSession(realm, realm.findClient("test-app"), 10, 10, 10);
assertPaginatedSession(realm, realm.findClient("test-app"), 20, 10, 5);
assertPaginatedSession(realm, realm.findClient("test-app"), 30, 10, 0);
}
private void assertPaginatedSession(RealmModel realm, ClientModel client, int start, int max, int expectedSize) {
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, client, start, max);
String[] actualIps = new String[sessions.size()];
for (int i = 0; i < actualIps.length; i++) {
actualIps[i] = sessions.get(i).getIpAddress();
}
String[] expectedIps = new String[expectedSize];
for (int i = 0; i < expectedSize; i++) {
expectedIps[i] = "127.0.0." + (i + start);
}
assertArrayEquals(expectedIps, actualIps);
}
@Test
public void testGetCountByClient() {
createSessions();
@ -175,5 +210,4 @@ public class UserSessionProviderTest {
assertArrayEquals(clients, actualClients);
}
}