diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js index 355091b171..1397eb1951 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js @@ -147,14 +147,21 @@ module.factory('ServerInfo', function($resource) { }); module.factory('User', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/users/:userId', { - realm : '@realm', - userId : '@userId' - }, { + return $resource('/auth/rest/admin/realms/:realm/users/:userId', { + realm : '@realm', + userId : '@userId' + }, { update : { method : 'PUT' } - }); + }); +}); + +module.factory('UserSessionStats', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/users/:userId/session-stats', { + realm : '@realm', + userId : '@userId' + }); }); module.factory('UserCredentials', function($resource) { @@ -241,6 +248,13 @@ module.factory('RealmPushRevocation', function($resource) { }); }); +module.factory('RealmSessionStats', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/session-stats', { + realm : '@realm' + }); +}); + + module.factory('RoleApplicationComposites', function($resource) { return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role/composites/applications/:application', { realm : '@realm', @@ -456,6 +470,7 @@ module.factory('ApplicationRole', function($resource) { } }); }); + module.factory('ApplicationClaims', function($resource) { return $resource('/auth/rest/admin/realms/:realm/applications/:application/claims', { realm : '@realm', @@ -467,6 +482,13 @@ module.factory('ApplicationClaims', function($resource) { }); }); +module.factory('ApplicationSessionStats', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/applications/:application/session-stats', { + realm : '@realm', + application : "@application" + }); +}); + module.factory('ApplicationPushRevocation', function($resource) { return $resource('/auth/rest/admin/realms/:realm/applications/:application/push-revocation', { realm : '@realm', diff --git a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java index 93ddfa70d2..b03f09eda7 100755 --- a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java +++ b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java @@ -9,6 +9,8 @@ public interface AdapterConstants { // URL endpoints public static final String K_LOGOUT = "k_logout"; public static final String K_PUSH_NOT_BEFORE = "k_push_not_before"; + public static final String K_GET_USER_STATS = "k_get_user_stats"; + public static final String K_GET_SESSION_STATS = "k_get_session_stats"; public static final String K_QUERY_BEARER_TOKEN = "k_query_bearer_token"; // This param name is defined again in Keycloak Subsystem class diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/AdminAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/AdminAction.java index c372fdec49..4340114fb9 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/action/AdminAction.java +++ b/core/src/main/java/org/keycloak/representations/adapters/action/AdminAction.java @@ -8,18 +8,20 @@ import org.codehaus.jackson.annotate.JsonIgnore; * @author Bill Burke * @version $Revision: 1 $ */ -public class AdminAction { +public abstract class AdminAction { protected String id; protected int expiration; protected String resource; + protected String action; public AdminAction() { } - public AdminAction(String id, int expiration, String resource) { + public AdminAction(String id, int expiration, String resource, String action) { this.id = id; this.expiration = expiration; this.resource = resource; + this.action = action; } public String getId() { @@ -56,4 +58,6 @@ public class AdminAction { public void setResource(String resource) { this.resource = resource; } + + public abstract boolean validate(); } diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java index 69f5872c91..ef94ebed86 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java +++ b/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java @@ -5,13 +5,14 @@ package org.keycloak.representations.adapters.action; * @version $Revision: 1 $ */ public class LogoutAction extends AdminAction { + public static final String LOGOUT = "LOGOUT"; protected String user; public LogoutAction() { } public LogoutAction(String id, int expiration, String resource, String user) { - super(id, expiration, resource); + super(id, expiration, resource, LOGOUT); this.user = user; } @@ -22,4 +23,9 @@ public class LogoutAction extends AdminAction { public void setUser(String user) { this.user = user; } + + @Override + public boolean validate() { + return LOGOUT.equals(action); + } } diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/PushNotBeforeAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/PushNotBeforeAction.java index b3ff1c4ea9..d11a624a84 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/action/PushNotBeforeAction.java +++ b/core/src/main/java/org/keycloak/representations/adapters/action/PushNotBeforeAction.java @@ -6,13 +6,14 @@ package org.keycloak.representations.adapters.action; */ public class PushNotBeforeAction extends AdminAction { + public static final String PUSH_NOT_BEFORE = "PUSH_NOT_BEFORE"; protected int notBefore; public PushNotBeforeAction() { } public PushNotBeforeAction(String id, int expiration, String resource, int notBefore) { - super(id, expiration, resource); + super(id, expiration, resource, PUSH_NOT_BEFORE); this.notBefore = notBefore; } @@ -23,4 +24,10 @@ public class PushNotBeforeAction extends AdminAction { public void setNotBefore(int notBefore) { this.notBefore = notBefore; } + + @Override + public boolean validate() { + return PUSH_NOT_BEFORE.equals(action); + } + } diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/SessionStats.java b/core/src/main/java/org/keycloak/representations/adapters/action/SessionStats.java new file mode 100755 index 0000000000..b272f7289b --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/adapters/action/SessionStats.java @@ -0,0 +1,38 @@ +package org.keycloak.representations.adapters.action; + +import java.util.List; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SessionStats { + protected int activeSessions; + protected int activeUsers; + protected Set users; + + public int getActiveSessions() { + return activeSessions; + } + + public void setActiveSessions(int activeSessions) { + this.activeSessions = activeSessions; + } + + public int getActiveUsers() { + return activeUsers; + } + + public void setActiveUsers(int activeUsers) { + this.activeUsers = activeUsers; + } + + public Set getUsers() { + return users; + } + + public void setUsers(Set users) { + this.users = users; + } +} diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/SessionStatsAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/SessionStatsAction.java new file mode 100755 index 0000000000..1bcb1fafc4 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/adapters/action/SessionStatsAction.java @@ -0,0 +1,35 @@ +package org.keycloak.representations.adapters.action; + +/** + * Query session stats. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SessionStatsAction extends AdminAction { + + public static final String SESSION_STATS = "SESSION_STATS"; + + protected boolean listUsers; + + public SessionStatsAction() { + } + + public SessionStatsAction(String id, int expiration, String resource) { + super(id, expiration, resource, SESSION_STATS); + } + + public boolean isListUsers() { + return listUsers; + } + + public void setListUsers(boolean listUsers) { + this.listUsers = listUsers; + } + + @Override + public boolean validate() { + return SESSION_STATS.equals(action); + } + +} diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/UserStats.java b/core/src/main/java/org/keycloak/representations/adapters/action/UserStats.java new file mode 100755 index 0000000000..2eb9778d67 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/adapters/action/UserStats.java @@ -0,0 +1,26 @@ +package org.keycloak.representations.adapters.action; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserStats { + protected boolean loggedIn; + protected long whenLoggedIn; + + public boolean isLoggedIn() { + return loggedIn; + } + + public void setLoggedIn(boolean loggedIn) { + this.loggedIn = loggedIn; + } + + public long getWhenLoggedIn() { + return whenLoggedIn; + } + + public void setWhenLoggedIn(long whenLoggedIn) { + this.whenLoggedIn = whenLoggedIn; + } +} diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/UserStatsAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/UserStatsAction.java new file mode 100755 index 0000000000..c7e5b63fc4 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/adapters/action/UserStatsAction.java @@ -0,0 +1,31 @@ +package org.keycloak.representations.adapters.action; + +/** + * Query session stats. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserStatsAction extends AdminAction { + + public static final String USER_STATS = "USER_STATS"; + protected String user; + + public UserStatsAction() { + } + + public UserStatsAction(String id, int expiration, String resource, String user) { + super(id, expiration, resource, USER_STATS); + this.user = user; + } + + public String getUser() { + return user; + } + + @Override + public boolean validate() { + return USER_STATS.equals(action); + } + +} diff --git a/core/src/main/java/org/keycloak/util/JsonSerialization.java b/core/src/main/java/org/keycloak/util/JsonSerialization.java index f69c971416..9e1633b486 100755 --- a/core/src/main/java/org/keycloak/util/JsonSerialization.java +++ b/core/src/main/java/org/keycloak/util/JsonSerialization.java @@ -6,6 +6,7 @@ import org.codehaus.jackson.map.annotate.JsonSerialize; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; /** * Utility class to handle simple JSON serializable for Keycloak. @@ -25,6 +26,11 @@ public class JsonSerialization { prettyMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL); } + public static void writeValueToStream(OutputStream os, Object obj) throws IOException { + mapper.writeValue(os, obj); + + } + public static String writeValueAsString(Object obj) throws IOException { return mapper.writeValueAsString(obj); } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSession.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSession.java index 32d4917bc0..77c6fef6b0 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSession.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSession.java @@ -52,6 +52,7 @@ public class RefreshableKeycloakSession extends KeycloakSecurityContext { } public void refreshExpiredToken() { + log.info("checking whether to refresh."); if (isActive()) return; if (this.realmConfiguration == null || refreshToken == null) return; // Might be serialized in HttpSession? @@ -75,6 +76,10 @@ public class RefreshableKeycloakSession extends KeycloakSecurityContext { } catch (VerificationException e) { log.error("failed verification of token"); } + if (response.getNotBeforePolicy() > realmConfiguration.getNotBefore()) { + realmConfiguration.setNotBefore(response.getNotBeforePolicy()); + } + this.token = token; this.refreshToken = response.getRefreshToken(); this.tokenString = tokenString; diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java index 4787a114ab..c9470db179 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java @@ -156,7 +156,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif protected void pushNotBefore(JWSInput token, HttpServletResponse response) throws IOException { try { - log.debug("->> pushNotBefore: "); + log.info("->> pushNotBefore: "); PushNotBeforeAction action = JsonSerialization.readValue(token.getContent(), PushNotBeforeAction.class); if (action.isExpired()) { log.warn("admin request failed, expired token"); diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletAdminActionsHandler.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletAdminActionsHandler.java index 1b7db55afa..64ea788c69 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletAdminActionsHandler.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletAdminActionsHandler.java @@ -12,12 +12,18 @@ import org.keycloak.adapters.ResourceMetadata; import org.keycloak.adapters.config.RealmConfiguration; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.representations.adapters.action.AdminAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction; +import org.keycloak.representations.adapters.action.SessionStats; +import org.keycloak.representations.adapters.action.SessionStatsAction; +import org.keycloak.representations.adapters.action.UserStats; +import org.keycloak.representations.adapters.action.UserStatsAction; import org.keycloak.util.JsonSerialization; import org.keycloak.util.StreamUtil; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; /** * @author Bill Burke @@ -101,6 +107,12 @@ public class ServletAdminActionsHandler implements HttpHandler { } else if (requestUri.endsWith(AdapterConstants.K_PUSH_NOT_BEFORE)) { handlePushNotBefore(request, response); return; + } else if (requestUri.endsWith(AdapterConstants.K_GET_SESSION_STATS)) { + handleGetSessionStats(request, response); + return; + }else if (requestUri.endsWith(AdapterConstants.K_GET_USER_STATS)) { + handleGetUserStats(request, response); + return; } else { next.handleRequest(exchange); return; @@ -110,20 +122,65 @@ public class ServletAdminActionsHandler implements HttpHandler { protected void handlePushNotBefore(HttpServletRequest request, HttpServletResponse response) throws Exception { log.info("K_PUSH_NOT_BEFORE sent"); JWSInput token = verifyAdminRequest(request, response); - if (token == null) return; + if (token == null) { + return; + } PushNotBeforeAction action = JsonSerialization.readValue(token.getContent(), PushNotBeforeAction.class); + if (!validateAction(response, action)) return; + realmConfig.setNotBefore(action.getNotBefore()); + return; + } + + protected boolean validateAction(HttpServletResponse response, AdminAction action) throws IOException { + if (!action.validate()) { + log.warn("admin request failed, not validated"); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Not validated"); + return false; + } if (action.isExpired()) { log.warn("admin request failed, expired token"); response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Expired token"); - return; + return false; } if (!resourceMetadata.getResourceName().equals(action.getResource())) { log.warn("Resource name does not match"); response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Resource name does not match"); - return; + return false; } - realmConfig.setNotBefore(action.getNotBefore()); + return true; + } + + protected void handleGetSessionStats(HttpServletRequest request, HttpServletResponse response) throws Exception { + log.info("K_GET_SESSION_STATS sent"); + JWSInput token = verifyAdminRequest(request, response); + if (token == null) return; + SessionStatsAction action = JsonSerialization.readValue(token.getContent(), SessionStatsAction.class); + if (!validateAction(response, action)) return; + SessionStats stats = new SessionStats(); + stats.setActiveSessions(userSessionManagement.getActiveSessions()); + stats.setActiveUsers(userSessionManagement.getActiveUsers().size()); + if (action.isListUsers()) stats.setUsers(userSessionManagement.getActiveUsers()); + response.setStatus(200); + JsonSerialization.writeValueToStream(response.getOutputStream(), stats); + return; + } + protected void handleGetUserStats(HttpServletRequest request, HttpServletResponse response) throws Exception { + log.info("K_GET_USER_STATS sent"); + JWSInput token = verifyAdminRequest(request, response); + if (token == null) return; + UserStatsAction action = JsonSerialization.readValue(token.getContent(), UserStatsAction.class); + if (!validateAction(response, action)) return; + UserStats stats = new UserStats(); + Long loginTime = userSessionManagement.getUserLoginTime(action.getUser()); + if (loginTime != null) { + stats.setLoggedIn(true); + stats.setWhenLoggedIn(loginTime); + } else { + stats.setLoggedIn(false); + } + response.setStatus(200); + JsonSerialization.writeValueToStream(response.getOutputStream(), stats); return; } } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UserSessionManagement.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UserSessionManagement.java index 4cbd1d1f71..1ed9133936 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UserSessionManagement.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UserSessionManagement.java @@ -31,7 +31,7 @@ import java.util.concurrent.ConcurrentHashMap; public class UserSessionManagement implements SessionListener { private static final Logger log = Logger.getLogger(UserSessionManagement.class); private static final String AUTH_SESSION_NAME = CachedAuthenticatedSessionHandler.class.getName() + ".AuthenticatedSession"; - protected ConcurrentHashMap> userSessionMap = new ConcurrentHashMap>(); + protected ConcurrentHashMap userSessionMap = new ConcurrentHashMap(); protected RealmConfiguration realmInfo; @@ -39,6 +39,51 @@ public class UserSessionManagement implements SessionListener { this.realmInfo = realmInfo; } + public static class UserSessions { + protected Set sessionIds = new HashSet(); + protected long loggedIn = System.currentTimeMillis(); + + public Set getSessionIds() { + return sessionIds; + } + + public long getLoggedIn() { + return loggedIn; + } + } + + public int getNumUserLogins() { + return userSessionMap.size(); + } + + public int getActiveSessions() { + int active = 0; + synchronized (userSessionMap) { + for (UserSessions sessions : userSessionMap.values()) { + active += sessions.getSessionIds().size(); + } + + } + return active; + } + + /** + * + * @param username + * @return null if user not logged in + */ + public Long getUserLoginTime(String username) { + UserSessions sessions = userSessionMap.get(username); + if (sessions == null) return null; + return sessions.getLoggedIn(); + } + + public Set getActiveUsers() { + HashSet set = new HashSet(); + set.addAll(userSessionMap.keySet()); + return set; + } + public void remoteLogout(JWSInput token, SessionManager manager, HttpServletResponse response) throws IOException { try { log.info("->> remoteLogout: "); @@ -77,28 +122,22 @@ public class UserSessionManagement implements SessionListener { protected void addAuthenticatedSession(String username, String sessionId) { synchronized (userSessionMap) { - Set map = userSessionMap.get(username); - if (map == null) { - final Set value = new HashSet(); - map = userSessionMap.putIfAbsent(username, value); - if (map == null) { - map = value; - } + UserSessions sessions = userSessionMap.get(username); + if (sessions == null) { + UserSessions session = new UserSessions(); + userSessionMap.put(username, session); } - synchronized (map) { - map.add(sessionId); - } - + sessions.getSessionIds().add(sessionId); } } protected void removeAuthenticatedSession(String sessionId, String username) { synchronized (userSessionMap) { - Set map = userSessionMap.get(username); - if (map == null) return; - synchronized (map) { - map.remove(sessionId); - if (map.isEmpty()) userSessionMap.remove(username); + UserSessions sessions = userSessionMap.get(username); + if (sessions == null) return; + sessions.getSessionIds().remove(sessionId); + if (sessions.getSessionIds().isEmpty()) { + userSessionMap.remove(username); } } } @@ -119,24 +158,24 @@ public class UserSessionManagement implements SessionListener { public void logout(SessionManager manager, String user) { log.info("logoutUser: " + user); - Set map = userSessionMap.remove(user); - if (map == null) { + UserSessions sessions = null; + synchronized (userSessionMap) { + sessions = userSessionMap.remove(user); + } + if (sessions == null) { log.info("no session for user: " + user); return; } log.info("found session for user"); - synchronized (map) { - for (String id : map) { - log.debug("invalidating session for user: " + user); - Session session = manager.getSession(id); - try { - session.invalidate(null); - } catch (Exception e) { - log.warn("Session already invalidated."); - } + for (String id : sessions.getSessionIds()) { + log.debug("invalidating session for user: " + user); + Session session = manager.getSession(id); + try { + session.invalidate(null); + } catch (Exception e) { + log.warn("Session already invalidated."); } } - } @Override 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 d084758673..f0dafeaf1e 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -7,8 +7,13 @@ import org.keycloak.TokenIdGenerator; import org.keycloak.adapters.AdapterConstants; import org.keycloak.models.ApplicationModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import org.keycloak.representations.adapters.action.LogoutAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction; +import org.keycloak.representations.adapters.action.SessionStats; +import org.keycloak.representations.adapters.action.SessionStatsAction; +import org.keycloak.representations.adapters.action.UserStats; +import org.keycloak.representations.adapters.action.UserStatsAction; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Response; @@ -21,6 +26,74 @@ import java.util.List; public class ResourceAdminManager { protected static Logger logger = Logger.getLogger(ResourceAdminManager.class); + public SessionStats getSessionStats(RealmModel realm, ApplicationModel application, boolean users) { + ResteasyClient client = new ResteasyClientBuilder() + .disableTrustManager() // todo fix this, should have a trust manager or a good default + .build(); + + try { + return getSessionStats(realm, application, users, client); + } finally { + client.close(); + } + + } + + public SessionStats getSessionStats(RealmModel realm, ApplicationModel application, boolean users, ResteasyClient client) { + String managementUrl = application.getManagementUrl(); + if (managementUrl != null) { + SessionStatsAction adminAction = new SessionStatsAction(TokenIdGenerator.generateId(), (int)(System.currentTimeMillis() / 1000) + 30, application.getName()); + adminAction.setListUsers(users); + String token = new TokenManager().encodeToken(realm, adminAction); + logger.info("session stats for application: {0} url: {1}", application.getName(), managementUrl); + Response response = client.target(managementUrl).path(AdapterConstants.K_GET_SESSION_STATS).request().post(Entity.text(token)); + if (response.getStatus() != 200) { + logger.warn("Failed to get stats: " + response.getStatus()); + return null; + } + SessionStats stats = response.readEntity(SessionStats.class); + return stats; + } else { + logger.info("no management url."); + return null; + } + + } + + public UserStats getUserStats(RealmModel realm, ApplicationModel application, UserModel user) { + ResteasyClient client = new ResteasyClientBuilder() + .disableTrustManager() // todo fix this, should have a trust manager or a good default + .build(); + + try { + return getUserStats(realm, application, user, client); + } finally { + client.close(); + } + + } + + + public UserStats getUserStats(RealmModel realm, ApplicationModel application, UserModel user, ResteasyClient client) { + String managementUrl = application.getManagementUrl(); + if (managementUrl != null) { + UserStatsAction adminAction = new UserStatsAction(TokenIdGenerator.generateId(), (int)(System.currentTimeMillis() / 1000) + 30, application.getName(), user.getId()); + String token = new TokenManager().encodeToken(realm, adminAction); + logger.info("session stats for application: {0} url: {1}", application.getName(), managementUrl); + Response response = client.target(managementUrl).path(AdapterConstants.K_GET_USER_STATS).request().post(Entity.text(token)); + if (response.getStatus() != 200) { + logger.warn("Failed to get stats: " + response.getStatus()); + return null; + } + UserStats stats = response.readEntity(UserStats.class); + return stats; + } else { + logger.info("no management url."); + return null; + } + + } + public void singleLogOut(RealmModel realm, String user) { ResteasyClient client = new ResteasyClientBuilder() .disableTrustManager() // todo fix this, should have a trust manager or a good default diff --git a/services/src/main/java/org/keycloak/services/managers/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java index 44689c2c3a..b617bb9bd2 100755 --- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java +++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java @@ -366,18 +366,20 @@ public class TokenManager { return encodedToken; } - public AccessTokenResponseBuilder responseBuilder(RealmModel realm) { - return new AccessTokenResponseBuilder(realm); + public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client) { + return new AccessTokenResponseBuilder(realm, client); } public class AccessTokenResponseBuilder { RealmModel realm; + ClientModel client; AccessToken accessToken; RefreshToken refreshToken; IDToken idToken; - public AccessTokenResponseBuilder(RealmModel realm) { + public AccessTokenResponseBuilder(RealmModel realm, ClientModel client) { this.realm = realm; + this.client = client; } public AccessTokenResponseBuilder accessToken(AccessToken accessToken) { @@ -465,7 +467,9 @@ public class TokenManager { String encodedToken = new JWSBuilder().jsonContent(refreshToken).rsa256(realm.getPrivateKey()); res.setRefreshToken(encodedToken); } - res.setNotBeforePolicy(realm.getNotBefore()); + int notBefore = realm.getNotBefore(); + if (client.getNotBefore() > notBefore) notBefore = client.getNotBefore(); + res.setNotBeforePolicy(notBefore); return res; } } 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 16db7d0310..7b45b182c3 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -160,7 +160,7 @@ public class TokenService { throw new NotAuthorizedException("Auth failed"); } String scope = form.getFirst("scope"); - AccessTokenResponse res = tokenManager.responseBuilder(realm) + AccessTokenResponse res = tokenManager.responseBuilder(realm, client) .generateAccessToken(scope, client, user) .generateIDToken() .build(); @@ -190,7 +190,7 @@ public class TokenService { throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(), e); } - AccessTokenResponse res = tokenManager.responseBuilder(realm) + AccessTokenResponse res = tokenManager.responseBuilder(realm, client) .accessToken(accessToken) .generateIDToken() .generateRefreshToken().build(); @@ -413,7 +413,7 @@ public class TokenService { .build(); } logger.debug("accessRequest SUCCESS"); - AccessTokenResponse res = tokenManager.responseBuilder(realm) + AccessTokenResponse res = tokenManager.responseBuilder(realm, client) .accessToken(accessCode.getToken()) .generateIDToken() .generateRefreshToken().build(); 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 db4dd126a9..d9c8122970 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 @@ -6,6 +6,7 @@ import org.keycloak.models.ApplicationModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; +import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.ApplicationManager; @@ -28,6 +29,8 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.Set; /** @@ -193,6 +196,15 @@ public class ApplicationResource { new ResourceAdminManager().pushApplicationRevocationPolicy(realm, application); } + @Path("session-stats") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public SessionStats getSessionStats() { + return new ResourceAdminManager().getSessionStats(realm, application, true); + } + + 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 0a8996903b..8c5674980d 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 @@ -2,8 +2,10 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.logging.Logger; +import org.keycloak.models.ApplicationModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.RealmManager; @@ -13,6 +15,10 @@ import org.keycloak.services.managers.TokenManager; import javax.ws.rs.*; import javax.ws.rs.container.ResourceContext; import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * @author Bill Burke @@ -112,4 +118,19 @@ public class RealmAdminResource { new ResourceAdminManager().pushRealmRevocationPolicy(realm); } + @Path("session-stats") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Map getSessionStats() { + + Map stats = new HashMap(); + for (ApplicationModel applicationModel : realm.getApplications()) { + if (applicationModel.getManagementUrl() == null) continue; + SessionStats appStats = new ResourceAdminManager().getSessionStats(realm, applicationModel, false); + stats.put(applicationModel.getName(), appStats); + } + return stats; + } + } 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 29b73748cc..605f7394eb 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 @@ -10,6 +10,8 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; +import org.keycloak.representations.adapters.action.SessionStats; +import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.idm.*; import org.keycloak.services.email.EmailException; import org.keycloak.services.email.EmailSender; @@ -17,6 +19,7 @@ import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Urls; @@ -34,6 +37,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.container.ResourceContext; import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.util.ArrayList; @@ -138,12 +142,32 @@ public class UsersResource { auth.requireView(); UserModel user = realm.getUser(username); - if (user == null || !isUser(user)) { + if (user == null) { throw new NotFoundException(); } return ModelToRepresentation.toRepresentation(user); } + @Path("{username}/session-stats") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Map getSessionStats(final @PathParam("username") String username) { + auth.requireView(); + UserModel user = realm.getUser(username); + if (user == null) { + throw new NotFoundException(); + } + Map stats = new HashMap(); + for (ApplicationModel applicationModel : realm.getApplications()) { + if (applicationModel.getManagementUrl() == null) continue; + UserStats appStats = new ResourceAdminManager().getUserStats(realm, applicationModel, user); + stats.put(applicationModel.getName(), appStats); + } + return stats; + } + + @Path("{username}") @DELETE @NoCache @@ -191,17 +215,11 @@ public class UsersResource { } for (UserModel user : userModels) { - if (isUser(user)) { - results.add(ModelToRepresentation.toRepresentation(user)); - } + results.add(ModelToRepresentation.toRepresentation(user)); } return results; } - private boolean isUser(UserModel user) { - return true; - } - @Path("{username}/role-mappings") @GET @Produces("application/json")