/* Copyright (c) 2011 Danish Maritime Authority.
*
* 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 net.maritimecloud.mms.server.security;
import com.typesafe.config.Config;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.websocket.Session;
import java.util.Objects;
/**
* The MMS server security manager.
* <p/>
* The security can be instantiated with a security configuration file that may define handler classes for the
* following type of security-related information groups:
* <ul>
* <li>ssl-conf: Configures an {@code SslHandler} handler class that instantiates a {@code SslContextFactory} used
* by the MMS server.</li>
* <li>auth-token-conf: Configures an {@code AuthenticationTokenHandler} class that resolves an
* {@code AuthenticationToken} from the websocket upgrade request.</li>
* <li>authentication-conf: Configures an {@code AuthenticationHandler} class that will
* authenticate the client using the resolved authentication token.</li>
* <li>client-verification-conf: Configures a {@code ClientVerificationHandler} class that will
* check verify that the client is valid for the current user.</li>
* </ul>
*
* The handlers are not used directly in security manager client code. Instead, the security manager
* and the {@code Subject} class contains functions that will use the handlers in turn.
*/
public class MmsSecurityManager {
private static final String SUBJECT_SESSION_ATTR = "mms.subject";
private static final String HANDLER_CLASS_ATTR = "handler-class";
private static final Logger LOG = LoggerFactory.getLogger(MmsSecurityManager.class);
private final Config conf;
private final SslHandler sslHandler;
private final AuthenticationTokenHandler authenticationTokenHandler;
private final AuthenticationHandler authenticationHandler;
private final ClientVerificationHandler clientVerificationHandler;
/**
* Constructor
* @param securityConf the MMS security configuration file
*/
public MmsSecurityManager(Config securityConf) {
conf = securityConf;
// Initialize the security handlers from the configuration file
sslHandler = newSecurityConfHandler(SslHandler.SECURITY_CONF_GROUP);
authenticationTokenHandler = newSecurityConfHandler(AuthenticationTokenHandler.SECURITY_CONF_GROUP);
authenticationHandler = newSecurityConfHandler(AuthenticationHandler.SECURITY_CONF_GROUP);
clientVerificationHandler = newSecurityConfHandler(ClientVerificationHandler.SECURITY_CONF_GROUP);
}
/**
* Returns the associated configuration file
* @return the associated configuration file
*/
public Config getConf() {
return conf;
}
/**
* Returns a new SSL context factory based on the SSL configuration
* @return a new SSL context factory based on the SSL configuration
*/
public SslContextFactory getSslContextFactory() {
return sslHandler != null ? sslHandler.getSslContextFactory() : new SslContextFactory();
}
/**
* Instantiates a new security configuration handler for the given security group name
*
* @param name the security group name
* @return the security configuration handler or null if undefined
*/
private <T extends BaseSecurityHandler> T newSecurityConfHandler(String name) {
if (conf.hasPath(name) && conf.getConfig(name).hasPath(HANDLER_CLASS_ATTR)) {
Config handlerConfig = conf.getConfig(name);
String handlerClass = handlerConfig.getString(HANDLER_CLASS_ATTR);
// If the class name starts with uppercase, it is in the security package
if (Character.isUpperCase(handlerClass.charAt(0))) {
handlerClass = MmsSecurityManager.class.getPackage().getName() + ".impl." + handlerClass;
}
try {
@SuppressWarnings("unchecked")
T confHandler = (T)Class.forName(handlerClass).newInstance();
confHandler.init(handlerConfig);
LOG.info("Instantiated handler " + confHandler + " of security group " + name);
return confHandler;
} catch (Exception e) {
LOG.error("Failed instantiating class " + handlerClass + " of security group " + name, e);
}
}
// Not properly defined
LOG.warn("Configuration does not define handler for security group " + name);
return null;
}
/**
* Instantiates a new {@code Subject} from the websocket session. If a subject has
* already been associated with the session, this is returned instead.
*
* @param session the websocket session
* @return the Subject
*/
synchronized Subject instantiateSubject(Session session) {
// Check if the subject is already instantiated
Subject subject = (Subject) session.getUserProperties().get(SUBJECT_SESSION_ATTR);
if (subject == null) {
// Instantiate a new subject
subject = new SubjectImpl(this);
// Associate the subject with the session
session.getUserProperties().put(SUBJECT_SESSION_ATTR, subject);
}
return subject;
}
/**
* Instantiates a new {@code Subject} from the servlet request.
*
* @param request the servlet request
* @return the Subject
*/
synchronized Subject instantiateSubject(HttpServletRequest request) {
// Instantiate a new subject
return new SubjectImpl(this);
}
/**
* Resolves an {@code AuthenticationToken} from the websocket session.
* <p>
* If none can be resolved, null is returned.
* <p>
* If an authentication token has been defined but is invalid (e.g. an expired JWT bearer token),
* an {@code AuthenticationException} may be thrown.
*
* @param session the websocket session
* @return the authentication token, or null if none is resolved
*/
public AuthenticationToken resolveAuthenticationToken(Session session) throws AuthenticationException {
// Check if we can resolve the authentication token from the upgrade request
WebSocketSession jettySession = (WebSocketSession)session;
ServletUpgradeRequest upgradeRequest = (ServletUpgradeRequest)jettySession.getUpgradeRequest();
return resolveAuthenticationToken(upgradeRequest.getHttpServletRequest());
}
/**
* Resolves an {@code AuthenticationToken} from the servlet request.
* <p>
* If none can be resolved, null is returned.
* <p>
* If an authentication token has been defined but is invalid (e.g. an expired JWT bearer token),
* an {@code AuthenticationException} may be thrown.
*
* @param request the servlet request
* @return the authentication token, or null if none is resolved
*/
public AuthenticationToken resolveAuthenticationToken(HttpServletRequest request) throws AuthenticationException {
if (authenticationTokenHandler != null) {
return authenticationTokenHandler.resolveAuthenticationToken(request);
}
return null;
}
/**
* An implementation of the [@code Subject} interface
*/
static class SubjectImpl implements Subject {
final MmsSecurityManager securityManager;
boolean authenticated;
Object principal;
/**
* Constructor
* @param securityManager the security manager
*/
public SubjectImpl(MmsSecurityManager securityManager) {
Objects.requireNonNull(securityManager);
this.securityManager = securityManager;
}
/** {@inheritDoc} */
@Override
public Object getPrincipal() {
return principal;
}
/** {@inheritDoc} */
@Override
public boolean isAuthenticated() {
return authenticated;
}
/** {@inheritDoc} */
@Override
public void checkClient(String clientId) throws ClientVerificationException {
// If no client verification handler is defined, accept all clients
if (securityManager.clientVerificationHandler != null) {
securityManager.clientVerificationHandler.verifyClient(principal, clientId);
}
}
/** {@inheritDoc} */
@Override
public synchronized void login(AuthenticationToken token) throws AuthenticationException {
// Clear any existing logged in info
logout();
if (securityManager.authenticationHandler == null) {
throw new AuthenticationException("No authentication handler configured");
}
// Authenticate - throws an exception if authentication fails
securityManager.authenticationHandler.authenticate(token);
// Flag that the subject is authenticated
authenticated = true;
principal = token.getPrincipal();
}
/** {@inheritDoc} */
@Override
public synchronized void logout() {
authenticated = false;
principal = null;
}
}
}