Skip to content

Commit

Permalink
SI-72 include in 23.10.24 LTS
Browse files Browse the repository at this point in the history
  • Loading branch information
erickgonzalez committed Jul 19, 2024
1 parent 8b184bd commit 8caa088
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 74 deletions.
3 changes: 2 additions & 1 deletion dotCMS/hotfix_tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,5 @@ This maintenance release includes the following code fixes:
117. https://github.com/dotCMS/core/issues/28890 : LanguageUtil.getLanguageId is always hitting the db #28890
118. https://github.com/dotCMS/core/issues/26421 : Block Editor: Add align-justify option to menu #26421
119. https://github.com/dotCMS/core/issues/27537 : Download database not working in latest version #27537
120. https://github.com/dotCMS/core/issues/27871 : refactor pg_dump inclusion in our docker image #27871
120. https://github.com/dotCMS/core/issues/27871 : refactor pg_dump inclusion in our docker image #27871
121. SI-72
245 changes: 187 additions & 58 deletions dotCMS/src/main/java/com/dotcms/cmsmaintenance/ajax/UserSessionAjax.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.dotcms.cmsmaintenance.ajax;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.time.Duration;
import java.time.Instant;
import java.util.*;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

Expand All @@ -26,85 +26,214 @@
import com.liferay.portal.SystemException;
import com.liferay.portal.model.User;

