From 3623d7964e57645fd84a5fa4481ad10ef1238c85 Mon Sep 17 00:00:00 2001 From: Darran Lofthouse Date: Sun, 11 Feb 2018 15:16:45 +0000 Subject: [PATCH] Resolves #123 Port over code to AuthenticationManager to configure a DeploymentInfo to activate Elytron based authentication. --- .../server/servlet/AuthenticationManager.java | 189 +++++++++++++++++- .../servlet/ElytronHttpServletExchange.java | 5 +- 2 files changed, 187 insertions(+), 7 deletions(-) diff --git a/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/AuthenticationManager.java b/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/AuthenticationManager.java index 0db624e5..4be3dcf0 100644 --- a/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/AuthenticationManager.java +++ b/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/AuthenticationManager.java @@ -16,12 +16,44 @@ package org.wildfly.elytron.web.undertow.server.servlet; +import static java.security.AccessController.doPrivileged; +import static org.wildfly.elytron.web.undertow.server.servlet.ElytronHttpServletExchange.APPLICATION_SCOPE_RESOLVER; +import static org.wildfly.elytron.web.undertow.server.servlet.IdentityMapping.mapIdentity; +import static org.wildfly.security.http.HttpConstants.CONFIG_CONTEXT_PATH; +import static org.wildfly.security.http.HttpConstants.CONFIG_ERROR_PAGE; +import static org.wildfly.security.http.HttpConstants.CONFIG_LOGIN_PAGE; +import static org.wildfly.security.http.HttpConstants.CONFIG_REALM; + +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Function; import java.util.function.UnaryOperator; +import org.jboss.metadata.javaee.jboss.RunAsIdentityMetaData; +import org.wildfly.elytron.web.undertow.server.ElytronContextAssociationHandler; +import org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler; +import org.wildfly.elytron.web.undertow.server.ScopeSessionListener; import org.wildfly.security.auth.server.HttpAuthenticationFactory; +import org.wildfly.security.auth.server.SecurityDomain; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; +import org.wildfly.security.http.Scope; +import org.wildfly.security.http.util.PropertiesServerMechanismFactory; +import org.wildfly.security.manager.WildFlySecurityManager; +import io.undertow.server.HttpHandler; +import io.undertow.servlet.api.AuthMethodConfig; +import io.undertow.servlet.api.AuthorizationManager; import io.undertow.servlet.api.DeploymentInfo; +import io.undertow.servlet.api.LoginConfig; /** * A utility class to take relevant Elytron component instances and apply them to an Undertow {@link DeploymentInfo} to apply @@ -31,20 +63,147 @@ */ public class AuthenticationManager { + private final Builder builder; + AuthenticationManager(Builder builder) { + // A builder can only be built once and further modifications are prohibited so we can cache it. + this.builder = builder; + } + /** + * Configure the {@link DeploymentInfo} so the deployment will use Elytron security based on the parameters used to + * initialise this {@link AuthenticationManaer}. + * + * @param deploymentInfo the {@link DeploymentInfo} to configure. + */ + public void configure(DeploymentInfo deploymentInfo) { + final ScopeSessionListener scopeSessionListener = ScopeSessionListener.builder() + .addScopeResolver(Scope.APPLICATION, APPLICATION_SCOPE_RESOLVER) + .build(); + + final SecurityDomain securityDomain = builder.httpAuthenticationFactory.getSecurityDomain(); + + if (WildFlySecurityManager.isChecking()) { + doPrivileged((PrivilegedAction) () -> { + securityDomain.registerWithClassLoader(deploymentInfo.getClassLoader()); + return null; + }); + } else { + securityDomain.registerWithClassLoader(deploymentInfo.getClassLoader()); + } + + deploymentInfo.addSessionListener(scopeSessionListener); + + final Function runAsMapper = builder.runAsMapper; + deploymentInfo.addInnerHandlerChainWrapper(h -> finalSecurityHandlers(h, securityDomain, runAsMapper)); + deploymentInfo.setInitialSecurityWrapper(h -> initialSecurityHandler(deploymentInfo, h, securityDomain, scopeSessionListener)); + if (runAsMapper != null) { + deploymentInfo.addLifecycleInterceptor(new RunAsLifecycleInterceptor(runAsMapper, securityDomain)); + } + + if (builder.authorizationManager != null) { + deploymentInfo.setAuthorizationManager(builder.authorizationManager); + } else { + // TODO - If we can discover the SecurityDomain at runtime this could also become a singleton. + deploymentInfo.setAuthorizationManager(new ElytronAuthorizationManager(securityDomain)); + } + } + + private HttpHandler initialSecurityHandler(final DeploymentInfo deploymentInfo, HttpHandler toWrap, SecurityDomain securityDomain, ScopeSessionListener scopeSessionListener) { + final Collection availableMechanisms = builder.httpAuthenticationFactory.getMechanismNames(); + if (availableMechanisms.isEmpty()) { + throw new IllegalStateException("There are no mechanisms available from the HttpAuthenticationFactory."); + //throw ROOT_LOGGER.noMechanismsAvailable(); + } + + Map tempBaseConfiguration = new HashMap<>(); + tempBaseConfiguration.put(CONFIG_CONTEXT_PATH, deploymentInfo.getContextPath()); + + LoginConfig loginConfig = deploymentInfo.getLoginConfig(); + if (loginConfig != null) { + String realm = loginConfig.getRealmName(); + if (realm != null) tempBaseConfiguration.put(CONFIG_REALM, realm); + String loginPage = loginConfig.getLoginPage(); + if (loginPage != null) tempBaseConfiguration.put(CONFIG_LOGIN_PAGE, loginPage); + String errorPage = loginConfig.getErrorPage(); + if (errorPage != null) tempBaseConfiguration.put(CONFIG_ERROR_PAGE, errorPage); + } + final Map baseConfiguration = Collections.unmodifiableMap(tempBaseConfiguration); + + final Map> selectedMechanisms = new LinkedHashMap<>(); + if (builder.overrideDeploymentConfig || (loginConfig == null)) { + final Map mechanismConfiguration = baseConfiguration; + for (String n : availableMechanisms) { + selectedMechanisms.put(n, mechanismConfiguration); + } + } else { + final List authMethods = loginConfig.getAuthMethods(); + if (authMethods.isEmpty()) { + throw new IllegalStateException("No authentication mechanisms have been selected."); + //throw ROOT_LOGGER.noMechanismsSelected(); + } + for (AuthMethodConfig c : authMethods) { + String name = c.getName(); + if (availableMechanisms.contains(name) == false) { + throw new IllegalStateException(String.format("The required mechanism '%s' is not available in mechanisms %s from the HttpAuthenticationFactory.", name, availableMechanisms)); + //throw ROOT_LOGGER.requiredMechanismNotAvailable(name, availableMechanisms); + } + + Map mechanismConfiguration; + Map additionalProperties = c.getProperties(); + if (additionalProperties != null) { + mechanismConfiguration = new HashMap<>(baseConfiguration); + mechanismConfiguration.putAll(additionalProperties); + mechanismConfiguration = Collections.unmodifiableMap(mechanismConfiguration); + } else { + mechanismConfiguration = baseConfiguration; + } + selectedMechanisms.put(name, mechanismConfiguration); + } + } + + return ElytronContextAssociationHandler.builder() + .setNext(toWrap) + .setSecurityDomain(securityDomain) + .setMechanismSupplier(() -> getAuthenticationMechanisms(selectedMechanisms)) + .setHttpExchangeSupplier(httpServerExchange -> new ElytronHttpServletExchange(httpServerExchange, scopeSessionListener)) + .build(); + } + + private HttpHandler finalSecurityHandlers(HttpHandler toWrap, final SecurityDomain securityDomain, final Function runAsMapper) { + return runAsMapper != null ? new ElytronRunAsHandler(toWrap, (s, e) -> mapIdentity(s, securityDomain, e, runAsMapper)) : new ElytronRunAsHandler(toWrap); + } + + private List getAuthenticationMechanisms(Map> selectedMechanisms) { + List mechanisms = new ArrayList<>(selectedMechanisms.size()); + UnaryOperator singleSignOnTransformer = builder.httpAuthenticationFactoryTransformer; + for (Entry> entry : selectedMechanisms.entrySet()) { + try { + UnaryOperator factoryTransformation = f -> { + HttpServerAuthenticationMechanismFactory factory = new PropertiesServerMechanismFactory(f, entry.getValue()); + return (singleSignOnTransformer != null) ? singleSignOnTransformer.apply(factory) : factory; + }; + HttpServerAuthenticationMechanism mechanism = builder.httpAuthenticationFactory.createMechanism(entry.getKey(), factoryTransformation); + if (mechanism != null) mechanisms.add(mechanism); + } catch (HttpAuthenticationException e) { + throw new IllegalStateException(e); + } + } + + return mechanisms; } public static Builder builder() { return new Builder(); } - static class Builder { + public static class Builder { private HttpAuthenticationFactory httpAuthenticationFactory; - private boolean enableJacc; private boolean overrideDeploymentConfig; + private AuthorizationManager authorizationManager; private UnaryOperator httpAuthenticationFactoryTransformer; + private Function runAsMapper; private boolean built = false; @@ -62,14 +221,14 @@ public Builder setHttpAuthenticationFactory(final HttpAuthenticationFactory http } /** - * Sets if JACC should be enabled for the deployment. + * Set an {@link AuthorizationManager} for the deployment, if none is provided the default Elytron AuthorizationManager will be used instead. * - * @param enableJacc should jacc be enabled for this deployment. + * @param authorizationManagerSupplier an {@link AuthorizationManager} for the deployment. * @return this {@link Builder} */ - public Builder setEnableJacc(final boolean enableJacc) { + public Builder setAuthorizationManager(final AuthorizationManager authorizationManager) { assertNotBuilt(); - this.enableJacc = enableJacc; + this.authorizationManager = authorizationManager; return this; } @@ -101,6 +260,24 @@ public Builder setHttpAuthenticationFactoryTransformer (final UnaryOperator runAsMapper) { + assertNotBuilt(); + this.runAsMapper = runAsMapper; + + return this; + } + + /** + * Assemble the supplied configuration into a complete {@link AuthenticationManager}. + * + * @return a configured {@link AuthenticationManager}. + */ public AuthenticationManager build() { assertNotBuilt(); built = true; diff --git a/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ElytronHttpServletExchange.java b/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ElytronHttpServletExchange.java index e8810d6e..cd0f8aeb 100644 --- a/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ElytronHttpServletExchange.java +++ b/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ElytronHttpServletExchange.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.function.Consumer; +import java.util.function.Function; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; @@ -57,6 +58,8 @@ class ElytronHttpServletExchange extends ElytronHttpExchange { private final HttpServerExchange httpServerExchange; private final ScopeSessionListener scopeSessionListener; + static Function APPLICATION_SCOPE_RESOLVER = ElytronHttpServletExchange::applicationScope; + protected ElytronHttpServletExchange(final HttpServerExchange httpServerExchange, final ScopeSessionListener scopeSessionListener) { super(httpServerExchange); this.httpServerExchange = httpServerExchange; @@ -129,7 +132,7 @@ public HttpScope getScope(Scope scope) { } } - static HttpScope applicationScope(HttpServerExchange exchange) { + private static HttpScope applicationScope(HttpServerExchange exchange) { ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); if (servletRequestContext != null) {