From 3f68180ee7b098cd31e70c5bdd4a5b12656ed86a Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 14 Jul 2014 11:49:59 +0100 Subject: [PATCH] KEYCLOAK-512 Pagination support for sessions --- .../connections/mongo/api/MongoStore.java | 4 +++ .../mongo/impl/MongoStoreImpl.java | 23 ++++++++++++ .../resources/js/controllers/applications.js | 36 ++++++++++++++++--- .../partials/application-sessions.html | 17 +++++++-- .../keycloak/models/UserSessionProvider.java | 1 + .../sessions/jpa/JpaUserSessionProvider.java | 8 ++++- .../jpa/entities/UserSessionEntity.java | 4 +-- .../sessions/mem/MemUserSessionProvider.java | 27 ++++++++++++++ .../mongo/MongoUserSessionProvider.java | 19 +++++++++- .../resources/admin/ApplicationResource.java | 6 ++-- .../model/UserSessionProviderTest.java | 36 ++++++++++++++++++- 11 files changed, 167 insertions(+), 14 deletions(-) diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/MongoStore.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/MongoStore.java index ccf41b541b..87d3669aa8 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/MongoStore.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/MongoStore.java @@ -31,6 +31,10 @@ public interface MongoStore { List loadEntities(Class type, DBObject query, MongoStoreInvocationContext context); + List loadEntities(Class type, DBObject query, DBObject sort, MongoStoreInvocationContext context, int firstResult, int maxResults); + + int countEntities(Class type, DBObject query, MongoStoreInvocationContext context); + boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context); boolean removeEntity(Class type, String id, MongoStoreInvocationContext context); diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java index 9f96056481..889194da37 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java @@ -280,6 +280,29 @@ public class MongoStoreImpl implements MongoStore { return convertCursor(type, cursor, context); } + @Override + public List loadEntities(Class 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 int countEntities(Class 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) { diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js index 2671891f99..60560f3e30 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js @@ -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; }) }; diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-sessions.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-sessions.html index 93c388e36e..248838a0bd 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-sessions.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-sessions.html @@ -28,19 +28,30 @@ - + - + + + + + + diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java index f678328b6f..fa643b3d76 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -16,6 +16,7 @@ public interface UserSessionProvider extends Provider { UserSessionModel getUserSession(RealmModel realm, String id); List getUserSessions(RealmModel realm, UserModel user); List getUserSessions(RealmModel realm, ClientModel client); + List 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); diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java index 770c326bd2..182019ef91 100644 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java @@ -97,10 +97,16 @@ public class JpaUserSessionProvider implements UserSessionProvider { @Override public List getUserSessions(RealmModel realm, ClientModel client) { + return getUserSessions(realm, client, 0, Integer.MAX_VALUE); + } + + public List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { List list = new LinkedList(); TypedQuery 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)); } diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java index df406f7154..2f27f34d06 100755 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java @@ -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"), diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java index ba24c1d67d..d5369e4291 100644 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java @@ -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 getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { + List 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 { + + @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; + } + } + } + } diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java index 68de8f43aa..1cbcb4137c 100644 --- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java @@ -84,9 +84,26 @@ public class MongoUserSessionProvider implements UserSessionProvider { return result; } + public List 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 sessions = mongoStore.loadEntities(MongoUserSessionEntity.class, query, sort, invocationContext, firstResult, maxResults); + List result = new LinkedList(); + 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 diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java index 6814ae86a2..919fcce413 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java @@ -340,10 +340,12 @@ public class ApplicationResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List getUserSessions() { + public List getUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) { auth.requireView(); + firstResult = firstResult != null ? firstResult : 0; + maxResults = maxResults != null ? maxResults : Integer.MAX_VALUE; List sessions = new ArrayList(); - 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); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index c73a021469..cb58949071 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -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 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); } - }
User From IP Session Start
+
+ + + +
+
{{session.user}}