org.finos.legend.server.pac4j.LegendPac4jBundle Maven / Gradle / Ivy
The newest version!
// Copyright 2020 Goldman Sachs
//
// 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 org.finos.legend.server.pac4j;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import io.dropwizard.Configuration;
import io.dropwizard.configuration.ConfigurationException;
import io.dropwizard.configuration.ConfigurationSourceProvider;
import io.dropwizard.server.SimpleServerFactory;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import javax.security.auth.Subject;
import javax.servlet.DispatcherType;
import org.apache.commons.lang.StringUtils;
import org.bson.Document;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.FilterMapping;
import org.eclipse.jetty.servlet.ServletHandler;
import org.finos.legend.server.pac4j.hazelcaststore.HazelcastSessionStore;
import org.finos.legend.server.pac4j.internal.AcceptHeaderAjaxRequestResolver;
import org.finos.legend.server.pac4j.internal.SecurityFilterHandler;
import org.finos.legend.server.pac4j.internal.UsernameFilter;
import org.finos.legend.server.pac4j.kerberos.SubjectExecutor;
import org.finos.legend.server.pac4j.mongostore.MongoDbSessionStore;
import org.pac4j.core.client.Client;
import org.pac4j.core.config.Config;
import org.pac4j.core.context.J2EContext;
import org.pac4j.core.context.session.J2ESessionStore;
import org.pac4j.core.engine.DefaultSecurityLogic;
import org.pac4j.core.http.url.DefaultUrlResolver;
import org.pac4j.core.matching.Matcher;
import org.pac4j.core.matching.PathMatcher;
import org.pac4j.core.util.JavaSerializationHelper;
import org.pac4j.dropwizard.Pac4jBundle;
import org.pac4j.dropwizard.Pac4jFactory;
import org.pac4j.dropwizard.Pac4jFeatureSupport;
import org.pac4j.j2e.filter.SecurityFilter;
import org.pac4j.jax.rs.pac4j.JaxRsContext;
import org.pac4j.jax.rs.servlet.pac4j.ServletSessionStore;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class LegendPac4jBundle extends Pac4jBundle implements Pac4jFeatureSupport
{
private static final String logoutSuffix = "/logout";
private static final String callBackSuffix = "/callback";
private static final String callbackMatcher = "notCallback";
private static final String bypassMatcher = "bypassPaths";
private final Function configSupplier;
private final Function> subjectSupplierSupplier;
private ConfigurationSourceProvider configurationSourceProvider;
private ObjectMapper objectMapper;
private String defaultSessionCookieName = "LegendSSO";
@SuppressWarnings("WeakerAccess")
public LegendPac4jBundle(Function configSupplier)
{
this(configSupplier, null);
}
@SuppressWarnings("WeakerAccess")
public LegendPac4jBundle(Function configSupplier,
Function> subjectSupplierSupplier)
{
this.configSupplier = configSupplier;
this.subjectSupplierSupplier = subjectSupplierSupplier;
}
private static String cleanUrl(String inUrl)
{
String url;
try
{
url = new URI(inUrl).normalize().toString();
} catch (URISyntaxException e)
{
throw new RuntimeException("Unable to normalize path " + inUrl, e);
}
// For some reason URI.normalize() doesn't clean the start of the URL
while (url.startsWith("//"))
{
url = url.substring(1);
}
return url;
}
@Override
public Pac4jFactory getPac4jFactory(C configuration)
{
LegendPac4jConfiguration legendConfig = configSupplier.apply(configuration);
try
{
legendConfig.loadDefaults(configurationSourceProvider, objectMapper);
} catch (IOException | ConfigurationException e)
{
throw new RuntimeException(e);
}
String applicationContextPath = legendConfig.getCallbackBaseUrl() != null && !legendConfig.getCallbackBaseUrl().isEmpty() ? legendConfig.getCallbackBaseUrl() : "/";
if (configuration.getServerFactory() instanceof SimpleServerFactory)
{
applicationContextPath = ((SimpleServerFactory) configuration.getServerFactory())
.getApplicationContextPath();
}
String callbackFilterUrl = cleanUrl(legendConfig.getCallbackPrefix() + callBackSuffix);
String clientCallbackUrl = cleanUrl(applicationContextPath + callbackFilterUrl);
final SubjectExecutor subjectExecutor = new SubjectExecutor(
Objects.isNull(this.subjectSupplierSupplier) ? null : this.subjectSupplierSupplier.apply(configuration));
MongoDatabase db = null;
if (StringUtils.isNotEmpty(legendConfig.getMongoDb()) && StringUtils.isNotEmpty(legendConfig.getMongoUri()))
{
MongoClient client = new MongoClient(new MongoClientURI(legendConfig.getMongoUri()));
db = subjectExecutor.execute(() -> client.getDatabase(legendConfig.getMongoDb()));
}
MongoDatabase finalDb = db;
Pac4jFactory factory =
new Pac4jFactory()
{
@Override
public Config build()
{
Config config = super.build();
String sessionCookieName = legendConfig.getSessionTokenName() != null ? legendConfig.getSessionTokenName() : defaultSessionCookieName;
if (legendConfig.getHazelcastSession() != null && legendConfig.getHazelcastSession().isEnabled())
{
config.setSessionStore(new HazelcastSessionStore(
legendConfig.getHazelcastSession().getConfigFilePath(),
ImmutableMap.of(
J2EContext.class, new J2ESessionStore(),
JaxRsContext.class, new ServletSessionStore()), sessionCookieName));
}
else if (legendConfig.getMongoSession() != null && legendConfig.getMongoSession().isEnabled())
{
if (Objects.isNull(finalDb))
{
throw new RuntimeException(
"MongoDB needs to be configured if MongoSession is used");
}
MongoCollection userSessions = subjectExecutor.execute(
() -> finalDb.getCollection(legendConfig.getMongoSession().getCollection()));
config.setSessionStore(
new MongoDbSessionStore(
legendConfig.getMongoSession().getCryptoAlgorithm(),
legendConfig.getMongoSession().getMaxSessionLength(),
userSessions, ImmutableMap.of(
J2EContext.class, new J2ESessionStore(),
JaxRsContext.class, new ServletSessionStore()),
subjectExecutor, legendConfig.getTrustedPackages(), sessionCookieName));
}
return config;
}
};
factory.setCallbackUrl(clientCallbackUrl);
factory.setAjaxRequestResolver(new AcceptHeaderAjaxRequestResolver());
factory.setUrlResolver(new DefaultUrlResolver());
Pac4jFactory.ServletConfiguration servletConfiguration =
new Pac4jFactory.ServletConfiguration();
Pac4jFactory.ServletSecurityFilterConfiguration securityFilterConfiguration =
new Pac4jFactory.ServletSecurityFilterConfiguration();
securityFilterConfiguration.setClients(
legendConfig.getClients().stream().map(Client::getName).collect(Collectors.joining(",")));
securityFilterConfiguration.setMatchers(String.join(",",
new String[]{callbackMatcher, bypassMatcher}));
servletConfiguration.setSecurity(Collections.singletonList(securityFilterConfiguration));
Pac4jFactory.ServletCallbackFilterConfiguration callbackFilterConfiguration =
new Pac4jFactory.ServletCallbackFilterConfiguration();
callbackFilterConfiguration.setMapping(callbackFilterUrl);
servletConfiguration.setCallback(Collections.singletonList(callbackFilterConfiguration));
Pac4jFactory.ServletLogoutFilterConfiguration logoutConfiguration =
new Pac4jFactory.ServletLogoutFilterConfiguration();
String logoutUrl = cleanUrl(legendConfig.getCallbackPrefix() + logoutSuffix);
logoutConfiguration.setMapping(logoutUrl);
logoutConfiguration.setDestroySession(true);
servletConfiguration.setLogout(Collections.singletonList(logoutConfiguration));
legendConfig.getAuthorizers().stream()
.filter(a -> a instanceof MongoDbConsumer)
.forEach(a -> ((MongoDbConsumer) a).setupDb(finalDb));
factory.setAuthorizers(legendConfig.getAuthorizers().stream()
.collect(Collectors.toMap(a -> a.getClass().getName(), a -> a)));
securityFilterConfiguration.setAuthorizers(String.join(",", factory.getAuthorizers().keySet()));
DefaultSecurityLogic s = new DefaultSecurityLogic();
s.setClientFinder(legendConfig.getDefaultSecurityClient());
s.setProfileStorageDecision(new LegendUserProfileStorageDecision());
factory.setSecurityLogic(s);
factory.setServlet(servletConfiguration);
PathMatcher matcher = new PathMatcher();
if (legendConfig.getBypassPaths() != null && !legendConfig.getBypassPaths().isEmpty())
{
legendConfig.getBypassPaths().forEach(matcher::excludePath);
}
if (legendConfig.getBypassBranches() != null && !legendConfig.getBypassBranches().isEmpty())
{
legendConfig.getBypassBranches().forEach(matcher::excludeBranch);
}
Map matchers =
ImmutableMap.of(callbackMatcher, new PathMatcher("^" + callbackFilterUrl + "$"),
bypassMatcher, matcher);
factory.setMatchers(matchers);
factory.setClients(legendConfig.getClients());
return factory;
}
@Override
protected void setupJettySession(Environment environment)
{
super.setupJettySession(environment);
environment
.servlets()
.addFilter("Username", new UsernameFilter())
.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
environment
.getApplicationContext()
.setServletHandler(
new SecurityFilterHandler(environment.getApplicationContext().getServletHandler())
{
@Override
protected void handleSecurityFilter(SecurityFilter filter)
{
// No-op, required to meet SecurityFilterHandler interface
}
@Override
protected void handleMapping(FilterMapping mapping)
{
mapping.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
}
});
swapClientFinderAndStorageDecision(environment);
}
public void swapClientFinderAndStorageDecision(Environment environment)
{
for (FilterHolder h: environment.getApplicationContext().getServletHandler().getFilters())
{
if (h.getHeldClass().equals(SecurityFilter.class))
{
ServletHandler s = new ServletHandler();
s.addFilter(h);
try
{
s.initialize();
SecurityFilter filter = (SecurityFilter) s.getFilters()[0].getFilter();
DefaultSecurityLogic securityLogic = (DefaultSecurityLogic) filter.getSecurityLogic();
securityLogic.setClientFinder(((DefaultSecurityLogic)this.getConfig().getSecurityLogic()).getClientFinder());
securityLogic.setProfileStorageDecision(new LegendUserProfileStorageDecision());
filter.setSecurityLogic(securityLogic);
h.stop();
h.setFilter(filter);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}
@Override
public void setup(Bootstrap> bootstrap)
{
configurationSourceProvider = bootstrap.getConfigurationSourceProvider();
objectMapper = bootstrap.getObjectMapper();
}
@Override
protected Collection supportedFeatures()
{
Collection supportedFeatures = super.supportedFeatures();
supportedFeatures.add(this);
return supportedFeatures;
}
public static JavaSerializationHelper getSerializationHelper(List extraPackages)
{
JavaSerializationHelper helper = new JavaSerializationHelper();
helper.addTrustedPackage("org.finos.legend.server.pac4j."); // Required to serialize KerberosProfile
helper.addTrustedPackage("org.pac4j.core.profile."); // Required to serialize UserProfile
helper.addTrustedPackage("javax.security.auth."); // Required to serialize KerberosTicket
helper.addTrustedPackage("[B"); // byte[] - Required to serialize KerberosTicket
helper.addTrustedPackage("[Z"); // boolean[] - Required to serialize KerberosTicket
for (String p:extraPackages)
{
helper.addTrustedPackage(p);
}
return helper;
}
}