From 2d86b29b6c1e832fc23e09b8c6898450b1228d4f Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 4 Mar 2014 15:52:27 -0500 Subject: [PATCH 1/2] session stats --- .../META-INF/resources/admin/js/services.js | 32 +++++- .../keycloak/adapters/AdapterConstants.java | 2 + .../adapters/action/AdminAction.java | 8 +- .../adapters/action/LogoutAction.java | 8 +- .../adapters/action/PushNotBeforeAction.java | 9 +- .../adapters/action/SessionStats.java | 38 ++++++++ .../adapters/action/SessionStatsAction.java | 35 +++++++ .../adapters/action/UserStats.java | 26 +++++ .../adapters/action/UserStatsAction.java | 31 ++++++ .../org/keycloak/util/JsonSerialization.java | 6 ++ .../adapters/RefreshableKeycloakSession.java | 5 + .../as7/KeycloakAuthenticatorValve.java | 2 +- .../undertow/ServletAdminActionsHandler.java | 65 ++++++++++++- .../undertow/UserSessionManagement.java | 97 +++++++++++++------ .../managers/ResourceAdminManager.java | 73 ++++++++++++++ .../services/managers/TokenManager.java | 12 ++- .../services/resources/TokenService.java | 6 +- .../resources/admin/ApplicationResource.java | 12 +++ .../resources/admin/RealmAdminResource.java | 21 ++++ .../resources/admin/UsersResource.java | 34 +++++-- 20 files changed, 464 insertions(+), 58 deletions(-) create mode 100755 core/src/main/java/org/keycloak/representations/adapters/action/SessionStats.java create mode 100755 core/src/main/java/org/keycloak/representations/adapters/action/SessionStatsAction.java create mode 100755 core/src/main/java/org/keycloak/representations/adapters/action/UserStats.java create mode 100755 core/src/main/java/org/keycloak/representations/adapters/action/UserStatsAction.java 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") From d6bd02ea7d52cbb1c55073b72926c06c6214e407 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 4 Mar 2014 22:25:33 -0500 Subject: [PATCH 2/2] session mgmt --- .../META-INF/resources/admin/js/app.js | 42 +++++++++++++ .../admin/js/controllers/applications.js | 39 ++++++++++++ .../resources/admin/js/controllers/realm.js | 20 +++++++ .../resources/admin/js/controllers/users.js | 29 +++++++++ .../META-INF/resources/admin/js/loaders.js | 48 +++++++++++---- .../META-INF/resources/admin/js/services.js | 36 ++++++++++- .../admin/partials/application-claims.html | 1 + .../partials/application-credentials.html | 1 + .../admin/partials/application-detail.html | 1 + .../partials/application-installation.html | 1 + .../partials/application-revocation.html | 1 + .../partials/application-role-detail.html | 1 + .../admin/partials/application-role-list.html | 1 + .../partials/application-scope-mappings.html | 1 + .../admin/partials/application-sessions.html | 60 +++++++++++++++++++ .../admin/partials/role-mappings.html | 1 + .../admin/partials/session-realm.html | 37 ++++++++++++ .../admin/partials/session-revocation.html | 1 + .../admin/partials/user-credentials.html | 1 + .../resources/admin/partials/user-detail.html | 1 + .../admin/partials/user-sessions.html | 41 +++++++++++++ .../keycloak/SkeletonKeyContextResolver.java | 4 +- .../adapters/action/AdminAction.java | 8 +++ .../adapters/action/SessionStats.java | 7 ++- .../undertow/ServletAdminActionsHandler.java | 30 ++++++++-- .../undertow/UserSessionManagement.java | 4 +- .../managers/ResourceAdminManager.java | 51 ++++++++++++++-- .../services/resources/TokenService.java | 7 +-- .../resources/admin/ApplicationResource.java | 46 +++++++++++++- .../resources/admin/RealmAdminResource.java | 10 +++- .../resources/admin/UsersResource.java | 15 ++++- 31 files changed, 505 insertions(+), 41 deletions(-) create mode 100755 admin-ui/src/main/resources/META-INF/resources/admin/partials/application-sessions.html create mode 100755 admin-ui/src/main/resources/META-INF/resources/admin/partials/session-realm.html create mode 100755 admin-ui/src/main/resources/META-INF/resources/admin/partials/user-sessions.html diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js index bfb76f7e70..bce292ed87 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js @@ -176,6 +176,21 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'UserRoleMappingCtrl' }) + .when('/realms/:realm/users/:user/sessions', { + templateUrl : 'partials/user-sessions.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + user : function(UserLoader) { + return UserLoader(); + }, + stats : function(UserSessionStatsLoader) { + return UserSessionStatsLoader(); + } + }, + controller : 'UserSessionsCtrl' + }) .when('/realms/:realm/users', { templateUrl : 'partials/user-list.html', resolve : { @@ -292,6 +307,21 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ApplicationClaimsCtrl' }) + .when('/realms/:realm/applications/:application/sessions', { + templateUrl : 'partials/application-sessions.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + application : function(ApplicationLoader) { + return ApplicationLoader(); + }, + stats : function(ApplicationSessionStatsLoader) { + return ApplicationSessionStatsLoader(); + } + }, + controller : 'ApplicationSessionsCtrl' + }) .when('/realms/:realm/applications/:application/credentials', { templateUrl : 'partials/application-credentials.html', resolve : { @@ -540,6 +570,18 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'RealmRevocationCtrl' }) + .when('/realms/:realm/sessions/realm', { + templateUrl : 'partials/session-realm.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + stats : function(RealmSessionStatsLoader) { + return RealmSessionStatsLoader(); + } + }, + controller : 'RealmSessionStatsCtrl' + }) .otherwise({ templateUrl : 'partials/notfound.html' diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/applications.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/applications.js index 27607e960c..d7416a07e3 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/applications.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/applications.js @@ -43,6 +43,45 @@ module.controller('ApplicationCredentialsCtrl', function($scope, $location, real }); }); +module.controller('ApplicationSessionsCtrl', function($scope, realm, stats, application, + ApplicationLogoutUser, + ApplicationLogoutAll, + ApplicationSessionStats, + ApplicationSessionStatsWithUsers, + $location, Dialog, Notifications) { + $scope.realm = realm; + $scope.stats = stats; + $scope.users = {}; + $scope.application = application; + + $scope.toDate = function(val) { + return new Date(val); + }; + + $scope.loadUsers = function() { + ApplicationSessionStatsWithUsers.get({ realm : realm.realm, application: $scope.application.name }, function(updated) { + $scope.stats = updated; + $scope.users = updated.users; + }) + }; + + $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, ApplicationClaims, $http, $location, Dialog, Notifications) { diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js index cb0fa24800..d2e4e68553 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js @@ -690,6 +690,26 @@ module.controller('RealmKeysDetailCtrl', function($scope, Realm, realm, $http, $ }; }); +module.controller('RealmSessionStatsCtrl', function($scope, realm, stats, RealmSessionStats, RealmLogoutAll, Notifications) { + $scope.realm = realm; + $scope.stats = stats; + + console.log(stats); + + $scope.logoutAll = function() { + RealmLogoutAll.save({realm : realm.realm}, function () { + Notifications.success('Logged out all users'); + RealmSessionStats.get({realm: realm.realm}, function(updated) { + Notifications.success('Logged out all users'); + $scope.stats = updated; + }) + }); + }; + + +}); + + module.controller('RealmRevocationCtrl', function($scope, Realm, RealmPushRevocation, realm, $http, $location, Dialog, Notifications) { $scope.realm = angular.copy(realm); diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/users.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/users.js index b0048a983b..178e26b6ab 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/users.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/users.js @@ -118,6 +118,35 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, ro }); +module.controller('UserSessionsCtrl', function($scope, realm, user, stats, UserLogout, ApplicationLogoutUser, UserSessionStats, Notifications) { + $scope.realm = realm; + $scope.user = user; + $scope.stats = stats; + + $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; + }) + }); + }; + + $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; + }) + }); + }; + + + +}); + + module.controller('UserListCtrl', function($scope, realm, User) { $scope.realm = realm; $scope.searchQuery = function() { diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js index d5b391772d..84f8dbce8d 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js @@ -48,20 +48,37 @@ module.factory('RealmLoader', function(Loader, Realm, $route, $q) { }); module.factory('UserListLoader', function(Loader, User, $route, $q) { - return Loader.query(User, function() { - return { - realm : $route.current.params.realm - } - }); + return Loader.query(User, function() { + return { + realm : $route.current.params.realm + } + }); +}); + +module.factory('RealmSessionStatsLoader', function(Loader, RealmSessionStats, $route, $q) { + return Loader.get(RealmSessionStats, function() { + return { + realm : $route.current.params.realm + } + }); }); module.factory('UserLoader', function(Loader, User, $route, $q) { - return Loader.get(User, function() { - return { - realm : $route.current.params.realm, - userId : $route.current.params.user - } - }); + return Loader.get(User, function() { + return { + realm : $route.current.params.realm, + userId : $route.current.params.user + } + }); +}); + +module.factory('UserSessionStatsLoader', function(Loader, UserSessionStats, $route, $q) { + return Loader.get(UserSessionStats, function() { + return { + realm : $route.current.params.realm, + user : $route.current.params.user + } + }); }); module.factory('RoleLoader', function(Loader, Role, $route, $q) { @@ -91,6 +108,15 @@ module.factory('ApplicationRoleLoader', function(Loader, ApplicationRole, $route }); }); +module.factory('ApplicationSessionStatsLoader', function(Loader, ApplicationSessionStats, $route, $q) { + return Loader.get(ApplicationSessionStats, 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/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 1397eb1951..cb31b5e842 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 @@ -158,9 +158,15 @@ module.factory('User', function($resource) { }); module.factory('UserSessionStats', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/users/:userId/session-stats', { + return $resource('/auth/rest/admin/realms/:realm/users/:user/session-stats', { realm : '@realm', - userId : '@userId' + user : '@user' + }); +}); +module.factory('UserLogout', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/users/:user/logout', { + realm : '@realm', + user : '@user' }); }); @@ -489,6 +495,32 @@ module.factory('ApplicationSessionStats', function($resource) { }); }); +module.factory('ApplicationSessionStatsWithUsers', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/applications/:application/session-stats?users=true', { + realm : '@realm', + application : "@application" + }); +}); + +module.factory('ApplicationLogoutAll', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/applications/:application/logout-all', { + realm : '@realm', + application : "@application" + }); +}); +module.factory('ApplicationLogoutUser', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/applications/:application/logout-user/:user', { + realm : '@realm', + application : "@application", + user : "@user" + }); +}); +module.factory('RealmLogoutAll', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/logout-all', { + realm : '@realm' + }); +}); + module.factory('ApplicationPushRevocation', function($resource) { return $resource('/auth/rest/admin/realms/:realm/applications/:application/push-revocation', { realm : '@realm', diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-claims.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-claims.html index 2b2e34bf61..f33e8cdbce 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-claims.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-claims.html @@ -8,6 +8,7 @@
  • Claims
  • Scope
  • Revocation
  • +
  • Sessions