/**
* UserSessionAjax provides AJAX endpoints for session management including
* validating user access, invalidating sessions, and retrieving session lists.
*/
public class UserSessionAjax {
private static final String CSRF_TOKEN_ATTRIBUTE = "csrfToken";
private static final String CSRF_TOKEN_TIMESTAMP_ATTRIBUTE = "csrfTokenTimestamp";
private static final Duration TOKEN_EXPIRY_DURATION = Duration.ofMinutes(15);
private static final String HMAC_ALGORITHM = "HmacSHA256";

/**
* Validates if the current user has access to the CMS Maintenance Portlet.
*
* @return true if the user has access, otherwise throws an exception.
*/
public boolean validateUser() {
HttpServletRequest req = WebContextFactory.get().getHttpServletRequest();
User user = null;
User user;
try {
user = com.liferay.portal.util.PortalUtil.getUser(req);
if(user == null || !APILocator.getLayoutAPI().doesUserHaveAccessToPortlet("maintenance", user)){
throw new DotSecurityException("User does not have access to the CMS Maintance Portlet");
if (user == null || !APILocator.getLayoutAPI().doesUserHaveAccessToPortlet("maintenance", user)) {
throw new DotSecurityException("User does not have access to the CMS Maintenance Portlet");
}
return true;
} catch (Exception e) {
Logger.error(this, e.getMessage());
throw new DotRuntimeException (e.getMessage());
} catch (NoSuchUserException | DotDataException | DotSecurityException e) {
Logger.error(this, "Error validating user: " + e.getMessage(), e);
throw new DotRuntimeException("Error validating user", e);
}
}

public void invalidateSession(String sessionId) throws PortalException, SystemException, NoSuchUserException, DotDataException, DotSecurityException {
/**
* Invalidates a session specified by the token.
*
* @param token the token of the session to invalidate
* @throws NoSuchUserException
* @throws DotDataException
* @throws DotSecurityException
*/
public void invalidateSession(String token) throws NoSuchUserException, DotDataException, DotSecurityException {
validateUser();
SessionMonitor sm = (SessionMonitor)
WebContextFactory.get().getServletContext().getAttribute(WebKeys.USER_SESSIONS);
SessionMonitor sm = getSessionMonitor();

HttpServletRequest req = WebContextFactory.get().getHttpServletRequest();
User currentUser = com.liferay.portal.util.PortalUtil.getUser(req);

if(sm.getUserSessions().containsKey(sessionId)) {
HttpSession session=sm.getUserSessions().get(sessionId);
User user=APILocator.getUserAPI().loadUserById(sm.getSysUsers().get(sessionId), APILocator.getUserAPI().getSystemUser(), false);

if(!currentUser.getUserId().equals(user.getUserId())) {
session.invalidate();
} else {
throw new IllegalArgumentException("can't invalidate session "+sessionId);
}
HttpSession callingSession = WebContextFactory.get().getSession();

String csrfToken = getAndValidateCSRF(callingSession);

boolean sessionInvalidated = false;

for (String sessionId : sm.getSysUsers().keySet()) {
HttpSession session = sm.getUserSessions().get(sessionId);
synchronized (session) {
if (validateSessionId(session.getId(), csrfToken, token)) {
User user = APILocator.getUserAPI().loadUserById(sm.getSysUsers().get(sessionId), APILocator.getUserAPI().getSystemUser(), false);
if (!callingSession.getId().equals(sessionId)) {
session.invalidate();
sessionInvalidated = true;
break;
} else {
throw new IllegalArgumentException("Can't invalidate your own session");
}
}
}
}
else {
throw new IllegalArgumentException("can't invalidate session "+sessionId);

if (!sessionInvalidated) {
throw new IllegalArgumentException("Invalid or expired token");
}
}

public void invalidateAllSessions() throws PortalException, SystemException, NoSuchUserException, DotDataException, DotSecurityException {
/**
* Invalidates all sessions except for the current user's session.
*
* @throws NoSuchUserException
* @throws DotDataException
* @throws DotSecurityException
*/
public void invalidateAllSessions() throws NoSuchUserException, DotDataException, DotSecurityException {
validateUser();
SessionMonitor sm = (SessionMonitor)
WebContextFactory.get().getServletContext().getAttribute(WebKeys.USER_SESSIONS);
SessionMonitor sm = getSessionMonitor();
HttpSession callingSession = WebContextFactory.get().getSession();

for (String sessionId : sm.getSysUsers().keySet()) {
HttpSession session = sm.getUserSessions().get(sessionId);
synchronized (session) {
if (!callingSession.getId().equals(sessionId)) {
session.invalidate();
}
}
}
}

HttpServletRequest req = WebContextFactory.get().getHttpServletRequest();
User currentUser = com.liferay.portal.util.PortalUtil.getUser(req);
/**
* Retrieves a list of active sessions with their details.
*
* @return a list of maps containing session details
* @throws NoSuchUserException
* @throws DotDataException
* @throws DotSecurityException
*/
public List<Map<String, String>> getSessionList() throws NoSuchUserException, DotDataException, DotSecurityException {
validateUser();
List<Map<String, String>> sessionList = new ArrayList<>();
SessionMonitor sm = getSessionMonitor();
HttpSession callingSession = WebContextFactory.get().getSession();
String csrfToken = generateAndStoreToken(callingSession);

for (String sessionId : sm.getSysUsers().keySet()) {
HttpSession session = sm.getUserSessions().get(sessionId);
synchronized (session) {
Map<String, String> sessionInfo = new HashMap<>();
String obfSession = obfuscateSessionId(sessionId, csrfToken);
sessionInfo.put("obfSession", obfSession);
sessionInfo.put("isCurrent", String.valueOf(sessionId.equals(callingSession.getId())));
User user = APILocator.getUserAPI().loadUserById(sm.getSysUsers().get(sessionId), APILocator.getUserAPI().getSystemUser(), false);
sessionInfo.put("userId", user.getUserId());
sessionInfo.put("userEmail", user.getEmailAddress());
sessionInfo.put("userFullName", user.getFullName());
sessionInfo.put("address", sm.getSysUsersAddress().get(sessionId));
Date creationTime = new Date(session.getCreationTime());
sessionInfo.put("sessionTime", DateUtil.prettyDateSince(creationTime, PublicCompanyFactory.getDefaultCompany().getLocale()));
sessionList.add(sessionInfo);
}
}
return sessionList;
}

for(String id : sm.getSysUsers().keySet()) {
HttpSession session=sm.getUserSessions().get(id);
User user=APILocator.getUserAPI().loadUserById(sm.getSysUsers().get(id), APILocator.getUserAPI().getSystemUser(), false);
/**
* Generates and stores a CSRF token in the session.
*
* @param session the HTTP session
* @return the generated CSRF token
*/
private String generateAndStoreToken(HttpSession session) {
String token = UUID.randomUUID().toString();
Instant timestamp = Instant.now();
session.setAttribute(CSRF_TOKEN_ATTRIBUTE, token);
session.setAttribute(CSRF_TOKEN_TIMESTAMP_ATTRIBUTE, timestamp);
return token;
}

if(!currentUser.getUserId().equals(user.getUserId())) {
session.invalidate();
}
/**
* Retrieves and validates the CSRF token from the session.
*
* @param session the HTTP session
* @return the CSRF token
*/
private String getAndValidateCSRF(HttpSession session) {
String csrf = (String) session.getAttribute(CSRF_TOKEN_ATTRIBUTE);
Instant tokenTimestamp = (Instant) session.getAttribute(CSRF_TOKEN_TIMESTAMP_ATTRIBUTE);
if (csrf == null || tokenTimestamp == null || !isTokenValid(tokenTimestamp)) {
throw new IllegalArgumentException("Invalid or expired token");
}
return csrf;
}

public List<Map<String,String>> getSessionList() throws NoSuchUserException, DotDataException, DotSecurityException {
validateUser();
List<Map<String,String>> sessionList=new ArrayList<>();
SessionMonitor sm = (SessionMonitor)
WebContextFactory.get().getServletContext().getAttribute(WebKeys.USER_SESSIONS);

SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

for(String id : sm.getSysUsers().keySet()) {
Map<String,String> ss=new HashMap<>();
ss.put("sessionId",id);
User user=APILocator.getUserAPI().loadUserById(sm.getSysUsers().get(id), APILocator.getUserAPI().getSystemUser(), false);
ss.put("userId",user.getUserId());
ss.put("userEmail", user.getEmailAddress());
ss.put("userFullName", user.getFullName());
ss.put("address", sm.getSysUsersAddress().get(id));
HttpSession session=sm.getUserSessions().get(id);
Date d = new Date();
d.setTime(session.getCreationTime());
ss.put("sessionTime", DateUtil.prettyDateSince(d, PublicCompanyFactory.getDefaultCompany().getLocale()) );
sessionList.add(ss);
/**
* Obfuscates a session ID using HMAC with the specified secret key.
*
* @param sessionId the session ID to obfuscate
* @param secretKey the secret key for HMAC
* @return the obfuscated session ID
*/
public static String obfuscateSessionId(String sessionId, String secretKey) {
try {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(), HMAC_ALGORITHM);
mac.init(key);
byte[] hash = mac.doFinal(sessionId.getBytes());
byte[] truncatedHash = new byte[16];
System.arraycopy(hash, 0, truncatedHash, 0, 16);
return Base64.getUrlEncoder().withoutPadding().encodeToString(truncatedHash);
} catch (Exception e) {
throw new RuntimeException(e);
}
return sessionList;
}

/**
* Validates if the provided obfuscated session ID matches the original session ID using the secret key.
*
* @param sessionId the original session ID
* @param secretKey the secret key for HMAC
* @param obfuscatedId the obfuscated session ID to validate
* @return true if the session IDs match, false otherwise
*/
public static boolean validateSessionId(String sessionId, String secretKey, String obfuscatedId) {
String generatedObfuscatedId = obfuscateSessionId(sessionId, secretKey);
return generatedObfuscatedId.equals(obfuscatedId);
}

/**
* Checks if the token is still valid based on its timestamp.
*
* @param tokenTimestamp the timestamp of the token
* @return true if the token is valid, false otherwise
*/
private boolean isTokenValid(Instant tokenTimestamp) {
return Instant.now().isBefore(tokenTimestamp.plus(TOKEN_EXPIRY_DURATION));
}

/**
* Retrieves the SessionMonitor from the servlet context.
*
* @return the SessionMonitor instance
*/
private SessionMonitor getSessionMonitor() {
return (SessionMonitor) WebContextFactory.get().getServletContext().getAttribute(WebKeys.USER_SESSIONS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -791,22 +791,22 @@ var createRow = function (tableId, rowInnerHtml, className, id) {
}, tableNode );
};
function killSessionById(sessionId) {
var invalidateButtonElem = dijit.byId("invalidateButtonNode-"+sessionId);
function killSessionById(obfSessionId) {
var invalidateButtonElem = dijit.byId("invalidateButtonNode-"+obfSessionId);
dojo.style(invalidateButtonElem.domNode,{display:"none",visibility:"hidden"});
dojo.query('#killSessionProgress-'+sessionId).style({display:"block"});
UserSessionAjax.invalidateSession(sessionId,{
dojo.query('#killSessionProgress-'+obfSessionId).style({display:"block"});
UserSessionAjax.invalidateSession(obfSessionId,{
callback:function() {
dojo.style(invalidateButtonElem.domNode,{display:"block",visibility:"visible"});
dojo.query('#killSessionProgress-'+sessionId).style({display:"none"});
dojo.query('#killSessionProgress-'+obfSessionId).style({display:"none"});
invalidateButtonElem.set('disabled',true);
showDotCMSSystemMessage('<%=UtilMethods.escapeSingleQuotes(LanguageUtil.get(pageContext,"logged-users-tab-killed"))%>');
},
errorHandler:function(message) {
dojo.style(invalidateButtonElem.domNode,{display:"block",visibility:"visible"});
dojo.query('#killSessionProgress-'+sessionId).style({display:"none"});
dojo.query('#killSessionProgress-'+obfSessionId).style({display:"none"});
showDotCMSSystemMessage('<%=UtilMethods.escapeSingleQuotes(LanguageUtil.get(pageContext,"logged-users-tab-notkilled"))%>');
}
Expand Down Expand Up @@ -855,21 +855,21 @@ function loadUsers() {
html+="<td>"+session.userEmail+"</td> ";
html+="<td>"+session.userFullName+"</td> ";
html+="<td> ";
if("<%=session.getId()%>" !=session.sessionId ){
html+=" <img style='display:none;' id='killSessionProgress-"+session.sessionId+"' src='/html/images/icons/round-progress-bar.gif'/> ";
html+=" <button id='" + invalidateButtonIdPrefix + session.sessionId + "'></button>";
}
if( "true" !== session.isCurrent ) {
html += " <img style='display:none;' id='killSessionProgress-" + session.obfSession + "' src='/html/images/icons/round-progress-bar.gif'/> ";
html += " <button id='" + invalidateButtonIdPrefix + session.obfSession + "'></button>";
}
html+=" </td>";
//Creating the row and adding it to the table
createRow(tableId, html, rowsClass, "loggedUser-"+session.sessionId)
createRow(tableId, html, rowsClass, "loggedUser-"+session.obfSession)
}
for(var i=0;i<sessionList.length;i++) {
var session=sessionList[i];
var id = invalidateButtonIdPrefix + session.sessionId;
var id = invalidateButtonIdPrefix + session.obfSession;
//Verify if a button widget with this id exist, if it exist we must delete firts before to try to create a new one
if (dojo.byId(id)) {
Expand All @@ -883,11 +883,11 @@ function loadUsers() {
label: "<%= UtilMethods.escapeDoubleQuotes(LanguageUtil.get(pageContext,"logged-users-tab-killsession")) %>",
iconClass: "deleteIcon",
"class": "killsessionButton",
sid : session.sessionId,
obsid : session.obfSession,
onClick: function(){
killSessionById(this.sid);
killSessionById(this.obsid);
}
}, invalidateButtonIdPrefix + session.sessionId);
}, invalidateButtonIdPrefix + session.obfSession);
}
}
}
Expand Down

0 comments on commit 8caa088

Please sign in to comment.