From ab8de6ba2533b3e7394b461d309fda33e1bb8c59 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Sat, 17 May 2014 14:24:32 -0400 Subject: [PATCH] client user-session association --- .../idm/UserSessionRepresentation.java | 67 +++++++ .../src/main/webapp/WEB-INF/keycloak.json | 1 + .../src/main/webapp/WEB-INF/keycloak.json | 3 +- .../theme/admin/base/resources/js/app.js | 12 +- .../resources/js/controllers/applications.js | 33 +--- .../base/resources/js/controllers/users.js | 25 ++- .../theme/admin/base/resources/js/loaders.js | 26 +++ .../theme/admin/base/resources/js/services.js | 34 ++++ .../partials/application-sessions.html | 23 +-- .../resources/partials/session-realm.html | 4 +- .../resources/partials/user-sessions.html | 30 +++- .../adapters/PreAuthActionsHandler.java | 4 +- .../adapters/UserSessionManagement.java | 4 +- .../as7/CatalinaRequestAuthenticator.java | 2 +- .../as7/CatalinaUserSessionManagement.java | 112 +++++++----- .../tomcat7/CatalinaRequestAuthenticator.java | 2 +- .../CatalinaUserSessionManagement.java | 113 +++++++----- .../undertow/ServletRequestAuthenticator.java | 2 +- .../undertow/SessionManagementBridge.java | 10 +- .../UndertowUserSessionManagement.java | 166 ++++++++++-------- .../java/org/keycloak/models/ClientModel.java | 4 + .../java/org/keycloak/models/RealmModel.java | 3 + .../org/keycloak/models/UserSessionModel.java | 8 + .../models/jpa/ApplicationAdapter.java | 2 +- .../keycloak/models/jpa/ClientAdapter.java | 37 +++- .../models/jpa/JpaKeycloakSession.java | 1 + .../models/jpa/OAuthClientAdapter.java | 5 +- .../org/keycloak/models/jpa/RealmAdapter.java | 36 +++- .../models/jpa/UserSessionAdapter.java | 61 ++++++- .../ClientUserSessionAssociationEntity.java | 83 +++++++++ .../jpa/entities/UserSessionEntity.java | 29 +++ .../test/resources/META-INF/persistence.xml | 1 + .../keycloak/adapters/ClientAdapter.java | 26 +++ .../mongo/keycloak/adapters/RealmAdapter.java | 17 +- .../keycloak/adapters/UserSessionAdapter.java | 76 +++++++- .../entities/MongoApplicationEntity.java | 6 + ...ngoClientUserSessionAssociationEntity.java | 37 ++++ .../entities/MongoOAuthClientEntity.java | 6 + .../keycloak/entities/MongoRealmEntity.java | 3 + .../entities/MongoUserSessionEntity.java | 19 +- .../main/resources/META-INF/persistence.xml | 1 + .../main/resources/META-INF/persistence.xml | 1 + .../managers/ModelToRepresentation.java | 19 ++ .../managers/ResourceAdminManager.java | 16 ++ .../services/resources/TokenService.java | 3 + .../resources/admin/ApplicationResource.java | 32 +++- .../resources/admin/RealmAdminResource.java | 27 +++ .../resources/admin/UsersResource.java | 23 +++ .../main/resources/META-INF/persistence.xml | 1 + .../test/resources/META-INF/persistence.xml | 1 + 50 files changed, 993 insertions(+), 264 deletions(-) create mode 100755 core/src/main/java/org/keycloak/representations/idm/UserSessionRepresentation.java create mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientUserSessionAssociationEntity.java mode change 100644 => 100755 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java create mode 100755 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientUserSessionAssociationEntity.java diff --git a/core/src/main/java/org/keycloak/representations/idm/UserSessionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserSessionRepresentation.java new file mode 100755 index 0000000000..b3f3c5affc --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/UserSessionRepresentation.java @@ -0,0 +1,67 @@ +package org.keycloak.representations.idm; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserSessionRepresentation { + private String id; + private String user; + private String ipAddress; + private long start; + private Set applications = new HashSet(); + private Map clients = new HashMap(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public long getStart() { + return start; + } + + public void setStart(long start) { + this.start = start; + } + + public Set getApplications() { + return applications; + } + + public void setApplications(Set applications) { + this.applications = applications; + } + + public Map getClients() { + return clients; + } + + public void setClients(Map clients) { + this.clients = clients; + } +} diff --git a/examples/cors/database-service/src/main/webapp/WEB-INF/keycloak.json b/examples/cors/database-service/src/main/webapp/WEB-INF/keycloak.json index c64ff4149e..066aec8a2c 100755 --- a/examples/cors/database-service/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/cors/database-service/src/main/webapp/WEB-INF/keycloak.json @@ -3,5 +3,6 @@ "resource" : "database-service", "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "bearer-only" : true, + "ssl-not-required": true, "enable-cors": true } diff --git a/examples/demo-template/database-service/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/database-service/src/main/webapp/WEB-INF/keycloak.json index 2e02cc3590..adf176ce8e 100755 --- a/examples/demo-template/database-service/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/demo-template/database-service/src/main/webapp/WEB-INF/keycloak.json @@ -3,6 +3,5 @@ "resource" : "database-service", "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "bearer-only" : true, - "enable-cors" : true - + "ssl-not-required": true } diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js index f73d94754c..e633dcab13 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js @@ -290,8 +290,8 @@ module.config([ '$routeProvider', function($routeProvider) { user : function(UserLoader) { return UserLoader(); }, - stats : function(UserSessionStatsLoader) { - return UserSessionStatsLoader(); + sessions : function(UserSessionsLoader) { + return UserSessionsLoader(); } }, controller : 'UserSessionsCtrl' @@ -436,8 +436,8 @@ module.config([ '$routeProvider', function($routeProvider) { application : function(ApplicationLoader) { return ApplicationLoader(); }, - stats : function(ApplicationSessionStatsLoader) { - return ApplicationSessionStatsLoader(); + sessionCount : function(ApplicationSessionCountLoader) { + return ApplicationSessionCountLoader(); } }, controller : 'ApplicationSessionsCtrl' @@ -705,8 +705,8 @@ module.config([ '$routeProvider', function($routeProvider) { realm : function(RealmLoader) { return RealmLoader(); }, - stats : function(RealmSessionStatsLoader) { - return RealmSessionStatsLoader(); + stats : function(RealmApplicationSessionStatsLoader) { + return RealmApplicationSessionStatsLoader(); } }, controller : 'RealmSessionStatsCtrl' 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 9f7615d9c1..ef54e1e709 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 @@ -43,15 +43,12 @@ module.controller('ApplicationCredentialsCtrl', function($scope, $location, real }); }); -module.controller('ApplicationSessionsCtrl', function($scope, realm, stats, application, - ApplicationLogoutUser, - ApplicationLogoutAll, - ApplicationSessionStats, - ApplicationSessionStatsWithUsers, +module.controller('ApplicationSessionsCtrl', function($scope, realm, sessionCount, application, + ApplicationUserSessions, $location, Dialog, Notifications) { $scope.realm = realm; - $scope.stats = stats; - $scope.users = {}; + $scope.count = sessionCount.count; + $scope.sessions = []; $scope.application = application; $scope.toDate = function(val) { @@ -59,27 +56,11 @@ module.controller('ApplicationSessionsCtrl', function($scope, realm, stats, appl }; $scope.loadUsers = function() { - ApplicationSessionStatsWithUsers.get({ realm : realm.realm, application: $scope.application.name }, function(updated) { - $scope.stats = updated; - $scope.users = updated.users; + ApplicationUserSessions.query({ realm : realm.realm, application: $scope.application.name }, function(updated) { + $scope.count = updated.length; + $scope.sessions = updated; }) }; - - $scope.logoutAll = function() { - ApplicationLogoutAll.save({realm : realm.realm, application: $scope.application.name}, function () { - Notifications.success('Logged out all users'); - $scope.loadUsers(); - }); - }; - - $scope.logoutUser = function(user) { - console.log('Trying to logout user: ' + user); - ApplicationLogoutUser.save({realm : realm.realm, application: $scope.application.name, user: user}, function () { - Notifications.success('Logged out user' + user); - $scope.loadUsers(); - }); - }; - }); module.controller('ApplicationClaimsCtrl', function($scope, realm, application, claims, diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js index ee05261fe5..35e4fb0161 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js @@ -99,32 +99,29 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, ap }); -module.controller('UserSessionsCtrl', function($scope, realm, user, stats, UserLogout, ApplicationLogoutUser, UserSessionStats, Notifications) { +module.controller('UserSessionsCtrl', function($scope, realm, user, sessions, UserSessions, UserLogout, UserSessionLogout, Notifications) { $scope.realm = realm; $scope.user = user; - $scope.stats = stats; + $scope.sessions = sessions; $scope.logoutAll = function() { UserLogout.save({realm : realm.realm, user: user.username}, function () { Notifications.success('Logged out user in all applications'); - UserSessionStats.get({realm: realm.realm, user: user.username}, function(updated) { - $scope.stats = updated; + UserSessions.get({realm: realm.realm, user: user.username}, function(updated) { + $scope.sessions = updated; }) }); }; - $scope.logoutApplication = function(app) { - console.log('log user out of app: ' + app); - ApplicationLogoutUser.save({realm : realm.realm, application: app, user: user.username}, function () { - Notifications.success('Logged out user from application'); - UserSessionStats.get({realm: realm.realm, user: user.username}, function(updated) { - $scope.stats = updated; + $scope.logoutSession = function(sessionId) { + console.log('here in logoutSession'); + UserSessionLogout.delete({realm : realm.realm, session: sessionId}, function() { + Notifications.success('Logged out session'); + UserSessions.get({realm: realm.realm, user: user.username}, function(updated) { + $scope.sessions = updated; }) }); - }; - - - + } }); module.controller('UserSocialCtrl', function($scope, realm, user, socialLinks) { diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js index 53101e4974..81a77b7be2 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js @@ -71,6 +71,14 @@ module.factory('RealmSessionStatsLoader', function(Loader, RealmSessionStats, $r }); }); +module.factory('RealmApplicationSessionStatsLoader', function(Loader, RealmApplicationSessionStats, $route, $q) { + return Loader.get(RealmApplicationSessionStats, function() { + return { + realm : $route.current.params.realm + } + }); +}); + module.factory('UserLoader', function(Loader, User, $route, $q) { return Loader.get(User, function() { return { @@ -89,6 +97,15 @@ module.factory('UserSessionStatsLoader', function(Loader, UserSessionStats, $rou }); }); +module.factory('UserSessionsLoader', function(Loader, UserSessions, $route, $q) { + return Loader.query(UserSessions, function() { + return { + realm : $route.current.params.realm, + user : $route.current.params.user + } + }); +}); + module.factory('UserSocialLinksLoader', function(Loader, UserSocialLinks, $route, $q) { return Loader.query(UserSocialLinks, function() { return { @@ -134,6 +151,15 @@ module.factory('ApplicationSessionStatsLoader', function(Loader, ApplicationSess }); }); +module.factory('ApplicationSessionCountLoader', function(Loader, ApplicationSessionCount, $route, $q) { + return Loader.get(ApplicationSessionCount, function() { + return { + realm : $route.current.params.realm, + application : $route.current.params.application + } + }); +}); + module.factory('ApplicationClaimsLoader', function(Loader, ApplicationClaims, $route, $q) { return Loader.get(ApplicationClaims, function() { return { diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js index 6d33070147..0294169311 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js @@ -183,6 +183,20 @@ module.factory('UserSessionStats', function($resource) { user : '@user' }); }); +module.factory('UserSessions', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/users/:user/sessions', { + realm : '@realm', + user : '@user' + }); +}); + +module.factory('UserSessionLogout', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/sessions/:session', { + realm : '@realm', + session : '@session' + }); +}); + module.factory('UserLogout', function($resource) { return $resource(authUrl + '/admin/realms/:realm/users/:user/logout', { realm : '@realm', @@ -347,6 +361,12 @@ module.factory('RealmSessionStats', function($resource) { }); }); +module.factory('RealmApplicationSessionStats', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/application-session-stats', { + realm : '@realm' + }); +}); + module.factory('RoleApplicationComposites', function($resource) { return $resource(authUrl + '/admin/realms/:realm/roles-by-id/:role/composites/applications/:application', { @@ -589,6 +609,20 @@ module.factory('ApplicationSessionStatsWithUsers', function($resource) { }); }); +module.factory('ApplicationSessionCount', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/applications/:application/session-count', { + realm : '@realm', + application : "@application" + }); +}); + +module.factory('ApplicationUserSessions', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/applications/:application/user-sessions', { + realm : '@realm', + application : "@application" + }); +}); + module.factory('ApplicationLogoutAll', function($resource) { return $resource(authUrl + '/admin/realms/:realm/applications/:application/logout-all', { realm : '@realm', 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 1bb3682c95..93c388e36e 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 @@ -21,38 +21,31 @@
- -
-
-
- -
- +
- +
- - + + - - - - + + + +
UserLogin TimeFrom IPSession Start
{{user}}{{data.whenLoggedIn | date:'medium'}}invalidate session
{{session.user}}{{session.ipAddress}}{{session.start | date:'medium'}}
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/session-realm.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/session-realm.html index c50a19219e..bea3c027f2 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/session-realm.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/session-realm.html @@ -24,14 +24,12 @@ Application Active Sessions - Active Users {{application}} - {{data.activeSessions}} - {{data.activeUsers}} + {{data}} diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-sessions.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-sessions.html index ef90aa2e03..cb84ccb6cd 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-sessions.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-sessions.html @@ -18,23 +18,37 @@ - - + - + + + - - - - + + + + + +
+
ApplicationIP Address Login TimeApplicationsOAuth ClientsAction
{{application}}{{data.whenLoggedIn | date:'medium'}}invalidate session
{{session.ipAddress}}{{session.start | date:'medium'}} + + logout
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java index 0842b8da98..16203a8700 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java @@ -120,7 +120,9 @@ public class PreAuthActionsHandler { String user = action.getUser(); if (user != null) { log.info("logout of session for: " + user); - userSessionManagement.logout(user); + userSessionManagement.logoutUser(user); + } else if (action.getSession() != null) { + userSessionManagement.logoutKeycloakSession(action.getSession()); } else { log.info("logout of all sessions"); if (action.getNotBefore() > deployment.getNotBefore()) { diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/UserSessionManagement.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/UserSessionManagement.java index 94da30fdbe..ca4653a20b 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/UserSessionManagement.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/UserSessionManagement.java @@ -15,5 +15,7 @@ public interface UserSessionManagement { void logoutAll(); - void logout(String user); + void logoutUser(String user); + + void logoutKeycloakSession(String id); } diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java index 9e211b237c..9a5ea4eb27 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java @@ -62,7 +62,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { session.setNote(KeycloakSecurityContext.class.getName(), securityContext); String username = securityContext.getToken().getSubject(); log.debug("userSessionManage.login: " + username); - userSessionManagement.login(session, username); + userSessionManagement.login(session, username, securityContext.getToken().getSessionState()); } @Override diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagement.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagement.java index 52af99348e..ecca57ebda 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagement.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagement.java @@ -8,6 +8,7 @@ import org.jboss.logging.Logger; import org.keycloak.adapters.UserSessionManagement; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -23,31 +24,21 @@ import java.util.concurrent.ConcurrentHashMap; public class CatalinaUserSessionManagement implements SessionListener, UserSessionManagement { private static final Logger log = Logger.getLogger(CatalinaUserSessionManagement.class); protected ConcurrentHashMap userSessionMap = new ConcurrentHashMap(); + protected ConcurrentHashMap keycloakSessionMap = new ConcurrentHashMap(); public static class UserSessions { - protected Map sessions = new ConcurrentHashMap(); + protected String user; protected long loggedIn = System.currentTimeMillis(); - - - public Map getSessions() { - return sessions; - } - + protected Map keycloakSessionToHttpSession = new HashMap(); + protected Map httpSessionToKeycloakSession = new HashMap(); + protected Map sessions = new HashMap(); public long getLoggedIn() { return loggedIn; } } - @Override - public int getActiveSessions() { - int active = 0; - synchronized (userSessionMap) { - for (UserSessions sessions : userSessionMap.values()) { - active += sessions.getSessions().size(); - } - - } - return active; + public synchronized int getActiveSessions() { + return keycloakSessionMap.size(); } /** @@ -56,60 +47,85 @@ public class CatalinaUserSessionManagement implements SessionListener, UserSessi * @return null if user not logged in */ @Override - public Long getUserLoginTime(String username) { + public synchronized Long getUserLoginTime(String username) { UserSessions sessions = userSessionMap.get(username); if (sessions == null) return null; return sessions.getLoggedIn(); } @Override - public Set getActiveUsers() { + public synchronized Set getActiveUsers() { HashSet set = new HashSet(); set.addAll(userSessionMap.keySet()); return set; } - protected void login(Session session, String username) { - synchronized (userSessionMap) { - UserSessions userSessions = userSessionMap.get(username); - if (userSessions == null) { - userSessions = new UserSessions(); - userSessionMap.put(username, userSessions); - } - userSessions.getSessions().put(session.getId(), session); + public synchronized void login(Session session, String username, String keycloakSessionId) { + String sessionId = session.getId(); + + UserSessions sessions = userSessionMap.get(username); + if (sessions == null) { + sessions = new UserSessions(); + sessions.user = username; + userSessionMap.put(username, sessions); } + keycloakSessionMap.put(keycloakSessionId, sessions); + sessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId); + sessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId); session.addSessionListener(this); } @Override public void logoutAll() { - List users = new ArrayList(); - users.addAll(userSessionMap.keySet()); - for (String user : users) logout(user); + for (String user : userSessionMap.keySet()) logoutUser(user); } @Override - public void logout(String user) { + public void logoutUser(String user) { log.debug("logoutUser: " + user); UserSessions sessions = null; - synchronized (userSessionMap) { - sessions = userSessionMap.remove(user); - - } + sessions = userSessionMap.remove(user); if (sessions == null) { log.debug("no session for user: " + user); return; - } log.debug("found session for user"); - for (Session session : sessions.getSessions().values()) { + for (Map.Entry entry : sessions.httpSessionToKeycloakSession.entrySet()) { + log.debug("invalidating session for user: " + user); + String sessionId = entry.getKey(); + String keycloakSessionId = entry.getValue(); + Session session = sessions.sessions.get(sessionId); session.setPrincipal(null); session.setAuthType(null); session.getSession().invalidate(); + keycloakSessionMap.remove(keycloakSessionId); } } + public synchronized void logoutKeycloakSession(String keycloakSessionId) { + log.debug("logoutKeycloakSession: " + keycloakSessionId); + UserSessions sessions = keycloakSessionMap.remove(keycloakSessionId); + if (sessions == null) { + log.debug("no session for keycloak session id: " + keycloakSessionId); + return; + } + String sessionId = sessions.keycloakSessionToHttpSession.remove(keycloakSessionId); + if (sessionId == null) { + log.debug("no session for keycloak session id: " + keycloakSessionId); + + } + sessions.httpSessionToKeycloakSession.remove(sessionId); + Session session = sessions.sessions.remove(sessionId); + session.setPrincipal(null); + session.setAuthType(null); + session.getSession().invalidate(); + if (sessions.keycloakSessionToHttpSession.size() == 0) { + userSessionMap.remove(sessions.user); + } + } + + public void sessionEvent(SessionEvent event) { // We only care about session destroyed events if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()) @@ -124,14 +140,22 @@ public class CatalinaUserSessionManagement implements SessionListener, UserSessi session.setAuthType(null); String username = principal.getUserPrincipal().getName(); - synchronized (userSessionMap) { - UserSessions sessions = userSessionMap.get(username); - if (sessions != null) { - sessions.getSessions().remove(session.getId()); - if (sessions.getSessions().isEmpty()) { - userSessionMap.remove(username); - } + UserSessions userSessions = userSessionMap.get(username); + if (userSessions == null) { + return; + } + String sessionid = session.getId(); + synchronized (this) { + String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(sessionid); + if (keycloakSessionId != null) { + userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId); + keycloakSessionMap.remove(keycloakSessionId); } + userSessions.sessions.remove(sessionid); + if (userSessions.httpSessionToKeycloakSession.size() == 0) { + userSessionMap.remove(username); + } + } } } diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaRequestAuthenticator.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaRequestAuthenticator.java index 7ed1f1d371..27a003dd25 100755 --- a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaRequestAuthenticator.java +++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaRequestAuthenticator.java @@ -62,7 +62,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { session.setNote(KeycloakSecurityContext.class.getName(), securityContext); String username = securityContext.getToken().getSubject(); log.finer("userSessionManage.login: " + username); - userSessionManagement.login(session, username); + userSessionManagement.login(session, username, securityContext.getToken().getSessionState()); } @Override diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagement.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagement.java index a84284e5e1..8b521e88ad 100755 --- a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagement.java +++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagement.java @@ -1,6 +1,7 @@ package org.keycloak.adapters.tomcat7; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -23,31 +24,21 @@ import org.keycloak.adapters.UserSessionManagement; public class CatalinaUserSessionManagement implements SessionListener, UserSessionManagement { private static final Logger log = Logger.getLogger(""+CatalinaUserSessionManagement.class); protected ConcurrentHashMap userSessionMap = new ConcurrentHashMap(); + protected ConcurrentHashMap keycloakSessionMap = new ConcurrentHashMap(); public static class UserSessions { - protected Map sessions = new ConcurrentHashMap(); + protected String user; protected long loggedIn = System.currentTimeMillis(); - - - public Map getSessions() { - return sessions; - } - + protected Map keycloakSessionToHttpSession = new HashMap(); + protected Map httpSessionToKeycloakSession = new HashMap(); + protected Map sessions = new HashMap(); public long getLoggedIn() { return loggedIn; } } - @Override - public int getActiveSessions() { - int active = 0; - synchronized (userSessionMap) { - for (UserSessions sessions : userSessionMap.values()) { - active += sessions.getSessions().size(); - } - - } - return active; + public synchronized int getActiveSessions() { + return keycloakSessionMap.size(); } /** @@ -56,60 +47,78 @@ public class CatalinaUserSessionManagement implements SessionListener, UserSessi * @return null if user not logged in */ @Override - public Long getUserLoginTime(String username) { + public synchronized Long getUserLoginTime(String username) { UserSessions sessions = userSessionMap.get(username); if (sessions == null) return null; return sessions.getLoggedIn(); } @Override - public Set getActiveUsers() { + public synchronized Set getActiveUsers() { HashSet set = new HashSet(); set.addAll(userSessionMap.keySet()); return set; } - protected void login(Session session, String username) { - synchronized (userSessionMap) { - UserSessions userSessions = userSessionMap.get(username); - if (userSessions == null) { - userSessions = new UserSessions(); - userSessionMap.put(username, userSessions); - } - userSessions.getSessions().put(session.getId(), session); + + public synchronized void login(Session session, String username, String keycloakSessionId) { + String sessionId = session.getId(); + + UserSessions sessions = userSessionMap.get(username); + if (sessions == null) { + sessions = new UserSessions(); + sessions.user = username; + userSessionMap.put(username, sessions); } + keycloakSessionMap.put(keycloakSessionId, sessions); + sessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId); + sessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId); session.addSessionListener(this); } @Override public void logoutAll() { - List users = new ArrayList(); - users.addAll(userSessionMap.keySet()); - for (String user : users) logout(user); + for (String user : userSessionMap.keySet()) logoutUser(user); } @Override - public void logout(String user) { - log.finer("logoutUser: " + user); + public void logoutUser(String user) { UserSessions sessions = null; - synchronized (userSessionMap) { - sessions = userSessionMap.remove(user); - - } + sessions = userSessionMap.remove(user); if (sessions == null) { - log.finer("no session for user: " + user); return; - } - - log.finer("found session for user"); - for (Session session : sessions.getSessions().values()) { + for (Map.Entry entry : sessions.httpSessionToKeycloakSession.entrySet()) { + String sessionId = entry.getKey(); + String keycloakSessionId = entry.getValue(); + Session session = sessions.sessions.get(sessionId); session.setPrincipal(null); session.setAuthType(null); session.getSession().invalidate(); + keycloakSessionMap.remove(keycloakSessionId); } } + public synchronized void logoutKeycloakSession(String keycloakSessionId) { + UserSessions sessions = keycloakSessionMap.remove(keycloakSessionId); + if (sessions == null) { + return; + } + String sessionId = sessions.keycloakSessionToHttpSession.remove(keycloakSessionId); + if (sessionId == null) { + + } + sessions.httpSessionToKeycloakSession.remove(sessionId); + Session session = sessions.sessions.remove(sessionId); + session.setPrincipal(null); + session.setAuthType(null); + session.getSession().invalidate(); + if (sessions.keycloakSessionToHttpSession.size() == 0) { + userSessionMap.remove(sessions.user); + } + } + + public void sessionEvent(SessionEvent event) { // We only care about session destroyed events if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()) @@ -124,14 +133,22 @@ public class CatalinaUserSessionManagement implements SessionListener, UserSessi session.setAuthType(null); String username = principal.getUserPrincipal().getName(); - synchronized (userSessionMap) { - UserSessions sessions = userSessionMap.get(username); - if (sessions != null) { - sessions.getSessions().remove(session.getId()); - if (sessions.getSessions().isEmpty()) { - userSessionMap.remove(username); - } + UserSessions userSessions = userSessionMap.get(username); + if (userSessions == null) { + return; + } + String sessionid = session.getId(); + synchronized (this) { + String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(sessionid); + if (keycloakSessionId != null) { + userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId); + keycloakSessionMap.remove(keycloakSessionId); } + userSessions.sessions.remove(sessionid); + if (userSessions.httpSessionToKeycloakSession.size() == 0) { + userSessionMap.remove(username); + } + } } } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java index f9c2edf736..f73227f287 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java @@ -66,7 +66,7 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator { HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); HttpSession session = req.getSession(true); session.setAttribute(KeycloakUndertowAccount.class.getName(), account); - userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager(), session, account.getPrincipal().getName()); + userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager(), session, account.getPrincipal().getName(), account.getKeycloakSecurityContext().getToken().getSessionState()); } } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/SessionManagementBridge.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/SessionManagementBridge.java index 4a5a4c24a4..11682f0246 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/SessionManagementBridge.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/SessionManagementBridge.java @@ -1,6 +1,5 @@ package org.keycloak.adapters.undertow; -import io.undertow.server.HttpServerExchange; import io.undertow.server.session.SessionManager; import org.keycloak.adapters.UserSessionManagement; @@ -41,7 +40,12 @@ public class SessionManagementBridge implements UserSessionManagement { } @Override - public void logout(String user) { - userSessionManagement.logout(sessionManager, user); + public void logoutUser(String user) { + userSessionManagement.logoutUser(sessionManager, user); + } + + @Override + public void logoutKeycloakSession(String id) { + userSessionManagement.logoutKeycloakSession(sessionManager, id); } } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java index ff75800aba..1c11d37517 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java @@ -6,19 +6,14 @@ import io.undertow.server.session.Session; import io.undertow.server.session.SessionListener; import io.undertow.server.session.SessionManager; import io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler; -import io.undertow.util.StatusCodes; import org.jboss.logging.Logger; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.representations.adapters.action.LogoutAction; -import org.keycloak.util.JsonSerialization; -import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -32,30 +27,22 @@ public class UndertowUserSessionManagement implements SessionListener { private static final Logger log = Logger.getLogger(UndertowUserSessionManagement.class); private static final String AUTH_SESSION_NAME = CachedAuthenticatedSessionHandler.class.getName() + ".AuthenticatedSession"; protected ConcurrentHashMap userSessionMap = new ConcurrentHashMap(); + protected ConcurrentHashMap keycloakSessionMap = new ConcurrentHashMap(); + protected volatile boolean registered; public static class UserSessions { - protected Set sessionIds = new HashSet(); + protected String user; protected long loggedIn = System.currentTimeMillis(); - - public Set getSessionIds() { - return sessionIds; - } - + protected Map keycloakSessionToHttpSession = new HashMap(); + protected Map httpSessionToKeycloakSession = new HashMap(); public long getLoggedIn() { return loggedIn; } } - public int getActiveSessions() { - int active = 0; - synchronized (userSessionMap) { - for (UserSessions sessions : userSessionMap.values()) { - active += sessions.getSessionIds().size(); - } - - } - return active; + public synchronized int getActiveSessions() { + return keycloakSessionMap.size(); } /** @@ -63,74 +50,88 @@ public class UndertowUserSessionManagement implements SessionListener { * @param username * @return null if user not logged in */ - public Long getUserLoginTime(String username) { + public synchronized Long getUserLoginTime(String username) { UserSessions sessions = userSessionMap.get(username); if (sessions == null) return null; return sessions.getLoggedIn(); } - public Set getActiveUsers() { + public synchronized Set getActiveUsers() { HashSet set = new HashSet(); set.addAll(userSessionMap.keySet()); return set; } - public void login(SessionManager manager, HttpSession session, String username) { + public synchronized void login(SessionManager manager, HttpSession session, String username, String keycloakSessionId) { String sessionId = session.getId(); - addAuthenticatedSession(username, sessionId); - manager.registerSessionListener(this); - } - protected void addAuthenticatedSession(String username, String sessionId) { - synchronized (userSessionMap) { - UserSessions sessions = userSessionMap.get(username); - if (sessions == null) { - sessions = new UserSessions(); - userSessionMap.put(username, sessions); - } - sessions.getSessionIds().add(sessionId); - } - } - - protected void removeAuthenticatedSession(String sessionId, String username) { - synchronized (userSessionMap) { - UserSessions sessions = userSessionMap.get(username); - if (sessions == null) return; - sessions.getSessionIds().remove(sessionId); - if (sessions.getSessionIds().isEmpty()) { - userSessionMap.remove(username); - } - } - } - - public void logoutAll(SessionManager manager) { - List users = new ArrayList(); - users.addAll(userSessionMap.keySet()); - for (String user : users) logout(manager, user); - } - - public void logout(SessionManager manager, String user) { - log.info("logoutUser: " + user); - UserSessions sessions = null; - synchronized (userSessionMap) { - sessions = userSessionMap.remove(user); - } + UserSessions sessions = userSessionMap.get(username); if (sessions == null) { - log.info("no session for user: " + user); + sessions = new UserSessions(); + sessions.user = username; + userSessionMap.put(username, sessions); + } + sessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId); + sessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId); + keycloakSessionMap.put(keycloakSessionId, sessions); + if (!registered) { + manager.registerSessionListener(this); + registered = true; + } + } + + public synchronized void logoutAll(SessionManager manager) { + for (String user : userSessionMap.keySet()) logoutUser(manager, user); + } + + public synchronized void logoutUser(SessionManager manager, String user) { + log.debug("logoutUser: " + user); + UserSessions sessions = null; + sessions = userSessionMap.remove(user); + if (sessions == null) { + log.debug("no session for user: " + user); return; } - log.info("found session for user"); - for (String id : sessions.getSessionIds()) { + log.debug("found session for user"); + for (Map.Entry entry : sessions.httpSessionToKeycloakSession.entrySet()) { log.debug("invalidating session for user: " + user); - Session session = manager.getSession(id); + String sessionId = entry.getKey(); + String keycloakSessionId = entry.getValue(); + Session session = manager.getSession(sessionId); try { session.invalidate(null); } catch (Exception e) { log.warn("Session already invalidated."); } + keycloakSessionMap.remove(keycloakSessionId); } } + public synchronized void logoutKeycloakSession(SessionManager manager, String keycloakSessionId) { + log.debug("logoutKeycloakSession: " + keycloakSessionId); + UserSessions sessions = keycloakSessionMap.remove(keycloakSessionId); + if (sessions == null) { + log.debug("no session for keycloak session id: " + keycloakSessionId); + return; + } + String sessionId = sessions.keycloakSessionToHttpSession.remove(keycloakSessionId); + if (sessionId == null) { + log.debug("no session for keycloak session id: " + keycloakSessionId); + + } + sessions.httpSessionToKeycloakSession.remove(sessionId); + Session session = manager.getSession(sessionId); + try { + session.invalidate(null); + } catch (Exception e) { + log.warn("Session already invalidated."); + } + if (sessions.keycloakSessionToHttpSession.size() == 0) { + userSessionMap.remove(sessions.user); + } + } + + @Override public void sessionCreated(Session session, HttpServerExchange exchange) { } @@ -141,7 +142,21 @@ public class UndertowUserSessionManagement implements SessionListener { String username = getUsernameFromSession(session); if (username == null) return; String sessionId = session.getId(); - removeAuthenticatedSession(sessionId, username); + UserSessions userSessions = userSessionMap.get(username); + if (userSessions == null) { + return; + } + synchronized (this) { + String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(sessionId); + if (keycloakSessionId != null) { + userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId); + keycloakSessionMap.remove(keycloakSessionId); + } + if (userSessions.httpSessionToKeycloakSession.size() == 0) { + userSessionMap.remove(username); + } + + } } protected String getUsernameFromSession(Session session) { @@ -156,8 +171,21 @@ public class UndertowUserSessionManagement implements SessionListener { public void sessionIdChanged(Session session, String oldSessionId) { String username = getUsernameFromSession(session); if (username == null) return; - removeAuthenticatedSession(oldSessionId, username); - addAuthenticatedSession(session.getId(), username); + String sessionId = session.getId(); + + UserSessions userSessions = userSessionMap.get(username); + if (userSessions == null) { + return; + } + + synchronized (this) { + String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(oldSessionId); + if (keycloakSessionId != null) { + userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId); + userSessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId); + userSessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId); + } + } } @Override diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java index b824db6d3e..d88812d648 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java @@ -1,5 +1,6 @@ package org.keycloak.models; +import java.util.List; import java.util.Set; /** @@ -64,4 +65,7 @@ public interface ClientModel { void setNotBefore(int notBefore); + Set getUserSessions(); + + int getActiveUserSessions(); } diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index c48613be74..68a89eae5e 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -263,4 +263,7 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa void removeExpiredUserSessions(); + ClientModel findClientById(String id); + + void removeUserSessions(); } diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java index e2e16a7afb..ada5899b5b 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java @@ -1,5 +1,8 @@ package org.keycloak.models; +import java.util.List; +import java.util.Set; + /** * @author Stian Thorgersen */ @@ -25,4 +28,9 @@ public interface UserSessionModel { void setLastSessionRefresh(int seconds); + void associateClient(ClientModel client); + + List getClientAssociations(); + + void removeAssociatedClient(ClientModel client); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java index d23a0f45fa..6616c36f64 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java @@ -27,7 +27,7 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode protected ApplicationEntity applicationEntity; public ApplicationAdapter(RealmModel realm, EntityManager em, ApplicationEntity applicationEntity) { - super(realm, applicationEntity); + super(realm, applicationEntity, em); this.realm = realm; this.em = em; this.applicationEntity = applicationEntity; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index 32673443ed..99e5d1bb5b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -2,10 +2,16 @@ package org.keycloak.models.jpa; import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.models.jpa.entities.ClientEntity; -import org.keycloak.models.jpa.entities.OAuthClientEntity; +import org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity; +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.persistence.TypedQuery; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -15,10 +21,12 @@ import java.util.Set; public class ClientAdapter implements ClientModel { protected ClientEntity entity; protected RealmModel realm; + protected EntityManager em; - public ClientAdapter(RealmModel realm, ClientEntity entity) { + public ClientAdapter(RealmModel realm, ClientEntity entity, EntityManager em) { this.realm = realm; this.entity = entity; + this.em = em; } public ClientEntity getEntity() { @@ -141,6 +149,31 @@ public class ClientAdapter implements ClientModel { entity.setNotBefore(notBefore); } + @Override + public int getActiveUserSessions() { + Query query = em.createNamedQuery("getActiveClientSessions"); + query.setParameter("clientId", getId()); + Object count = query.getSingleResult(); + return ((Number)count).intValue(); + } + + @Override + public Set getUserSessions() { + Set list = new HashSet(); + TypedQuery query = em.createNamedQuery("getClientUserSessionByClient", ClientUserSessionAssociationEntity.class); + String id = getId(); + query.setParameter("clientId", id); + List results = query.getResultList(); + for (ClientUserSessionAssociationEntity entity : results) { + list.add(new UserSessionAdapter(em, realm, entity.getSession())); + } + return list; + } + + public void deleteUserSessionAssociation() { + em.createNamedQuery("removeClientUserSessionByClient").setParameter("clientId", getId()).executeUpdate(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java index 56140538fb..36aabd6a15 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java @@ -80,6 +80,7 @@ public class JpaKeycloakSession implements KeycloakSession { } RealmAdapter adapter = new RealmAdapter(em, realm); + adapter.removeUserSessions(); for (ApplicationEntity a : new LinkedList(realm.getApplications())) { adapter.removeApplication(a.getId()); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/OAuthClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/OAuthClientAdapter.java index 48d0e4f8ce..cd78093be5 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/OAuthClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/OAuthClientAdapter.java @@ -5,6 +5,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.jpa.entities.OAuthClientEntity; +import javax.persistence.EntityManager; import java.util.HashSet; import java.util.Set; @@ -14,8 +15,8 @@ import java.util.Set; */ public class OAuthClientAdapter extends ClientAdapter implements OAuthClientModel { - public OAuthClientAdapter(RealmModel realm, OAuthClientEntity entity) { - super(realm, entity); + public OAuthClientAdapter(RealmModel realm, OAuthClientEntity entity, EntityManager em) { + super(realm, entity, em); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 3f420c8a51..95e5ed066e 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -72,6 +72,10 @@ public class RealmAdapter implements RealmModel { this.realm = realm; } + public RealmEntity getEntity() { + return realm; + } + @Override public String getId() { return realm.getId(); @@ -582,6 +586,13 @@ public class RealmAdapter implements RealmModel { return getOAuthClient(clientId); } + @Override + public ClientModel findClientById(String id) { + ClientModel model = getApplicationById(id); + if (model != null) return model; + return getOAuthClientById(id); + } + @Override public Map getApplicationNameMap() { Map map = new HashMap(); @@ -627,6 +638,7 @@ public class RealmAdapter implements RealmModel { ApplicationModel application = getApplicationById(id); if (application == null) return false; + ((ApplicationAdapter)application).deleteUserSessionAssociation(); for (RoleModel role : application.getRoles()) { application.removeRole(role); } @@ -846,13 +858,14 @@ public class RealmAdapter implements RealmModel { data.setRealm(realm); em.persist(data); em.flush(); - return new OAuthClientAdapter(this, data); + return new OAuthClientAdapter(this, data, em); } @Override public boolean removeOAuthClient(String id) { OAuthClientModel oauth = getOAuthClientById(id); if (oauth == null) return false; + ((OAuthClientAdapter)oauth).deleteUserSessionAssociation(); OAuthClientEntity client = (OAuthClientEntity) ((OAuthClientAdapter) oauth).getEntity(); em.createQuery("delete from " + ScopeMappingEntity.class.getSimpleName() + " where client = :client").setParameter("client", client).executeUpdate(); em.remove(client); @@ -867,7 +880,7 @@ public class RealmAdapter implements RealmModel { query.setParameter("realm", realm); List entities = query.getResultList(); if (entities.size() == 0) return null; - return new OAuthClientAdapter(this, entities.get(0)); + return new OAuthClientAdapter(this, entities.get(0), em); } @Override @@ -876,7 +889,7 @@ public class RealmAdapter implements RealmModel { // Check if client belongs to this realm if (client == null || !this.realm.getId().equals(client.getRealm().getId())) return null; - return new OAuthClientAdapter(this, client); + return new OAuthClientAdapter(this, client, em); } @@ -886,7 +899,7 @@ public class RealmAdapter implements RealmModel { query.setParameter("realm", realm); List entities = query.getResultList(); List list = new ArrayList(); - for (OAuthClientEntity entity : entities) list.add(new OAuthClientAdapter(this, entity)); + for (OAuthClientEntity entity : entities) list.add(new OAuthClientAdapter(this, entity, em)); return list; } @@ -1385,6 +1398,7 @@ public class RealmAdapter implements RealmModel { @Override public UserSessionModel createUserSession(UserModel user, String ipAddress) { UserSessionEntity entity = new UserSessionEntity(); + entity.setRealm(realm); entity.setUser(((UserAdapter) user).getUser()); entity.setIpAddress(ipAddress); @@ -1394,20 +1408,20 @@ public class RealmAdapter implements RealmModel { entity.setLastSessionRefresh(currentTime); em.persist(entity); - return new UserSessionAdapter(entity); + return new UserSessionAdapter(em, this, entity); } @Override public UserSessionModel getUserSession(String id) { UserSessionEntity entity = em.find(UserSessionEntity.class, id); - return entity != null ? new UserSessionAdapter(entity) : null; + return entity != null ? new UserSessionAdapter(em, this, entity) : null; } @Override public List getUserSessions(UserModel user) { List sessions = new LinkedList(); for (UserSessionEntity e : em.createNamedQuery("getUserSessionByUser", UserSessionEntity.class).setParameter("user", ((UserAdapter) user).getUser()).getResultList()) { - sessions.add(new UserSessionAdapter(e)); + sessions.add(new UserSessionAdapter(em, this, e)); } return sessions; } @@ -1417,12 +1431,20 @@ public class RealmAdapter implements RealmModel { em.remove(((UserSessionAdapter) session).getEntity()); } + @Override + public void removeUserSessions() { + em.createNamedQuery("removeClientUserSessionByRealm").setParameter("realm", realm).executeUpdate(); + em.createNamedQuery("removeRealmUserSessions").setParameter("realm", realm).executeUpdate(); + + } + @Override public void removeUserSessions(UserModel user) { removeUserSessions(((UserAdapter) user).getUser()); } private void removeUserSessions(UserEntity user) { + em.createNamedQuery("removeClientUserSessionByUser").setParameter("user", user).executeUpdate(); em.createNamedQuery("removeUserSessionByUser").setParameter("user", user).executeUpdate(); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java index 0de48e5067..79e1827dda 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java @@ -1,18 +1,30 @@ package org.keycloak.models.jpa; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserModel; +import org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity; import org.keycloak.models.jpa.entities.UserSessionEntity; +import javax.persistence.EntityManager; +import java.util.ArrayList; +import java.util.List; + /** * @author Stian Thorgersen */ public class UserSessionAdapter implements UserSessionModel { + private RealmModel realm; private UserSessionEntity entity; + private EntityManager em; - public UserSessionAdapter(UserSessionEntity entity) { + public UserSessionAdapter(EntityManager em, RealmModel realm, UserSessionEntity entity) { this.entity = entity; + this.em = em; + this.realm = realm; } public UserSessionEntity getEntity() { @@ -68,4 +80,51 @@ public class UserSessionAdapter implements UserSessionModel { public void setLastSessionRefresh(int seconds) { entity.setLastSessionRefresh(seconds); } + + @Override + public void associateClient(ClientModel client) { + for (ClientUserSessionAssociationEntity ass : entity.getClients()) { + if (ass.getClientId().equals(client.getId())) return; + } + ClientUserSessionAssociationEntity association = new ClientUserSessionAssociationEntity(); + association.setClientId(client.getId()); + association.setSession(entity); + association.setUser(entity.getUser()); + association.setRealm(((RealmAdapter)realm).getEntity()); + em.persist(association); + entity.getClients().add(association); + } + + @Override + public void removeAssociatedClient(ClientModel client) { + em.createNamedQuery("removeClientUserSessionByClient").setParameter("clientId", client.getId()).executeUpdate(); + + } + + @Override + public List getClientAssociations() { + List clients = new ArrayList(); + for (ClientUserSessionAssociationEntity association : entity.getClients()) { + ClientModel client = realm.findClientById(association.getClientId()); + if (client == null) { + throw new ModelException("couldnt find client"); + } + clients.add(client); + } + return clients; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserSessionAdapter that = (UserSessionAdapter) o; + return that.getId().equals(this.getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientUserSessionAssociationEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientUserSessionAssociationEntity.java new file mode 100755 index 0000000000..3c381425a2 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientUserSessionAssociationEntity.java @@ -0,0 +1,83 @@ +package org.keycloak.models.jpa.entities; + +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Entity +@NamedQueries({ + @NamedQuery(name = "getAllClientUserSessions", query = "select s from ClientUserSessionAssociationEntity s"), + @NamedQuery(name = "getClientUserSessionBySession", query = "select s from ClientUserSessionAssociationEntity s where s.session = :session"), + @NamedQuery(name = "getClientUserSessionByClient", query = "select s from ClientUserSessionAssociationEntity s where s.clientId = :clientId"), + @NamedQuery(name = "getActiveClientSessions", query = "select COUNT(s) from ClientUserSessionAssociationEntity s where s.clientId = :clientId"), + @NamedQuery(name = "removeClientUserSessionByClient", query = "delete from ClientUserSessionAssociationEntity s where s.clientId = :clientId"), + @NamedQuery(name = "removeClientUserSessionByUser", query = "delete from ClientUserSessionAssociationEntity s where s.user = :user"), + @NamedQuery(name = "removeClientUserSessionByRealm", query = "delete from ClientUserSessionAssociationEntity s where s.realm = :realm") +}) +public class ClientUserSessionAssociationEntity { + @Id + @GenericGenerator(name="uuid_generator", strategy="org.keycloak.models.jpa.utils.JpaIdGenerator") + @GeneratedValue(generator = "uuid_generator") + private String id; + + @ManyToOne(fetch= FetchType.LAZY) + private UserSessionEntity session; + + @ManyToOne(fetch= FetchType.LAZY) + private UserEntity user; + + @ManyToOne(fetch= FetchType.LAZY) + private RealmEntity realm; + + private String clientId; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public UserEntity getUser() { + return user; + } + + public void setUser(UserEntity user) { + this.user = user; + } + + public UserSessionEntity getSession() { + return session; + } + + public void setSession(UserSessionEntity session) { + this.session = session; + } + + public RealmEntity getRealm() { + return realm; + } + + public void setRealm(RealmEntity realm) { + this.realm = realm; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java index 62769ea2fb..976df2f478 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java @@ -3,12 +3,17 @@ package org.keycloak.models.jpa.entities; import org.hibernate.annotations.GenericGenerator; import org.keycloak.models.UserModel; +import javax.persistence.CascadeType; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import java.util.ArrayList; +import java.util.Collection; /** * @author Stian Thorgersen @@ -16,6 +21,7 @@ import javax.persistence.NamedQuery; @Entity @NamedQueries({ @NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.user = :user"), + @NamedQuery(name = "removeRealmUserSessions", query = "delete from UserSessionEntity s where s.realm = :realm"), @NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.user = :user"), @NamedQuery(name = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.started < :maxTime or s.lastSessionRefresh < :idleTime") }) @@ -29,12 +35,19 @@ public class UserSessionEntity { @ManyToOne private UserEntity user; + @ManyToOne(fetch = FetchType.LAZY) + private RealmEntity realm; + String ipAddress; int started; int lastSessionRefresh; + @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy="session") + Collection clients = new ArrayList(); + + public String getId() { return id; } @@ -74,4 +87,20 @@ public class UserSessionEntity { public void setLastSessionRefresh(int lastSessionRefresh) { this.lastSessionRefresh = lastSessionRefresh; } + + public Collection getClients() { + return clients; + } + + public void setClients(Collection clients) { + this.clients = clients; + } + + public RealmEntity getRealm() { + return realm; + } + + public void setRealm(RealmEntity realm) { + this.realm = realm; + } } diff --git a/model/jpa/src/test/resources/META-INF/persistence.xml b/model/jpa/src/test/resources/META-INF/persistence.xml index 900cb5d4de..c79ddab8d5 100755 --- a/model/jpa/src/test/resources/META-INF/persistence.xml +++ b/model/jpa/src/test/resources/META-INF/persistence.xml @@ -17,6 +17,7 @@ org.keycloak.models.jpa.entities.AuthenticationLinkEntity org.keycloak.models.jpa.entities.UserEntity org.keycloak.models.jpa.entities.UserSessionEntity + org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity org.keycloak.models.jpa.entities.UserRoleMappingEntity org.keycloak.models.jpa.entities.UsernameLoginFailureEntity org.keycloak.models.jpa.entities.ScopeMappingEntity diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java old mode 100644 new mode 100755 index f651f3a744..b7404fde7f --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java @@ -5,11 +5,16 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.models.entities.ClientEntity; import org.keycloak.models.mongo.api.MongoIdentifiableEntity; import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.entities.MongoClientUserSessionAssociationEntity; /** * @author Marek Posolda @@ -164,4 +169,25 @@ public class ClientAdapter extends AbstractMo updateMongoEntity(); } + @Override + public Set getUserSessions() { + DBObject query = new QueryBuilder() + .and("clientId").is(getId()) + .get(); + List associations = getMongoStore().loadEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext); + + Set result = new HashSet(); + for (MongoClientUserSessionAssociationEntity association : associations) { + UserSessionModel session = realm.getUserSession(association.getSessionId()); + result.add(session); + } + return result; + + } + + @Override + public int getActiveUserSessions() { + // todo, something more efficient like COUNT in JPAQL? + return getUserSessions().size(); + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index eb61b5be88..f7e82ec89a 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -661,6 +661,14 @@ public class RealmAdapter extends AbstractMongoAdapter impleme return getOAuthClient(clientId); } + @Override + public ClientModel findClientById(String id) { + ClientModel model = getApplicationById(id); + if (model != null) return model; + return getOAuthClientById(id); + } + + @Override public ApplicationModel getApplicationById(String id) { @@ -1352,6 +1360,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme @Override public UserSessionModel createUserSession(UserModel user, String ipAddress) { MongoUserSessionEntity entity = new MongoUserSessionEntity(); + entity.setRealmId(getId()); entity.setUser(user.getId()); entity.setIpAddress(ipAddress); @@ -1386,7 +1395,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme @Override public void removeUserSession(UserSessionModel session) { - getMongoStore().removeEntity(((UserSessionAdapter) session).getEntity(), invocationContext); + getMongoStore().removeEntity(((UserSessionAdapter) session).getMongoEntity(), invocationContext); } @Override @@ -1395,6 +1404,12 @@ public class RealmAdapter extends AbstractMongoAdapter impleme getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext); } + @Override + public void removeUserSessions() { + DBObject query = new BasicDBObject("realmId", getId()); + getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext); + } + @Override public void removeExpiredUserSessions() { int currentTime = Time.currentTime(); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java index 675e4ba4a0..78dfcb8801 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java @@ -1,26 +1,40 @@ package org.keycloak.models.mongo.keycloak.adapters; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.entities.MongoClientUserSessionAssociationEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + /** * @author Stian Thorgersen */ -public class UserSessionAdapter implements UserSessionModel { +public class UserSessionAdapter extends AbstractMongoAdapter implements UserSessionModel { private MongoUserSessionEntity entity; private RealmAdapter realm; - private MongoStoreInvocationContext invContext; - public UserSessionAdapter(MongoUserSessionEntity entity, RealmAdapter realm, MongoStoreInvocationContext invContext) { + public UserSessionAdapter(MongoUserSessionEntity entity, RealmAdapter realm, MongoStoreInvocationContext invContext) + { + super(invContext); this.entity = entity; this.realm = realm; - this.invContext = invContext; } - public MongoUserSessionEntity getEntity() { + @Override + protected MongoUserSessionEntity getMongoEntity() { return entity; } @@ -74,4 +88,56 @@ public class UserSessionAdapter implements UserSessionModel { entity.setLastSessionRefresh(seconds); } + @Override + public void associateClient(ClientModel client) { + List clients = getClientAssociations(); + for (ClientModel ass : clients) { + if (ass.getId().equals(client.getId())) return; + } + + MongoClientUserSessionAssociationEntity association = new MongoClientUserSessionAssociationEntity(); + association.setClientId(client.getId()); + association.setSessionId(getId()); + + getMongoStore().insertEntity(association, invocationContext); + } + + @Override + public List getClientAssociations() { + DBObject query = new QueryBuilder() + .and("sessionId").is(getId()) + .get(); + List associations = getMongoStore().loadEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext); + + List result = new ArrayList(); + for (MongoClientUserSessionAssociationEntity association : associations) { + ClientModel client = realm.findClientById(association.getClientId()); + result.add(client); + } + return result; + } + + @Override + public void removeAssociatedClient(ClientModel client) { + DBObject query = new QueryBuilder() + .and("sessionId").is(getId()) + .and("clientId").is(client.getId()) + .get(); + getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + UserSessionAdapter that = (UserSessionAdapter) o; + return getId().equals(that.getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoApplicationEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoApplicationEntity.java index c4a8d1a272..bb83e0ee83 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoApplicationEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoApplicationEntity.java @@ -22,5 +22,11 @@ public class MongoApplicationEntity extends ApplicationEntity implements MongoId .and("applicationId").is(getId()) .get(); context.getMongoStore().removeEntities(MongoRoleEntity.class, query, context); + + query = new QueryBuilder() + .and("clientId").is(getId()) + .get(); + context.getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, context); + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientUserSessionAssociationEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientUserSessionAssociationEntity.java new file mode 100755 index 0000000000..d28e561999 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientUserSessionAssociationEntity.java @@ -0,0 +1,37 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import org.keycloak.models.entities.AbstractIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoCollection; +import org.keycloak.models.mongo.api.MongoIdentifiableEntity; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@MongoCollection(collectionName = "session-client-associations") +public class MongoClientUserSessionAssociationEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity { + private String clientId; + private String sessionId; + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + @Override + public void afterRemove(MongoStoreInvocationContext invocationContext) { + } + +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOAuthClientEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOAuthClientEntity.java index f9b69597b0..a499e51fb0 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOAuthClientEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOAuthClientEntity.java @@ -1,5 +1,7 @@ package org.keycloak.models.mongo.keycloak.entities; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; import org.keycloak.models.entities.OAuthClientEntity; import org.keycloak.models.mongo.api.MongoCollection; import org.keycloak.models.mongo.api.MongoIdentifiableEntity; @@ -15,5 +17,9 @@ public class MongoOAuthClientEntity extends OAuthClientEntity implements MongoId @Override public void afterRemove(MongoStoreInvocationContext invocationContext) { + DBObject query = new QueryBuilder() + .and("clientId").is(getId()) + .get(); + invocationContext.getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext); } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java index 1bd2f900ed..d091619f16 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java @@ -32,5 +32,8 @@ public class MongoRealmEntity extends RealmEntity implements MongoIdentifiableEn // Remove all clients of this realm context.getMongoStore().removeEntities(MongoOAuthClientEntity.class, query, context); + + // Remove all sessions of this realm + context.getMongoStore().removeEntities(MongoUserSessionEntity.class, query, context); } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java index 2d98d190b6..238984085d 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java @@ -1,5 +1,7 @@ package org.keycloak.models.mongo.keycloak.entities; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; import org.keycloak.models.entities.AbstractIdentifiableEntity; import org.keycloak.models.mongo.api.MongoCollection; import org.keycloak.models.mongo.api.MongoIdentifiableEntity; @@ -11,6 +13,8 @@ import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; @MongoCollection(collectionName = "sessions") public class MongoUserSessionEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity { + private String realmId; + private String user; private String ipAddress; @@ -19,6 +23,14 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement private int lastSessionRefresh; + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + public String getUser() { return user; } @@ -52,7 +64,12 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement } @Override - public void afterRemove(MongoStoreInvocationContext invocationContext) { + public void afterRemove(MongoStoreInvocationContext context) { + // Remove all roles, which belongs to this application + DBObject query = new QueryBuilder() + .and("sessionId").is(getId()) + .get(); + context.getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, context); } } diff --git a/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml b/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml index 616f8e8c5e..a75390a610 100755 --- a/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml +++ b/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml @@ -16,6 +16,7 @@ org.keycloak.models.jpa.entities.AuthenticationLinkEntity org.keycloak.models.jpa.entities.UserEntity org.keycloak.models.jpa.entities.UserSessionEntity + org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity org.keycloak.models.jpa.entities.UsernameLoginFailureEntity org.keycloak.models.jpa.entities.UserRoleMappingEntity org.keycloak.models.jpa.entities.ScopeMappingEntity diff --git a/server/src/main/resources/META-INF/persistence.xml b/server/src/main/resources/META-INF/persistence.xml index 616f8e8c5e..a75390a610 100755 --- a/server/src/main/resources/META-INF/persistence.xml +++ b/server/src/main/resources/META-INF/persistence.xml @@ -16,6 +16,7 @@ org.keycloak.models.jpa.entities.AuthenticationLinkEntity org.keycloak.models.jpa.entities.UserEntity org.keycloak.models.jpa.entities.UserSessionEntity + org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity org.keycloak.models.jpa.entities.UsernameLoginFailureEntity org.keycloak.models.jpa.entities.UserRoleMappingEntity org.keycloak.models.jpa.entities.ScopeMappingEntity diff --git a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java index 91c7686eb5..c2d4ea667d 100755 --- a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java +++ b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java @@ -5,12 +5,14 @@ import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.ClaimMask; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; +import org.keycloak.models.OAuthClientModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RoleModel; import org.keycloak.models.SocialLinkModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.representations.idm.AuthenticationProviderRepresentation; import org.keycloak.representations.idm.ClaimRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; @@ -19,6 +21,7 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.SocialLinkRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.idm.UserSessionRepresentation; import java.util.ArrayList; import java.util.HashMap; @@ -179,4 +182,20 @@ public class ModelToRepresentation { rep.setSocialUserId(socialLink.getSocialUserId()); return rep; } + + public static UserSessionRepresentation toRepresentation(UserSessionModel session) { + UserSessionRepresentation rep = new UserSessionRepresentation(); + rep.setId(session.getId()); + rep.setStart(((long)session.getStarted()) * 1000); + rep.setUser(session.getUser().getLoginName()); + rep.setIpAddress(session.getIpAddress()); + for (ClientModel client : session.getClientAssociations()) { + if (client instanceof ApplicationModel) { + rep.getApplications().add(client.getClientId()); + } else if (client instanceof OAuthClientModel) { + rep.getClients().put(client.getId(), client.getClientId()); + } + } + return rep; + } } diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index ce06542b48..2498eda523 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -160,6 +160,22 @@ public class ResourceAdminManager { executor.getHttpClient().getConnectionManager().shutdown(); } } + + public void logoutSession(URI requestUri, RealmModel realm, String session) { + ApacheHttpClient4Executor executor = createExecutor(); + + try { + // don't set user notBefore as we don't want a database hit on a user driven logout + List resources = realm.getApplications(); + logger.debugv("logging out {0} resources ", resources.size()); + for (ApplicationModel resource : resources) { + logoutApplication(requestUri, realm, resource, null, session, executor, 0); + } + } finally { + executor.getHttpClient().getConnectionManager().shutdown(); + } + } + public void logoutAll(URI requestUri, RealmModel realm) { ApacheHttpClient4Executor executor = createExecutor(); diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index e9c2c80ab7..d1e51acf26 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -266,6 +266,7 @@ public class TokenService { String scope = form.getFirst(OAuth2Constants.SCOPE); UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr()); + session.associateClient(client); audit.session(session); AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit) @@ -648,6 +649,8 @@ public class TokenService { logger.debug("accessRequest SUCCESS"); + session.associateClient(client); + AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit) .accessToken(accessCode.getToken()) .generateIDToken() 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 d1c37060c2..7927fad668 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 @@ -9,10 +9,12 @@ import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.services.managers.ApplicationManager; import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.RealmManager; @@ -36,7 +38,10 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -228,7 +233,32 @@ public class ApplicationResource { logger.info("activeSessions: " + stats.getActiveSessions()); } return stats; - } + } + + @Path("session-count") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Map getApplicationSessionCount() { + auth.requireView(); + Map map = new HashMap(); + map.put("count", application.getActiveUserSessions()); + return map; + } + + @Path("user-sessions") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public List getUserSessions() { + auth.requireView(); + List sessions = new ArrayList(); + for (UserSessionModel session : application.getUserSessions()) { + UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session); + sessions.add(rep); + } + return sessions; + } @Path("logout-all") @POST diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 862aeb2faf..7a089d8067 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -11,6 +11,7 @@ import org.keycloak.models.ApplicationModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.provider.ProviderSession; import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.idm.RealmAuditRepresentation; @@ -27,6 +28,7 @@ import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; @@ -156,9 +158,34 @@ public class RealmAdminResource { @POST public void logoutAll() { auth.requireManage(); + realm.removeUserSessions(); new ResourceAdminManager().logoutAll(uriInfo.getRequestUri(), realm); } + @Path("sessions/{session}") + @DELETE + public void deleteSession(@PathParam("session") String sessionId) { + UserSessionModel session = realm.getUserSession(sessionId); + if (session == null) throw new NotFoundException("Sesssion not found"); + realm.removeUserSession(session); + new ResourceAdminManager().logoutSession(uriInfo.getRequestUri(), realm, session.getId()); + } + + @Path("application-session-stats") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Map getApplicationSessionStats() { + auth.requireView(); + Map stats = new HashMap(); + for (ApplicationModel applicationModel : realm.getApplications()) { + int size = applicationModel.getActiveUserSessions(); + if (size == 0) continue; + stats.put(applicationModel.getName(), size); + } + return stats; + } + @Path("session-stats") @GET @NoCache diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 1002fc2244..feba6f9859 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -14,6 +14,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.SocialLinkModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.idm.ApplicationMappingsRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; @@ -21,6 +22,7 @@ import org.keycloak.representations.idm.MappingsRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.SocialLinkRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.services.email.EmailException; import org.keycloak.services.email.EmailSender; import org.keycloak.services.managers.AccessCodeEntry; @@ -181,6 +183,26 @@ public class UsersResource { return stats; } + @Path("{username}/sessions") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public List getSessions(final @PathParam("username") String username) { + logger.info("sessions"); + auth.requireView(); + UserModel user = realm.getUser(username); + if (user == null) { + throw new NotFoundException("User not found"); + } + List sessions = realm.getUserSessions(user); + List reps = new ArrayList(); + for (UserSessionModel session : sessions) { + UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session); + reps.add(rep); + } + return reps; + } + @Path("{username}/social-links") @GET @NoCache @@ -208,6 +230,7 @@ public class UsersResource { if (user == null) { throw new NotFoundException("User not found"); } + realm.removeUserSessions(user); // set notBefore so that user will be forced to log in. user.setNotBefore(Time.currentTime()); new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), null); diff --git a/testsuite/integration/src/main/resources/META-INF/persistence.xml b/testsuite/integration/src/main/resources/META-INF/persistence.xml index a5a78494ae..6e68551340 100755 --- a/testsuite/integration/src/main/resources/META-INF/persistence.xml +++ b/testsuite/integration/src/main/resources/META-INF/persistence.xml @@ -16,6 +16,7 @@ org.keycloak.models.jpa.entities.SocialLinkEntity org.keycloak.models.jpa.entities.AuthenticationLinkEntity org.keycloak.models.jpa.entities.UserEntity + org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity org.keycloak.models.jpa.entities.UserSessionEntity org.keycloak.models.jpa.entities.UsernameLoginFailureEntity org.keycloak.models.jpa.entities.UserRoleMappingEntity diff --git a/testsuite/performance/src/test/resources/META-INF/persistence.xml b/testsuite/performance/src/test/resources/META-INF/persistence.xml index e5ae89a0db..13695fa189 100755 --- a/testsuite/performance/src/test/resources/META-INF/persistence.xml +++ b/testsuite/performance/src/test/resources/META-INF/persistence.xml @@ -17,6 +17,7 @@ org.keycloak.models.jpa.entities.AuthenticationLinkEntity org.keycloak.models.jpa.entities.UserEntity org.keycloak.models.jpa.entities.UserSessionEntity + org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity org.keycloak.models.jpa.entities.UsernameLoginFailureEntity org.keycloak.models.jpa.entities.UserRoleMappingEntity org.keycloak.models.jpa.entities.ScopeMappingEntity