KEYCLOAK-512 Pagination support for sessions
This commit is contained in:
parent
6bb6dc0aaf
commit
3f68180ee7
11 changed files with 167 additions and 14 deletions
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue