From d70859ef1bbbc06ba8fe88486fe8bc4a71cfb9d6 Mon Sep 17 00:00:00 2001 From: Hynek Mlnarik Date: Mon, 21 May 2018 14:28:08 +0200 Subject: [PATCH] KEYCLOAK-7277 KEYCLOAK-7282 Jetty/Pax Web integration --- adapters/oidc/fuse7/jetty94/pom.xml | 130 +++++++++ .../jetty94/KeycloakAuthenticatorService.java | 41 +++ .../jetty94/PaxWebIntegrationService.java | 275 ++++++++++++++++++ ...ops4j.pax.web.service.AuthenticatorService | 1 + adapters/oidc/fuse7/pom.xml | 1 + .../osgi/PaxWebIntegrationService.java | 2 +- .../features/src/main/resources/features.xml | 13 + 7 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 adapters/oidc/fuse7/jetty94/pom.xml create mode 100644 adapters/oidc/fuse7/jetty94/src/main/java/org/keycloak/adapters/osgi/jetty94/KeycloakAuthenticatorService.java create mode 100644 adapters/oidc/fuse7/jetty94/src/main/java/org/keycloak/adapters/osgi/jetty94/PaxWebIntegrationService.java create mode 100644 adapters/oidc/fuse7/jetty94/src/main/resources/META-INF/services/org.ops4j.pax.web.service.AuthenticatorService diff --git a/adapters/oidc/fuse7/jetty94/pom.xml b/adapters/oidc/fuse7/jetty94/pom.xml new file mode 100644 index 0000000000..b1a291355a --- /dev/null +++ b/adapters/oidc/fuse7/jetty94/pom.xml @@ -0,0 +1,130 @@ + + + + + + keycloak-fuse7-integration-pom + org.keycloak + 4.0.0.Beta3-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-pax-web-jetty94 + Keycloak Fuse 7.0 Adapter - Jetty 9.4 + bundle + + + + org.keycloak.adapters.osgi.jetty94.*;version="${project.version}" + + + !org.keycloak.adapters.osgi.jetty94, + org.keycloak.*;version="${project.version}", + *;resolution:=optional + + org.ops4j.pax.web.pax-web-jetty + + + + + org.jboss.logging + jboss-logging + provided + + + org.ops4j.pax.web + pax-web-api + provided + + + org.ops4j.pax.web + pax-web-spi + provided + + + org.osgi + org.osgi.core + provided + + + org.osgi + org.osgi.enterprise + provided + + + + org.keycloak + keycloak-jetty94-adapter + provided + + + org.eclipse.jetty + jetty-security + ${jetty9.version} + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.apache.felix + maven-bundle-plugin + true + + + bundle-manifest + process-classes + + manifest + + + + + + ${project.name} + ${project.groupId}.${project.artifactId} + ${keycloak.osgi.import} + ${keycloak.osgi.export} + ${keycloak.osgi.fragment} + + + + + + diff --git a/adapters/oidc/fuse7/jetty94/src/main/java/org/keycloak/adapters/osgi/jetty94/KeycloakAuthenticatorService.java b/adapters/oidc/fuse7/jetty94/src/main/java/org/keycloak/adapters/osgi/jetty94/KeycloakAuthenticatorService.java new file mode 100644 index 0000000000..30f767990a --- /dev/null +++ b/adapters/oidc/fuse7/jetty94/src/main/java/org/keycloak/adapters/osgi/jetty94/KeycloakAuthenticatorService.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.keycloak.adapters.osgi.jetty94; + +import org.ops4j.pax.web.service.AuthenticatorService; + +/** + * + * @author hmlnarik + */ +public class KeycloakAuthenticatorService implements AuthenticatorService { + + @Override + public T getAuthenticatorService(String method, Class iface) { + if (method == null || iface != org.eclipse.jetty.security.Authenticator.class) { + return null; + } + + if ("KEYCLOAK".equalsIgnoreCase(method)) { + return iface.cast(new org.keycloak.adapters.jetty.KeycloakJettyAuthenticator()); + } + + return null; + } + +} diff --git a/adapters/oidc/fuse7/jetty94/src/main/java/org/keycloak/adapters/osgi/jetty94/PaxWebIntegrationService.java b/adapters/oidc/fuse7/jetty94/src/main/java/org/keycloak/adapters/osgi/jetty94/PaxWebIntegrationService.java new file mode 100644 index 0000000000..35f5d9f959 --- /dev/null +++ b/adapters/oidc/fuse7/jetty94/src/main/java/org/keycloak/adapters/osgi/jetty94/PaxWebIntegrationService.java @@ -0,0 +1,275 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.keycloak.adapters.osgi.jetty94; + +import org.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.util.security.Constraint; +import org.jboss.logging.Logger; +import org.ops4j.pax.web.service.WebContainer; +import org.ops4j.pax.web.service.spi.model.SecurityConstraintMappingModel; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.HttpContext; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +import java.net.URL; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Integration with pax-web, which allows to inject custom jetty-web.xml configuration from current bundle classpath into {@link WebContainer} + * and allows to inject custom security constraint for securing resources by Keycloak. + * + *

