package org.ovirt.engine.core.bll.aaa; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.DateUtils; import org.ovirt.engine.api.extensions.aaa.Acct; import org.ovirt.engine.core.aaa.AcctUtils; import org.ovirt.engine.core.aaa.AuthenticationProfile; import org.ovirt.engine.core.aaa.AuthenticationProfileRepository; import org.ovirt.engine.core.aaa.SsoOAuthServiceUtils; import org.ovirt.engine.core.common.businessentities.EngineSession; import org.ovirt.engine.core.common.businessentities.aaa.DbUser; import org.ovirt.engine.core.common.config.Config; import org.ovirt.engine.core.common.config.ConfigValues; import org.ovirt.engine.core.dao.EngineSessionDao; import org.ovirt.engine.core.utils.timer.OnTimerMethodAnnotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Singleton public class SessionDataContainer { SsoSessionValidator ssoSessionValidator = new SsoSessionValidator(); @Inject SsoSessionUtils ssoSessionUtils; private static class SessionInfo { private ConcurrentMap<String, Object> contentOfSession = new ConcurrentHashMap<>(); } protected Logger log = LoggerFactory.getLogger(getClass()); private ConcurrentMap<String, SessionInfo> sessionInfoMap = new ConcurrentHashMap<>(); private static final String USER_PARAMETER_NAME = "user"; private static final String SOURCE_IP = "source_ip"; private static final String PROFILE_PARAMETER_NAME = "profile"; private static final String HARD_LIMIT_PARAMETER_NAME = "hard_limit"; private static final String SOFT_LIMIT_PARAMETER_NAME = "soft_limit"; private static final String ENGINE_SESSION_SEQ_ID = "engine_session_seq_id"; private static final String ENGINE_SESSION_ID = "engine_session_id"; private static final String PRINCIPAL_PARAMETER_NAME = "username"; private static final String SSO_ACCESS_TOKEN_PARAMETER_NAME = "sso_access_token"; private static final String SSO_IS_OVIRT_APP_API_SCOPE_PARAMETER_NAME = "sso_is_ovirt_app_api_scope"; private static final String SESSION_VALID_PARAMETER_NAME = "session_valid"; private static final String SOFT_LIMIT_INTERVAL_PARAMETER_NAME = "soft_limit_interval"; private static final String SESSION_START_TIME = "session_start_time"; private static final String SESSION_LAST_ACTIVE_TIME = "session_last_active_time"; private static final String OVIRT_APP_API_SCOPE = "ovirt-app-api"; private static final String OVIRT_APP_ADMIN_SCOPE = "ovirt-app-admin"; private static final String OVIRT_APP_PORTAL_SCOPE = "ovirt-app-portal"; @Inject private EngineSessionDao engineSessionDao; public String generateEngineSessionId() { String engineSessionId; try { byte[] s = new byte[64]; SecureRandom.getInstance("SHA1PRNG").nextBytes(s); engineSessionId = new Base64(0).encodeToString(s); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } return engineSessionId; } /** * Get data by session and internal key * * @param sessionId * - id of session * @param key * - the internal key * @param refresh * - if perform refresh of session */ public final Object getData(String sessionId, String key, boolean refresh) { if (sessionId == null) { return null; } SessionInfo sessionInfo = getSessionInfo(sessionId); Object value = null; if (sessionInfo != null) { if (refresh) { refresh(sessionInfo); } value = sessionInfo.contentOfSession.get(key); } return value; } public final void setData(String sessionId, String key, Object value) { SessionInfo sessionInfo = getSessionInfo(sessionId); if (sessionInfo == null) { sessionInfo = new SessionInfo(); sessionInfo.contentOfSession.put(ENGINE_SESSION_ID, sessionId); // Add default soft-limit interval for new sessions sessionInfo.contentOfSession.put(SOFT_LIMIT_INTERVAL_PARAMETER_NAME, Config.<Integer> getValue(ConfigValues.UserSessionTimeOutInterval)); SessionInfo oldSessionInfo = sessionInfoMap.putIfAbsent(sessionId, sessionInfo); if (oldSessionInfo != null) { sessionInfo = oldSessionInfo; } } sessionInfo.contentOfSession.put(key, value); } private SessionInfo getSessionInfo(String sessionId) { return sessionInfoMap.get(sessionId); } private void persistEngineSession(String sessionId) { SessionInfo sessionInfo = getSessionInfo(sessionId); if (sessionInfo != null) { sessionInfo.contentOfSession.put(ENGINE_SESSION_SEQ_ID, engineSessionDao.save(new EngineSession(getUser(sessionId, false), sessionId, getSourceIp(sessionId)))); setSessionStartTime(sessionId); } } public long getEngineSessionSeqId(String sessionId) { if (!sessionInfoMap.containsKey(sessionId)) { throw new RuntimeException("Session not found for sessionId " + sessionId); } return (Long) sessionInfoMap.get(sessionId).contentOfSession.get(ENGINE_SESSION_SEQ_ID); } public String getSessionIdBySeqId(long sessionSequenceId) { String sessionId = null; for (SessionInfo sessionInfo : sessionInfoMap.values()) { if (Long.valueOf(sessionSequenceId).equals(sessionInfo.contentOfSession.get(ENGINE_SESSION_SEQ_ID))) { sessionId = (String) sessionInfo.contentOfSession.get(ENGINE_SESSION_ID); break; } } return sessionId; } public String getSessionIdBySsoAccessToken(String ssoToken) { String sessionId = null; if (StringUtils.isNotEmpty(ssoToken)) { for (SessionInfo sessionInfo : sessionInfoMap.values()) { if (ssoToken.equals(sessionInfo.contentOfSession.get(SSO_ACCESS_TOKEN_PARAMETER_NAME))) { sessionId = (String) sessionInfo.contentOfSession.get(ENGINE_SESSION_ID); break; } } } return sessionId; } public void cleanupEngineSessionsOnStartup() { engineSessionDao.removeAll(); } public void cleanupEngineSessionsForSsoAccessToken(String ssoAccessToken) { if (StringUtils.isNotEmpty(ssoAccessToken)) { Iterator<Entry<String, SessionInfo>> iter = sessionInfoMap.entrySet().iterator(); while (iter.hasNext()) { Entry<String, SessionInfo> entry = iter.next(); ConcurrentMap<String, Object> sessionMap = entry.getValue().contentOfSession; if (ssoAccessToken.equals(sessionMap.get(SSO_ACCESS_TOKEN_PARAMETER_NAME))) { removeSessionImpl(entry.getKey(), Acct.ReportReason.PRINCIPAL_SESSION_EXPIRED, "Session has expired for principal %1$s", getUserName(entry.getKey())); } } } } /** * Remove the cached data of current session * * @param sessionId * - id of current session */ public final void removeSessionOnLogout(String sessionId) { removeSessionImpl(sessionId, Acct.ReportReason.PRINCIPAL_LOGOUT, "Prinicial %1$s has performed logout", getUserName(sessionId)); } /** * Will run the process of cleaning expired sessions. */ @OnTimerMethodAnnotation("cleanExpiredUsersSessions") public final void cleanExpiredUsersSessions() { Date now = new Date(); Iterator<Entry<String, SessionInfo>> iter = sessionInfoMap.entrySet().iterator(); Set<String> tokens = sessionInfoMap.values().stream() .map(sessionInfo -> (String) sessionInfo.contentOfSession.get(SSO_ACCESS_TOKEN_PARAMETER_NAME)) .collect(Collectors.toSet()); // retrieve session statues from SSO Map<String, Boolean> sessionStatuses = ssoSessionValidator.getSessionStatuses(tokens); while (iter.hasNext()) { Entry<String, SessionInfo> entry = iter.next(); ConcurrentMap<String, Object> sessionMap = entry.getValue().contentOfSession; Date hardLimit = (Date) sessionMap.get(HARD_LIMIT_PARAMETER_NAME); Date softLimit = (Date) sessionMap.get(SOFT_LIMIT_PARAMETER_NAME); String token = (String) sessionMap.get(SSO_ACCESS_TOKEN_PARAMETER_NAME); // if the session was created after the tokens statuses were retrieved from the server, the token will not // have a session status in the sessionStatuses map. The session for the token will be checked and cleaned // in the next iteration. if (!sessionStatuses.containsKey(token)) { continue; } boolean sessionValid = StringUtils.isEmpty(token) ? false : sessionStatuses.get(token); if (((hardLimit != null && hardLimit.before(now)) || (softLimit != null && softLimit.before(now))) || !(boolean) sessionMap.get(SESSION_VALID_PARAMETER_NAME) || !sessionValid) { removeSessionImpl(entry.getKey(), Acct.ReportReason.PRINCIPAL_SESSION_EXPIRED, "Session has expired for principal %1$s", getUserName(entry.getKey())); if (sessionValid) { SsoOAuthServiceUtils.revoke((String) sessionMap.get(SSO_ACCESS_TOKEN_PARAMETER_NAME), ""); } } } } /** * Sets the user for the given session Id * @param sessionId The session to set * @param user The user to set */ public final void setUser(String sessionId, DbUser user) { setData(sessionId, USER_PARAMETER_NAME, user); setSessionValid(sessionId, true); persistEngineSession(sessionId); } public final void setSessionValid(String sessionId, boolean valid) { setData(sessionId, SESSION_VALID_PARAMETER_NAME, valid); } public final void setSessionStartTime(String sessionId) { setData(sessionId, SESSION_START_TIME, new Date()); } public final Date getSessionStartTime(String sessionId) { return (Date) getData(sessionId, SESSION_START_TIME, false); } public final void updateSessionLastActiveTime(String sessionId) { if (isSessionExists(sessionId)) { setData(sessionId, SESSION_LAST_ACTIVE_TIME, new Date()); refresh(sessionId); } } public final Date getSessionLastActiveTime(String sessionId) { return (Date) getData(sessionId, SESSION_LAST_ACTIVE_TIME, false); } public boolean getSessionValid(String sessionId, boolean refresh) { Object obj = getData(sessionId, SESSION_VALID_PARAMETER_NAME, refresh); return obj == null ? false : (boolean) obj; } public final void setHardLimit(String sessionId, Date hardLimit) { setData(sessionId, HARD_LIMIT_PARAMETER_NAME, hardLimit); } public final void setSoftLimit(String sessionId, Date softLimit) { setData(sessionId, SOFT_LIMIT_PARAMETER_NAME, softLimit); } public final void setSoftLimitInterval(String sessionId, int softLimitInterval) { setData(sessionId, SOFT_LIMIT_INTERVAL_PARAMETER_NAME, softLimitInterval); } /** * @param sessionId The session to get the user for * @param refresh Whether refreshing the session is needed * @return The user set for the given {@link #session} */ public DbUser getUser(String sessionId, boolean refresh) { return (DbUser) getData(sessionId, USER_PARAMETER_NAME, refresh); } public void refresh(String sessionId) { refresh(getSessionInfo(sessionId)); } public void setProfile(String sessionId, AuthenticationProfile profile) { setData(sessionId, PROFILE_PARAMETER_NAME, profile); } public AuthenticationProfile getProfile(String sessionId) { AuthenticationProfile profile = (AuthenticationProfile) getData(sessionId, PROFILE_PARAMETER_NAME, false); if (profile == null) { profile = getProfileFromUser(getUser(sessionId, false)); } return profile; } private AuthenticationProfile getProfileFromUser(DbUser user) { AuthenticationProfile retVal = null; if (user != null) { for (AuthenticationProfile profile : AuthenticationProfileRepository.getInstance().getProfiles()) { if (profile.getAuthzName().equals(user.getDomain())) { retVal = profile; break; } } } return retVal; } public String getPrincipalName(String sessionId) { return (String) getData(sessionId, PRINCIPAL_PARAMETER_NAME, false); } public String getUserName(String sessionId) { return String.format( "%s@%s", getPrincipalName(sessionId), getProfile(sessionId) != null ? getProfile(sessionId).getAuthzName() : "N/A"); } public void setPrincipalName(String engineSessionId, String name) { setData(engineSessionId, PRINCIPAL_PARAMETER_NAME, name); } public void setSsoAccessToken(String engineSessionId, String ssoToken) { setData(engineSessionId, SSO_ACCESS_TOKEN_PARAMETER_NAME, ssoToken); } public String getSsoAccessToken(String engineSessionId) { return (String) getData(engineSessionId, SSO_ACCESS_TOKEN_PARAMETER_NAME, false); } public void setSsoOvirtAppApiScope(String engineSessionId, String scope) { List<String> scopes = StringUtils.isEmpty(scope) ? Collections.emptyList() : Arrays.asList(scope.trim().split("\\s *")); setData(engineSessionId, SSO_IS_OVIRT_APP_API_SCOPE_PARAMETER_NAME, scopes.contains(OVIRT_APP_API_SCOPE) && !scopes.contains(OVIRT_APP_ADMIN_SCOPE) && !scopes.contains(OVIRT_APP_PORTAL_SCOPE)); } public boolean isSsoOvirtAppApiScope(String engineSessionId) { return isSessionExists(engineSessionId) ? (boolean) getData(engineSessionId, SSO_IS_OVIRT_APP_API_SCOPE_PARAMETER_NAME, false): false; } public void setSourceIp(String engineSessionId, String sourceIp) { setData(engineSessionId, SOURCE_IP, sourceIp); } public String getSourceIp(String engineSessionId) { return (String) getData(engineSessionId, SOURCE_IP, false); } private void refresh(SessionInfo sessionInfo) { int softLimitValue = (Integer) sessionInfo.contentOfSession.get(SOFT_LIMIT_INTERVAL_PARAMETER_NAME); if (softLimitValue > 0) { sessionInfo.contentOfSession.put(SOFT_LIMIT_PARAMETER_NAME, DateUtils.addMinutes(new Date(), softLimitValue)); } } public boolean isSessionExists(String sessionId) { return StringUtils.isEmpty(sessionId) ? false : sessionInfoMap.containsKey(sessionId); } private void removeSessionImpl(String sessionId, int reason, String message, Object... msgArgs) { // Only remove session if there are no running commands for this session if (ssoSessionUtils.isSessionInUse(getEngineSessionSeqId(sessionId))) { DbUser dbUser = getUser(sessionId, false); log.info("Not removing session '{}', session has running commands{}", sessionId, dbUser == null ? "." : String.format(" for user '%s@%s'.", dbUser.getLoginName(), dbUser.getDomain())); return; } /* * So we won't need to add profile to tests */ String authzName = null; if (getProfile(sessionId) != null) { authzName = getProfile(sessionId).getAuthzName(); } AcctUtils.reportRecords(reason, authzName, getPrincipalName(sessionId), message, msgArgs ); engineSessionDao.remove(getEngineSessionSeqId(sessionId)); sessionInfoMap.remove(sessionId); } class SsoSessionValidator { public Map<String, Boolean> getSessionStatuses(Set<String> tokens) { Map<String, Boolean> sessionStatuses = Collections.emptyMap(); if (!tokens.isEmpty()) { try { Map<String, Object> response = SsoOAuthServiceUtils.getSessionStatues(tokens); if (response.get("error") == null) { sessionStatuses = (Map<String, Boolean>) response.get("result"); } } catch (Exception e) { log.error("Unable to retrieve session statuses." + e.getMessage()); } } return sessionStatuses; } } }