420 lines
15 KiB
Java
420 lines
15 KiB
Java
/*
|
|
* Copyright 2013 Robert von Burg <eitch@eitchnet.ch>
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package li.strolch.runtime.sessions;
|
|
|
|
import li.strolch.agent.api.ComponentContainer;
|
|
import li.strolch.agent.api.StrolchComponent;
|
|
import li.strolch.exception.StrolchNotAuthenticatedException;
|
|
import li.strolch.privilege.base.AccessDeniedException;
|
|
import li.strolch.privilege.base.PrivilegeException;
|
|
import li.strolch.privilege.model.Certificate;
|
|
import li.strolch.privilege.model.PrivilegeContext;
|
|
import li.strolch.privilege.model.SimpleRestrictable;
|
|
import li.strolch.privilege.model.Usage;
|
|
import li.strolch.runtime.configuration.ComponentConfiguration;
|
|
import li.strolch.runtime.privilege.PrivilegeHandler;
|
|
import li.strolch.utils.dbc.DBC;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.text.MessageFormat;
|
|
import java.time.ZonedDateTime;
|
|
import java.util.*;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.Future;
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.stream.Collectors;
|
|
|
|
import static java.util.function.Function.identity;
|
|
import static li.strolch.runtime.StrolchConstants.StrolchPrivilegeConstants.PRIVILEGE_GET_SESSION;
|
|
import static li.strolch.runtime.StrolchConstants.StrolchPrivilegeConstants.PRIVILEGE_INVALIDATE_SESSION;
|
|
|
|
/**
|
|
* @author Robert von Burg <eitch@eitchnet.ch>
|
|
*/
|
|
public class DefaultStrolchSessionHandler extends StrolchComponent implements StrolchSessionHandler {
|
|
|
|
public static final String PARAM_SESSION_TTL_MINUTES = "session.ttl.minutes";
|
|
public static final String PARAM_SESSION_MAX_KEEP_ALIVE_MINUTES = "session.maxKeepAlive.minutes";
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(DefaultStrolchSessionHandler.class);
|
|
private PrivilegeHandler privilegeHandler;
|
|
private final Map<String, Certificate> certificateMap;
|
|
private int sessionTtlMinutes;
|
|
private int maxKeepAliveMinutes;
|
|
|
|
private ScheduledFuture<?> validateSessionsTask;
|
|
private Future<?> persistSessionsTask;
|
|
|
|
public DefaultStrolchSessionHandler(ComponentContainer container, String componentName) {
|
|
super(container, componentName);
|
|
this.certificateMap = new ConcurrentHashMap<>();
|
|
}
|
|
|
|
@Override
|
|
public int getSessionTtlMinutes() {
|
|
return this.sessionTtlMinutes;
|
|
}
|
|
|
|
@Override
|
|
public void refreshSessions() {
|
|
Map<String, Certificate> certificates;
|
|
try {
|
|
certificates = runAsAgentWithResult(ctx -> {
|
|
Certificate cert = ctx.getCertificate();
|
|
return this.privilegeHandler.getPrivilegeHandler().getCertificates(cert).stream()
|
|
.filter(c -> !c.getUserState().isSystem())
|
|
.collect(Collectors.toMap(Certificate::getAuthToken, identity()));
|
|
});
|
|
} catch (Exception e) {
|
|
throw new IllegalStateException("Failed to refresh sessions!", e);
|
|
}
|
|
|
|
synchronized (this.certificateMap) {
|
|
this.certificateMap.clear();
|
|
this.certificateMap.putAll(certificates);
|
|
}
|
|
checkSessionsForTimeout();
|
|
logger.info("Restored " + certificates.size() + " sessions of which " +
|
|
(certificates.size() - this.certificateMap.size()) + " had timed out and were removed.");
|
|
}
|
|
|
|
@Override
|
|
public int getSessionMaxKeepAliveMinutes() {
|
|
return this.maxKeepAliveMinutes;
|
|
}
|
|
|
|
@Override
|
|
public boolean isRefreshAllowed() {
|
|
return this.privilegeHandler.isRefreshAllowed();
|
|
}
|
|
|
|
@Override
|
|
public void initialize(ComponentConfiguration configuration) throws Exception {
|
|
this.sessionTtlMinutes = configuration.getInt(PARAM_SESSION_TTL_MINUTES, 30);
|
|
this.maxKeepAliveMinutes = configuration.getInt(PARAM_SESSION_MAX_KEEP_ALIVE_MINUTES,
|
|
Math.max(this.sessionTtlMinutes, 30));
|
|
super.initialize(configuration);
|
|
}
|
|
|
|
@Override
|
|
public void start() throws Exception {
|
|
this.privilegeHandler = getContainer().getComponent(PrivilegeHandler.class);
|
|
|
|
refreshSessions();
|
|
|
|
this.validateSessionsTask = getScheduledExecutor("SessionHandler").scheduleWithFixedDelay(
|
|
this::checkSessionsForTimeout, 5, 1, TimeUnit.MINUTES);
|
|
|
|
super.start();
|
|
}
|
|
|
|
@Override
|
|
public void stop() throws Exception {
|
|
|
|
if (this.validateSessionsTask != null)
|
|
this.validateSessionsTask.cancel(true);
|
|
|
|
this.privilegeHandler = null;
|
|
super.stop();
|
|
}
|
|
|
|
@Override
|
|
public void destroy() throws Exception {
|
|
this.certificateMap.clear();
|
|
super.destroy();
|
|
}
|
|
|
|
@Override
|
|
public Certificate authenticate(String username, char[] password, String source, Usage usage, boolean keepAlive) {
|
|
DBC.PRE.assertNotEmpty("Username must be set!", username);
|
|
DBC.PRE.assertNotNull("Passwort must be set", password);
|
|
|
|
Certificate certificate = this.privilegeHandler.authenticate(username, password, source, usage, keepAlive);
|
|
|
|
this.certificateMap.put(certificate.getAuthToken(), certificate);
|
|
logger.info(MessageFormat.format("{0} sessions currently active.", this.certificateMap.size()));
|
|
|
|
return certificate;
|
|
}
|
|
|
|
@Override
|
|
public Certificate authenticateSingleSignOn(Object data) {
|
|
Certificate certificate = this.privilegeHandler.authenticateSingleSignOn(data);
|
|
|
|
this.certificateMap.put(certificate.getAuthToken(), certificate);
|
|
logger.info(MessageFormat.format("{0} sessions currently active.", this.certificateMap.size()));
|
|
|
|
return certificate;
|
|
}
|
|
|
|
@Override
|
|
public Certificate authenticateSingleSignOn(Object data, String source) {
|
|
Certificate certificate = this.privilegeHandler.authenticateSingleSignOn(data, source);
|
|
|
|
this.certificateMap.put(certificate.getAuthToken(), certificate);
|
|
logger.info(MessageFormat.format("{0} sessions currently active.", this.certificateMap.size()));
|
|
|
|
return certificate;
|
|
}
|
|
|
|
@Override
|
|
public Certificate refreshSession(Certificate certificate, String source) {
|
|
Certificate refreshedSession = this.privilegeHandler.refreshSession(certificate, source);
|
|
|
|
invalidate(certificate);
|
|
this.certificateMap.put(refreshedSession.getAuthToken(), refreshedSession);
|
|
logger.info(MessageFormat.format("{0} sessions currently active.", this.certificateMap.size()));
|
|
|
|
return refreshedSession;
|
|
}
|
|
|
|
@Override
|
|
public boolean isSessionKnown(String authToken) {
|
|
DBC.PRE.assertNotEmpty("authToken must be set!", authToken);
|
|
return this.certificateMap.containsKey(authToken);
|
|
}
|
|
|
|
@Override
|
|
public Certificate validate(String authToken) throws StrolchNotAuthenticatedException {
|
|
DBC.PRE.assertNotEmpty("authToken must be set!", authToken);
|
|
|
|
Certificate certificate = this.certificateMap.get(authToken);
|
|
if (certificate == null)
|
|
throw new StrolchNotAuthenticatedException(
|
|
MessageFormat.format("No certificate exists for sessionId {0}", authToken));
|
|
|
|
return validate(certificate).getCertificate();
|
|
}
|
|
|
|
@Override
|
|
public Certificate validate(String authToken, String source) throws StrolchNotAuthenticatedException {
|
|
DBC.PRE.assertNotEmpty("authToken must be set!", authToken);
|
|
|
|
Certificate certificate = this.certificateMap.get(authToken);
|
|
if (certificate == null)
|
|
throw new StrolchNotAuthenticatedException(
|
|
MessageFormat.format("No certificate exists for sessionId {0}", authToken));
|
|
|
|
return validate(certificate, source).getCertificate();
|
|
}
|
|
|
|
@Override
|
|
public PrivilegeContext validate(Certificate certificate) throws StrolchNotAuthenticatedException {
|
|
try {
|
|
PrivilegeContext privilegeContext = this.privilegeHandler.validate(certificate);
|
|
|
|
if (this.persistSessionsTask != null)
|
|
this.persistSessionsTask = getScheduledExecutor("SessionHandler").schedule(this::persistSessions, 5,
|
|
TimeUnit.SECONDS);
|
|
|
|
return privilegeContext;
|
|
} catch (PrivilegeException e) {
|
|
throw new StrolchNotAuthenticatedException(e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public PrivilegeContext validate(Certificate certificate, String source) throws StrolchNotAuthenticatedException {
|
|
try {
|
|
PrivilegeContext privilegeContext = this.privilegeHandler.validate(certificate, source);
|
|
|
|
if (this.persistSessionsTask != null)
|
|
this.persistSessionsTask = getScheduledExecutor("SessionHandler").schedule(this::persistSessions, 5,
|
|
TimeUnit.SECONDS);
|
|
|
|
return privilegeContext;
|
|
} catch (PrivilegeException e) {
|
|
throw new StrolchNotAuthenticatedException(e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
private void persistSessions() {
|
|
try {
|
|
runAsAgent(ctx -> this.privilegeHandler.getPrivilegeHandler()
|
|
.persistSessions(ctx.getCertificate(), ctx.getCertificate().getSource()));
|
|
} catch (Exception e) {
|
|
logger.error("Failed to persist sessions", e);
|
|
} finally {
|
|
this.persistSessionsTask = null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void invalidate(Certificate certificate) {
|
|
DBC.PRE.assertNotNull("Certificate must be given!", certificate);
|
|
|
|
Certificate removedCert = this.certificateMap.remove(certificate.getAuthToken());
|
|
if (removedCert == null)
|
|
logger.error(MessageFormat.format("No session was registered with token {0}", certificate.getAuthToken()));
|
|
|
|
this.privilegeHandler.invalidate(certificate);
|
|
}
|
|
|
|
@Override
|
|
public void initiateChallengeFor(Usage usage, String username) {
|
|
this.privilegeHandler.getPrivilegeHandler().initiateChallengeFor(usage, username);
|
|
}
|
|
|
|
@Override
|
|
public void initiateChallengeFor(Usage usage, String username, String source) {
|
|
this.privilegeHandler.getPrivilegeHandler().initiateChallengeFor(usage, username, source);
|
|
}
|
|
|
|
@Override
|
|
public Certificate validateChallenge(String username, String challenge) throws PrivilegeException {
|
|
DBC.PRE.assertNotEmpty("username must be set!", username);
|
|
DBC.PRE.assertNotEmpty("challenge must be set", challenge);
|
|
|
|
Certificate certificate = this.privilegeHandler.getPrivilegeHandler().validateChallenge(username, challenge);
|
|
|
|
this.certificateMap.put(certificate.getAuthToken(), certificate);
|
|
logger.info(MessageFormat.format("{0} sessions currently active.", this.certificateMap.size()));
|
|
|
|
return certificate;
|
|
}
|
|
|
|
@Override
|
|
public Certificate validateChallenge(String username, String challenge, String source) throws PrivilegeException {
|
|
DBC.PRE.assertNotEmpty("username must be set!", username);
|
|
DBC.PRE.assertNotEmpty("challenge must be set", challenge);
|
|
|
|
Certificate certificate = this.privilegeHandler.getPrivilegeHandler()
|
|
.validateChallenge(username, challenge, source);
|
|
|
|
this.certificateMap.put(certificate.getAuthToken(), certificate);
|
|
logger.info(MessageFormat.format("{0} sessions currently active.", this.certificateMap.size()));
|
|
|
|
return certificate;
|
|
}
|
|
|
|
protected void checkSessionsForTimeout() {
|
|
ZonedDateTime maxKeepAliveTime = ZonedDateTime.now().minusMinutes(this.maxKeepAliveMinutes);
|
|
ZonedDateTime timeOutTime = ZonedDateTime.now().minusMinutes(this.sessionTtlMinutes);
|
|
|
|
Map<String, Certificate> certificateMap = getCertificateMapCopy();
|
|
for (Certificate certificate : certificateMap.values()) {
|
|
if (certificate.isKeepAlive()) {
|
|
|
|
if (maxKeepAliveTime.isAfter(certificate.getLoginTime())) {
|
|
String msg = "KeepAlive for session {0} for user {1} has expired, invalidating session...";
|
|
logger.info(MessageFormat.format(msg, certificate.getSessionId(), certificate.getUsername()));
|
|
sessionTimeout(certificate);
|
|
}
|
|
|
|
} else {
|
|
if (timeOutTime.isAfter(certificate.getLastAccess())) {
|
|
String msg = "Session {0} for user {1} has expired, invalidating session...";
|
|
logger.info(MessageFormat.format(msg, certificate.getSessionId(), certificate.getUsername()));
|
|
sessionTimeout(certificate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void sessionTimeout(Certificate certificate) {
|
|
DBC.PRE.assertNotNull("Certificate must be given!", certificate);
|
|
|
|
Certificate removedCert = this.certificateMap.remove(certificate.getAuthToken());
|
|
if (removedCert == null)
|
|
logger.error(MessageFormat.format("No session was registered with token {0}", certificate.getAuthToken()));
|
|
|
|
this.privilegeHandler.sessionTimeout(certificate);
|
|
}
|
|
|
|
@Override
|
|
public UserSession getSession(Certificate certificate, String source, String sessionId) throws PrivilegeException {
|
|
PrivilegeContext ctx = this.privilegeHandler.validate(certificate, source);
|
|
ctx.assertHasPrivilege(PRIVILEGE_GET_SESSION);
|
|
|
|
Map<String, Certificate> certificateMap = getCertificateMapCopy();
|
|
for (Certificate cert : certificateMap.values()) {
|
|
if (cert.getSessionId().equals(sessionId)) {
|
|
ctx.validateAction(new SimpleRestrictable(PRIVILEGE_GET_SESSION, cert));
|
|
return new UserSession(cert);
|
|
}
|
|
}
|
|
|
|
throw new PrivilegeException("No Session exists with the id " + sessionId);
|
|
}
|
|
|
|
@Override
|
|
public List<UserSession> getSessions(Certificate certificate, String source) {
|
|
PrivilegeContext ctx = this.privilegeHandler.validate(certificate, source);
|
|
ctx.assertHasPrivilege(PRIVILEGE_GET_SESSION);
|
|
List<UserSession> sessions = new ArrayList<>(this.certificateMap.size());
|
|
|
|
Map<String, Certificate> certificateMap = getCertificateMapCopy();
|
|
for (Certificate cert : certificateMap.values()) {
|
|
try {
|
|
ctx.validateAction(new SimpleRestrictable(PRIVILEGE_GET_SESSION, cert));
|
|
sessions.add(new UserSession(cert));
|
|
} catch (AccessDeniedException e) {
|
|
// no, user may not get this session
|
|
}
|
|
}
|
|
|
|
return sessions;
|
|
}
|
|
|
|
@Override
|
|
public void invalidate(Certificate certificate, String sessionId) {
|
|
PrivilegeContext ctx = this.privilegeHandler.validate(certificate);
|
|
ctx.assertHasPrivilege(PRIVILEGE_INVALIDATE_SESSION);
|
|
|
|
boolean ok = false;
|
|
Map<String, Certificate> certificateMap = getCertificateMapCopy();
|
|
for (Certificate cert : certificateMap.values()) {
|
|
if (cert.getSessionId().equals(sessionId)) {
|
|
ctx.validateAction(new SimpleRestrictable(PRIVILEGE_INVALIDATE_SESSION, cert));
|
|
invalidate(cert);
|
|
ok = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
throw new PrivilegeException("Can not invalidate session as no session exists with the id " + sessionId);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setSessionLocale(Certificate certificate, String sessionId, Locale locale) {
|
|
if (!certificate.getSessionId().equals(sessionId)) {
|
|
String msg = "Users can only change their own session locale: {0} may not change locale of session {1}";
|
|
throw new AccessDeniedException(MessageFormat.format(msg, certificate.getUsername(), sessionId));
|
|
}
|
|
|
|
Map<String, Certificate> certificateMap = getCertificateMapCopy();
|
|
for (Certificate cert : certificateMap.values()) {
|
|
if (cert.getSessionId().equals(sessionId)) {
|
|
if (!cert.getLocale().equals(locale)) {
|
|
cert.setLocale(locale);
|
|
persistSessions();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private Map<String, Certificate> getCertificateMapCopy() {
|
|
synchronized (this.certificateMap) {
|
|
return new HashMap<>(this.certificateMap);
|
|
}
|
|
}
|
|
}
|