-
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-credentials.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-credentials.html
index e95bcc1178..b3b98392ee 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-credentials.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-credentials.html
@@ -8,6 +8,7 @@
- Claims
- Scope
- Revocation +
- Sessions
- Claims
- Scope
- Revocation +
- Sessions
- Claims
- Scope
- Revocation +
- Sessions
- Claims
- Scope
- Revocation +
- Sessions
- Claims
- Scope
- Revocation +
- Sessions
- Claims
- Scope
- Revocation +
- Sessions
- Claims
- Scope
- Revocation +
- Sessions
- Settings +
- Credentials +
- Installation +
- Roles +
- Claims +
- Scope +
- Revocation +
- Sessions +
- {{application.name}} +
- Application Sessions +
- Attributes
- Credentials
- Role Mappings +
- Sessions
- Revocation +
- Realm Sessions +
- {{realm.realm}} +
- Realm Sessions +
- Attributes
- Credentials
- Role Mappings +
- Sessions
- Attributes
- Credentials
- Role Mappings +
- Sessions
- Attributes +
- Credentials +
- Role Mappings +
- Sessions +
- {{realm.realm}} +
- Users +
- {{user.username}} +
- User Sessions +
-
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-detail.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-detail.html
index d8b1dc4433..dc834c371d 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-detail.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-detail.html
@@ -8,6 +8,7 @@
-
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-installation.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-installation.html
index 2caaa81b48..9c527d782d 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-installation.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-installation.html
@@ -9,6 +9,7 @@
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-revocation.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-revocation.html
index 8295782ac1..6b7e951b7a 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-revocation.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-revocation.html
@@ -8,6 +8,7 @@
-
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-role-detail.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-role-detail.html
index a7bae45fe2..8e3d603e86 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-role-detail.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-role-detail.html
@@ -8,6 +8,7 @@
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-role-list.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-role-list.html
index 49329fef98..77b4065940 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-role-list.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-role-list.html
@@ -9,6 +9,7 @@
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-scope-mappings.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-scope-mappings.html
index 5be878c152..ac6dd6f58b 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-scope-mappings.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-scope-mappings.html
@@ -9,6 +9,7 @@
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-sessions.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-sessions.html
new file mode 100755
index 0000000000..129647cf3d
--- /dev/null
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-sessions.html
@@ -0,0 +1,60 @@
+
+
+
\ No newline at end of file
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-mappings.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-mappings.html
index c290d7abaf..63e34da355 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-mappings.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-mappings.html
@@ -4,6 +4,7 @@
-
+
+
+
+
+
+-
+
{{application.name}} Sessions
+ +
+
+ Logout All
+ Show Users
+
+ |
+ ||
---|---|---|
User | +Login Time | ++ |
{{user}} | +{{data.whenLoggedIn | date:'medium'}} | +logout | +
-
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-realm.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-realm.html
new file mode 100755
index 0000000000..27a4e4ec35
--- /dev/null
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-realm.html
@@ -0,0 +1,37 @@
+
+
+
\ No newline at end of file
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-revocation.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-revocation.html
index e8cf513af7..3ca24d0b72 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-revocation.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-revocation.html
@@ -2,6 +2,7 @@
-
+
+
+
+
+
+-
+
{{realm.realm}} Sessions
+
+
+ Logout All
+
+ |
+ ||
---|---|---|
Application | +Active Sessions | +Active Users | +
{{application}} | +{{data.activeSessions}} | +{{data.activeUsers}} | +
-
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-credentials.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-credentials.html
index 6d0fd702b3..38ca069985 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-credentials.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-credentials.html
@@ -5,6 +5,7 @@
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-detail.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-detail.html
index 4ce093a959..0894d18855 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-detail.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-detail.html
@@ -5,6 +5,7 @@
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-sessions.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-sessions.html
new file mode 100755
index 0000000000..0011ad356f
--- /dev/null
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-sessions.html
@@ -0,0 +1,41 @@
+
+
protected ObjectMapper mapper = new ObjectMapper();
public SkeletonKeyContextResolver() {
- mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT);
+ mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
}
public SkeletonKeyContextResolver(boolean indent) {
- mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT);
+ mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
if (indent) {
mapper.enable(SerializationConfig.Feature.INDENT_OUTPUT);
}
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..7fcc3a006a 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,14 @@ public class AdminAction {
public void setResource(String resource) {
this.resource = resource;
}
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ 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..84d08c1837
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/adapters/action/SessionStats.java
@@ -0,0 +1,39 @@
+package org.keycloak.representations.adapters.action;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class SessionStats {
+ protected int activeSessions;
+ protected int activeUsers;
+ protected Map 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 Map getUsers() {
+ return users;
+ }
+
+ public void setUsers(Map 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..092b99bbb5 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,22 @@ 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;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
/**
* @author Bill Burke
@@ -101,6 +111,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 +126,79 @@ 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" + action.getAction());
+ 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() && userSessionManagement.getActiveSessions() > 0) {
+ Map list = new HashMap();
+ for (String user : userSessionManagement.getActiveUsers()) {
+ list.put(user, getUserStats(user));
+ }
+ stats.setUsers(list);
+ }
+ response.setStatus(200);
+ response.setContentType("application/json");
+ 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;
+ String user = action.getUser();
+ UserStats stats = getUserStats(user);
+ response.setStatus(200);
+ response.setContentType("application/json");
+ JsonSerialization.writeValueToStream(response.getOutputStream(), stats);
+ return;
+ }
+
+ protected UserStats getUserStats(String user) {
+ UserStats stats = new UserStats();
+ Long loginTime = userSessionManagement.getUserLoginTime(user);
+ if (loginTime != null) {
+ stats.setLoggedIn(true);
+ stats.setWhenLoggedIn(loginTime);
+ } else {
+ stats.setLoggedIn(false);
+ }
+ return stats;
+ }
}
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..fcf502ba69 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) {
+ sessions = new UserSessions();
+ userSessionMap.put(username, sessions);
}
- 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..96a49f0f01 100755
--- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
@@ -7,12 +7,19 @@ 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;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* @author Bill Burke
@@ -21,7 +28,87 @@ import java.util.List;
public class ResourceAdminManager {
protected static Logger logger = Logger.getLogger(ResourceAdminManager.class);
- public void singleLogOut(RealmModel realm, String user) {
+ 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);
+
+ // replace with username
+ if (users && stats.getUsers() != null) {
+ Map newUsers = new HashMap();
+ for (Map.Entry entry : stats.getUsers().entrySet()) {
+ UserModel user = realm.getUserById(entry.getKey());
+ if (user == null) continue;
+ newUsers.put(user.getLoginName(), entry.getValue());
+
+ }
+ stats.setUsers(newUsers);
+ }
+ 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 logoutUser(RealmModel realm, String user) {
ResteasyClient client = new ResteasyClientBuilder()
.disableTrustManager() // todo fix this, should have a trust manager or a good default
.build();
@@ -30,14 +117,43 @@ public class ResourceAdminManager {
List resources = realm.getApplications();
logger.debug("logging out {0} resources ", resources.size());
for (ApplicationModel resource : resources) {
- logoutResource(realm, resource, user, client);
+ logoutApplication(realm, resource, user, client);
+ }
+ } finally {
+ client.close();
+ }
+ }
+ public void logoutAll(RealmModel realm) {
+ ResteasyClient client = new ResteasyClientBuilder()
+ .disableTrustManager() // todo fix this, should have a trust manager or a good default
+ .build();
+
+ try {
+ List resources = realm.getApplications();
+ logger.debug("logging out {0} resources ", resources.size());
+ for (ApplicationModel resource : resources) {
+ logoutApplication(realm, resource, null, client);
}
} finally {
client.close();
}
}
- protected boolean logoutResource(RealmModel realm, ApplicationModel resource, String user, ResteasyClient client) {
+ public void logoutApplication(RealmModel realm, ApplicationModel resource, String user) {
+ ResteasyClient client = new ResteasyClientBuilder()
+ .disableTrustManager() // todo fix this, should have a trust manager or a good default
+ .build();
+
+ try {
+ logoutApplication(realm, resource, user, client);
+ } finally {
+ client.close();
+ }
+
+ }
+
+
+ protected boolean logoutApplication(RealmModel realm, ApplicationModel resource, String user, ResteasyClient client) {
String managementUrl = resource.getManagementUrl();
if (managementUrl != null) {
LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), (int)(System.currentTimeMillis() / 1000) + 30, resource.getName(), user);
@@ -49,7 +165,7 @@ public class ResourceAdminManager {
logger.info("logout success.");
return success;
} else {
- logger.info("logout failure.");
+ logger.info("Can't logout" + resource.getName() + " no mgmt url.");
return false;
}
}
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 6784de8c99..0cae7c2caa 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -5,17 +5,13 @@ import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.OAuthErrorException;
-import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
-import org.keycloak.models.ApplicationModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
-import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.AccessToken;
@@ -54,7 +50,6 @@ import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
-import java.security.PrivateKey;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -161,7 +156,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();
@@ -191,7 +186,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();
@@ -421,7 +416,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();
@@ -563,7 +558,7 @@ public class TokenService {
logger.info("Logging out: {0}", user.getLoginName());
authManager.expireIdentityCookie(realm, uriInfo);
authManager.expireRememberMeCookie(realm, uriInfo);
- resourceAdminManager.singleLogOut(realm, user.getId());
+ resourceAdminManager.logoutUser(realm, user.getId());
} else {
logger.info("No user logged in for logout");
}
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..085bbec8ba 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,9 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
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.ApplicationRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.ApplicationManager;
@@ -17,17 +20,22 @@ import org.keycloak.util.JsonSerialization;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
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.Application;
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;
/**
@@ -35,7 +43,7 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public class ApplicationResource {
- protected static final Logger logger = Logger.getLogger(RealmAdminResource.class);
+ protected static final Logger logger = Logger.getLogger(ApplicationResource.class);
protected RealmModel realm;
private RealmAuth auth;
protected ApplicationModel application;
@@ -193,6 +201,50 @@ public class ApplicationResource {
new ResourceAdminManager().pushApplicationRevocationPolicy(realm, application);
}
+ @Path("session-stats")
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public SessionStats getSessionStats(@QueryParam("users") @DefaultValue("false") boolean users) {
+ logger.info("session-stats");
+ auth.requireView();
+ if (application.getManagementUrl() == null || application.getManagementUrl().trim().equals("")) {
+ logger.info("sending empty stats");
+ SessionStats stats = new SessionStats();
+ if (users) stats.setUsers(new HashMap());
+ return stats;
+ }
+ SessionStats stats = new ResourceAdminManager().getSessionStats(realm, application, users);
+ if (stats == null) {
+ logger.info("app returned null stats");
+ } else {
+ logger.info("activeUsers: " + stats.getActiveUsers());
+ logger.info("activeSessions: " + stats.getActiveSessions());
+ }
+ return stats;
+ }
+
+ @Path("logout-all")
+ @POST
+ public void logoutAll() {
+ auth.requireManage();
+ new ResourceAdminManager().logoutApplication(realm, application, null);
+ }
+
+ @Path("logout-user/{username}")
+ @POST
+ public void logout(final @PathParam("username") String username) {
+ auth.requireManage();
+ UserModel user = realm.getUser(username);
+ if (user == null) {
+ throw new NotFoundException();
+ }
+ new ResourceAdminManager().logoutApplication(realm, application, user.getId());
+ }
+
+
+
+
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..c9bf4f4d20 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,27 @@ public class RealmAdminResource {
new ResourceAdminManager().pushRealmRevocationPolicy(realm);
}
+ @Path("logout-all")
+ @POST
+ public void logoutAll() {
+ auth.requireManage();
+ new ResourceAdminManager().logoutAll(realm);
+ }
+
+ @Path("session-stats")
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public Map getSessionStats() {
+ logger.info("session-stats");
+ auth.requireView();
+ 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..e3f9bbe845 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,45 @@ 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) {
+ logger.info("session-stats");
+ 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);
+ if (appStats.isLoggedIn()) stats.put(applicationModel.getName(), appStats);
+ }
+ return stats;
+ }
+
+ @Path("{username}/logout")
+ @POST
+ public void logout(final @PathParam("username") String username) {
+ auth.requireManage();
+ UserModel user = realm.getUser(username);
+ if (user == null) {
+ throw new NotFoundException();
+ }
+ new ResourceAdminManager().logoutUser(realm, user.getId());
+ }
+
+
+
@Path("{username}")
@DELETE
@NoCache
@@ -191,17 +228,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")
+
\ No newline at end of file
diff --git a/core-jaxrs/src/main/java/org/keycloak/SkeletonKeyContextResolver.java b/core-jaxrs/src/main/java/org/keycloak/SkeletonKeyContextResolver.java
index 805a8438d1..01aafd6e78 100755
--- a/core-jaxrs/src/main/java/org/keycloak/SkeletonKeyContextResolver.java
+++ b/core-jaxrs/src/main/java/org/keycloak/SkeletonKeyContextResolver.java
@@ -18,11 +18,11 @@ public class SkeletonKeyContextResolver implements ContextResolver-
+
+
+
+
+
+-
+
{{user.username}} Sessions
+
+
+ Logout
+
+ |
+ ||
---|---|---|
Application | +Login Time | ++ |
{{application}} | +{{data.whenLoggedIn | date:'medium'}} | +logout | +