It assumes that pax-web {@link WebContainer} is used as implementation of OSGI {@link org.osgi.service.http.HttpService}, which + * is true in karaf/fuse environment

+ * + * @author Marek Posolda + */ +public class PaxWebIntegrationService { + + protected static final Logger log = Logger.getLogger(PaxWebIntegrationService.class); + + private BundleContext bundleContext; + private String jettyWebXmlLocation; + private List constraintMappings; + + private ServiceTracker webContainerTracker; + private HttpContext httpContext; + + public BundleContext getBundleContext() { + return bundleContext; + } + + public void setBundleContext(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + public String getJettyWebXmlLocation() { + return jettyWebXmlLocation; + } + + public void setJettyWebXmlLocation(String jettyWebXmlLocation) { + this.jettyWebXmlLocation = jettyWebXmlLocation; + } + + public List getConstraintMappings() { + return constraintMappings; + } + + public void setConstraintMappings(List constraintMappings) { + this.constraintMappings = constraintMappings; + } + + protected ServiceTracker getWebContainerTracker() { + return webContainerTracker; + } + + protected HttpContext getHttpContext() { + return httpContext; + } + + + public void start() { + ServiceTrackerCustomizer trackerCustomizer = new ServiceTrackerCustomizer() { + + @Override + public Object addingService(ServiceReference reference) { + return addingWebContainerCallback(reference); + } + + @Override + public void modifiedService(ServiceReference reference, Object service) { + } + + @Override + public void removedService(ServiceReference reference, Object service) { + removingWebContainerCallback(reference); + } + }; + + webContainerTracker = new ServiceTracker(bundleContext, WebContainer.class.getName(), trackerCustomizer); + webContainerTracker.open(); + } + + public void stop() { + webContainerTracker.remove(webContainerTracker.getServiceReference()); + } + + protected WebContainer addingWebContainerCallback(ServiceReference webContainerServiceReference) { + WebContainer service = (WebContainer) bundleContext.getService(webContainerServiceReference); + httpContext = service.createDefaultHttpContext(); + + addJettyWebXml(service); + + if (constraintMappings == null) { + throw new IllegalStateException("constraintMappings was null!"); + } + List handlers = new ArrayList<>(); + try { + handlers.add(new JettyConstraintHandler()); + } catch (Throwable t) { + // Ignore + } + try { + handlers.add(new PaxWebConstraintHandler()); + } catch (Throwable t) { + // Ignore + } + for (Object constraintMapping : constraintMappings) { + boolean handled = false; + for (ConstraintHandler handler : handlers) { + handled |= handler.addConstraintMapping(httpContext, service, constraintMapping); + } + if (!handled) { + log.warnv("Unable to add constraint mapping for constraint of type " + constraintMapping.getClass().toString()); + } + } + + service.registerLoginConfig("BASIC", "does-not-matter", null, null, httpContext); + + return service; + } + + protected void addJettyWebXml(WebContainer service) { + String jettyWebXmlLoc; + if (this.jettyWebXmlLocation == null) { + jettyWebXmlLoc = "/WEB-INF/jetty-web.xml"; + } else { + jettyWebXmlLoc = this.jettyWebXmlLocation; + } + + URL jettyWebXml = bundleContext.getBundle().getResource(jettyWebXmlLoc); + if (jettyWebXml != null) { + log.debug("Found jetty-web XML configuration on bundle classpath on " + jettyWebXmlLoc); + service.registerJettyWebXml(jettyWebXml, httpContext); + } else { + log.debug("Not found jetty-web XML configuration on bundle classpath on " + jettyWebXmlLoc); + } + } + + protected void addConstraintMapping(WebContainer service, SecurityConstraintMappingModel constraintMapping) { + String name = constraintMapping.getConstraintName(); + if (name == null) { + name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE); + } + log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getUrl() + ", dataConstraint=" + constraintMapping.getDataConstraint() + ", canAuthenticate=" + + constraintMapping.isAuthentication() + ", roles=" + constraintMapping.getRoles()); + service.registerConstraintMapping(name, constraintMapping.getUrl(), constraintMapping.getMapping(), constraintMapping.getDataConstraint(), constraintMapping.isAuthentication(), constraintMapping.getRoles(), httpContext); + } + + protected void addConstraintMapping(WebContainer service, ConstraintMapping constraintMapping) { + Constraint constraint = constraintMapping.getConstraint(); + String[] roles = constraint.getRoles(); + // name property is unavailable on constraint object :/ + + String name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE); + + int dataConstraint = constraint.getDataConstraint(); + String dataConstraintStr; + switch (dataConstraint) { + case Constraint.DC_UNSET: dataConstraintStr = null; break; + case Constraint.DC_NONE: dataConstraintStr = "NONE"; break; + case Constraint.DC_CONFIDENTIAL: dataConstraintStr = "CONFIDENTIAL"; break; + case Constraint.DC_INTEGRAL: dataConstraintStr = "INTEGRAL"; break; + default: + log.warnv("Unknown data constraint: " + dataConstraint); + dataConstraintStr = "CONFIDENTIAL"; + } + List rolesList = Arrays.asList(roles); + + log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getPathSpec() + ", dataConstraint=" + dataConstraintStr + ", canAuthenticate=" + + constraint.getAuthenticate() + ", roles=" + rolesList); + service.registerConstraintMapping(name, constraintMapping.getPathSpec(), null, dataConstraintStr, constraint.getAuthenticate(), rolesList, httpContext); + } + + protected void removingWebContainerCallback(ServiceReference serviceReference) { + WebContainer service = (WebContainer)bundleContext.getService(serviceReference); + if (service != null) { + service.unregisterLoginConfig(httpContext); + service.unregisterConstraintMapping(httpContext); + } + } + + private interface ConstraintHandler { + boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm); + } + + private static class PaxWebConstraintHandler implements ConstraintHandler { + + public boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm) { + if (cm instanceof SecurityConstraintMappingModel) { + SecurityConstraintMappingModel constraintMapping = (SecurityConstraintMappingModel) cm; + String name = constraintMapping.getConstraintName(); + if (name == null) { + name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE); + } + log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getUrl() + ", dataConstraint=" + constraintMapping.getDataConstraint() + ", canAuthenticate=" + + constraintMapping.isAuthentication() + ", roles=" + constraintMapping.getRoles()); + service.registerConstraintMapping(name, constraintMapping.getUrl(), constraintMapping.getMapping(), constraintMapping.getDataConstraint(), constraintMapping.isAuthentication(), constraintMapping.getRoles(), httpContext); + return true; + } + return false; + } + + } + + private static class JettyConstraintHandler implements ConstraintHandler { + + public boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm) { + if (cm instanceof ConstraintMapping) { + ConstraintMapping constraintMapping = (ConstraintMapping) cm; + Constraint constraint = constraintMapping.getConstraint(); + String[] roles = constraint.getRoles(); + // name property is unavailable on constraint object :/ + + String name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE); + + int dataConstraint = constraint.getDataConstraint(); + String dataConstraintStr; + switch (dataConstraint) { + case Constraint.DC_UNSET: + dataConstraintStr = null; + break; + case Constraint.DC_NONE: + dataConstraintStr = "NONE"; + break; + case Constraint.DC_CONFIDENTIAL: + dataConstraintStr = "CONFIDENTIAL"; + break; + case Constraint.DC_INTEGRAL: + dataConstraintStr = "INTEGRAL"; + break; + default: + log.warnv("Unknown data constraint: " + dataConstraint); + dataConstraintStr = "CONFIDENTIAL"; + } + List rolesList = Arrays.asList(roles); + + log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getPathSpec() + ", dataConstraint=" + dataConstraintStr + ", canAuthenticate=" + + constraint.getAuthenticate() + ", roles=" + rolesList); + service.registerConstraintMapping(name, constraintMapping.getPathSpec(), null, dataConstraintStr, constraint.getAuthenticate(), rolesList, httpContext); + return true; + } + return false; + } + + } +} \ No newline at end of file diff --git a/adapters/oidc/fuse7/jetty94/src/main/resources/META-INF/services/org.ops4j.pax.web.service.AuthenticatorService b/adapters/oidc/fuse7/jetty94/src/main/resources/META-INF/services/org.ops4j.pax.web.service.AuthenticatorService new file mode 100644 index 0000000000..cce9788ce7 --- /dev/null +++ b/adapters/oidc/fuse7/jetty94/src/main/resources/META-INF/services/org.ops4j.pax.web.service.AuthenticatorService @@ -0,0 +1 @@ +org.keycloak.adapters.osgi.jetty94.KeycloakAuthenticatorService \ No newline at end of file diff --git a/adapters/oidc/fuse7/pom.xml b/adapters/oidc/fuse7/pom.xml index fad9726e48..6d90b8417f 100644 --- a/adapters/oidc/fuse7/pom.xml +++ b/adapters/oidc/fuse7/pom.xml @@ -38,6 +38,7 @@ camel-undertow + jetty94 undertow diff --git a/adapters/oidc/osgi-adapter/src/main/java/org/keycloak/adapters/osgi/PaxWebIntegrationService.java b/adapters/oidc/osgi-adapter/src/main/java/org/keycloak/adapters/osgi/PaxWebIntegrationService.java index 76cd62f9d5..2cfb4c8cda 100644 --- a/adapters/oidc/osgi-adapter/src/main/java/org/keycloak/adapters/osgi/PaxWebIntegrationService.java +++ b/adapters/oidc/osgi-adapter/src/main/java/org/keycloak/adapters/osgi/PaxWebIntegrationService.java @@ -34,7 +34,7 @@ import java.util.Arrays; import java.util.List; /** - * Integration with pax-web, which allows to inject custom jetty-web.xml configuration from current bundle classpath into {@link WebContainer} + * Integration with pax-web in Fuse 6.3, which allows to inject custom jetty-web.xml configuration from current bundle classpath into {@link WebContainer} * and allows to inject custom security constraint for securing resources by Keycloak. * *

It assumes that pax-web {@link WebContainer} is used as implementation of OSGI {@link org.osgi.service.http.HttpService}, which diff --git a/distribution/adapters/osgi/features/src/main/resources/features.xml b/distribution/adapters/osgi/features/src/main/resources/features.xml index c510990622..8899b34af4 100755 --- a/distribution/adapters/osgi/features/src/main/resources/features.xml +++ b/distribution/adapters/osgi/features/src/main/resources/features.xml @@ -62,6 +62,19 @@ mvn:org.keycloak/keycloak-jetty92-adapter/${project.version} + + +

Keycloak Pax-Web adapter for Undertow