keycloak-uncached
Changes
forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js 36(+32 -4)
forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-sessions.html 17(+14 -3)
model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java 4(+2 -2)
model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java 8(+7 -1)
model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java 27(+27 -0)
Details
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 ccf41b5..87d3669 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 {
<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);
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 9f96056..889194d 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 <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) {
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 2671891..60560f3 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 93c388e..248838a 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 @@
</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>
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 f678328..fa643b3 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<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);
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 df406f7..2f27f34 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-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 770c326..182019e 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<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));
}
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 ba24c1d..d5369e4 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,10 +82,22 @@ 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;
for (UserSessionEntity s : sessions.values()) {
@@ -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;
+ }
+ }
+ }
+
}
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 68de8f4..1cbcb41 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<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
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 6814ae8..919fcce 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<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);
}
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 c73a021..cb58949 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;
@@ -113,6 +114,40 @@ public class UserSessionProviderTest {
}
@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);
}
-
}