From a457cded28b18e5dea75044e267cbae9b78b1338 Mon Sep 17 00:00:00 2001 From: Nishant Bangarwa Date: Fri, 3 Feb 2017 02:25:21 +0530 Subject: [PATCH] Druid Extension to enable Authentication using Kerberos. (#3853) * Add extension for supporting kerberos security - This PR adds an extension for supporting druid authentication via Kerberos. - Working on the docs. * Add docs * review comments * more review comments * Block all paths by default * more review comments - use proper Oid * Allow extensions to override httpclient for integration tests * Add kerberos lock to prevent multithreaded issues. * review comment - remove enabled flag and fix router injection * Add Cookie Handling and more detailed docs * review comment - rename DruidKerberosConfig -> AuthKerberosConfig * review comments * fix travis failure on jdk7 --- distribution/pom.xml | 2 + .../extensions-core/druid-kerberos.md | 71 +++++++ docs/content/development/extensions.md | 1 + extensions-core/druid-kerberos/pom.xml | 182 ++++++++++++++++++ .../AuthenticationKerberosConfig.java | 78 ++++++++ .../kerberos/DruidKerberosModule.java | 76 ++++++++ .../security/kerberos/DruidKerberosUtil.java | 148 ++++++++++++++ .../security/kerberos/KerberosHttpClient.java | 154 +++++++++++++++ .../kerberos/KerberosHttpClientProvider.java | 55 ++++++ .../KerberosJettyHttpClientProvider.java | 128 ++++++++++++ .../kerberos/ResponseCookieHandler.java | 92 +++++++++ .../RetryIfUnauthorizedResponseHandler.java | 104 ++++++++++ .../kerberos/RetryResponseHolder.java | 47 +++++ .../security/kerberos/SpnegoFilterConfig.java | 134 +++++++++++++ .../security/kerberos/SpnegoFilterHolder.java | 139 +++++++++++++ .../io.druid.initialization.DruidModule | 1 + .../AuthenticationKerberosConfigTest.java | 74 +++++++ .../kerberos/SpnegoFilterConfigTest.java | 75 ++++++++ .../test/resources/test.runtime.properties | 7 + .../druid/testing/guice/DruidTestModule.java | 22 +-- pom.xml | 1 + 21 files changed, 1574 insertions(+), 17 deletions(-) create mode 100644 docs/content/development/extensions-core/druid-kerberos.md create mode 100644 extensions-core/druid-kerberos/pom.xml create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/AuthenticationKerberosConfig.java create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/DruidKerberosModule.java create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/DruidKerberosUtil.java create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosHttpClient.java create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosHttpClientProvider.java create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosJettyHttpClientProvider.java create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/ResponseCookieHandler.java create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/RetryIfUnauthorizedResponseHandler.java create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/RetryResponseHolder.java create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/SpnegoFilterConfig.java create mode 100644 extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/SpnegoFilterHolder.java create mode 100644 extensions-core/druid-kerberos/src/main/resources/META-INF/services/io.druid.initialization.DruidModule create mode 100644 extensions-core/druid-kerberos/src/test/java/io/druid/security/kerberos/AuthenticationKerberosConfigTest.java create mode 100644 extensions-core/druid-kerberos/src/test/java/io/druid/security/kerberos/SpnegoFilterConfigTest.java create mode 100644 extensions-core/druid-kerberos/src/test/resources/test.runtime.properties diff --git a/distribution/pom.xml b/distribution/pom.xml index 044728d6291..0a832e3b1ea 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -106,6 +106,8 @@ io.druid.extensions:postgresql-metadata-storage -c io.druid.extensions.contrib:scan-query + -c + io.druid.extensions:druid-kerberos ${druid.distribution.pulldeps.opts} diff --git a/docs/content/development/extensions-core/druid-kerberos.md b/docs/content/development/extensions-core/druid-kerberos.md new file mode 100644 index 00000000000..44e67cce2c7 --- /dev/null +++ b/docs/content/development/extensions-core/druid-kerberos.md @@ -0,0 +1,71 @@ +--- +layout: doc_page +--- + +# Druid-Kerberos + +Druid Extension to enable Authentication for Druid Nodes using Kerberos. +This extension adds AuthenticationFilter which is used to protect HTTP Endpoints using the simple and protected GSSAPI negotiation mechanism [SPNEGO](https://en.wikipedia.org/wiki/SPNEGO). +Make sure to [include](../../operations/including-extensions.html) `druid-kerberos` as an extension. + + +## Configuration + +|Property|Possible Values|Description|Default|required| +|--------|---------------|-----------|-------|--------| +|`druid.hadoop.security.kerberos.principal`|`druid@EXAMPLE.COM`| Principal user name, used for internal node communication|empty|Yes| +|`druid.hadoop.security.kerberos.keytab`|`/etc/security/keytabs/druid.keytab`|Path to keytab file used for internal node communication|empty|Yes| +|`druid.hadoop.security.spnego.principal`|`HTTP/_HOST@EXAMPLE.COM`| SPNego service principal used by druid nodes|empty|Yes| +|`druid.hadoop.security.spnego.keytab`|`/etc/security/keytabs/spnego.service.keytab`|SPNego service keytab used by druid nodes|empty|Yes| +|`druid.hadoop.security.spnego.authToLocal`|`RULE:[1:$1@$0](druid@EXAMPLE.COM)s/.*/druid DEFAULT`|It allows you to set a general rule for mapping principal names to local user names. It will be used if there is not an explicit mapping for the principal name that is being translated.|DEFAULT|No| +|`druid.hadoop.security.spnego.excludedPaths`|`['/status','/health']`| Array of HTTP paths which which does NOT need to be authenticated.|None|No| +|`druid.hadoop.security.spnego.cookieSignatureSecret`|`secretString`| Secret used to sign authentication cookies. It is advisable to explicitly set it, if you have multiple druid ndoes running on same machine with different ports as the Cookie Specification does not guarantee isolation by port.||No| + +As a note, it is required that the SPNego principal in use by the druid nodes must start with HTTP (This specified by [RFC-4559](https://tools.ietf.org/html/rfc4559)) and must be of the form "HTTP/_HOST@REALM". +The special string _HOST will be replaced automatically with the value of config `druid.host` + +### Auth to Local Syntax + + +`druid.hadoop.security.spnego.authToLocal` allows you to set a general rules for mapping principal names to local user names. +The syntax for mapping rules is `RULE:\[n:string](regexp)s/pattern/replacement/g`. The integer n indicates how many components the target principal should have. If this matches, then a string will be formed from string, substituting the realm of the principal for $0 and the n‘th component of the principal for $n. e.g. if the principal was druid/admin then `\[2:$2$1suffix]` would result in the string `admindruidsuffix`. +If this string matches regexp, then the s//\[g] substitution command will be run over the string. The optional g will cause the substitution to be global over the string, instead of replacing only the first match in the string. +If required, multiple rules can be be joined by newline character and specified as a String. + +## Accessing Druid HTTP end points when kerberos security is enabled +1. To access druid HTTP endpoints via curl user will need to first login using `kinit` command as follows - + + ``` + kinit -k -t user@REALM.COM + ``` + +2. Once the login is successful verify that login is successful using `klist` command +3. Now you can access druid HTTP endpoints using curl command as follows - + + ``` + curl --negotiate -u:anyUser -b ~/cookies.txt -c ~/cookies.txt -X POST -H'Content-Type: application/json' + ``` + + e.g to send a query from file `query.json` to druid broker use this command - + + ``` + curl --negotiate -u:anyUser -b ~/cookies.txt -c ~/cookies.txt -X POST -H'Content-Type: application/json' http://broker-host:port/druid/v2/?pretty -d @query.json + ``` + Note: Above command will authenticate the user first time using SPNego negotiate mechanism and store the authentication cookie in file. For subsequent requests the cookie will be used for authentication. + +## Accessing coordinator or overlord console from web browser +To access Coordinator/Overlord console from browser you will need to configure your browser for SPNego authentication as follows - + +1. Safari - No configurations required. +2. Firefox - Open firefox and follow these steps - + 1. Go to `about:config` and search for `network.negotiate-auth.trusted-uris`. + 2. Double-click and add the following values: `"http://druid-coordinator-hostname:ui-port"` and `"http://druid-overlord-hostname:port"` +3. Google Chrome - From the command line run following commands - + 1. `google-chrome --auth-server-whitelist="druid-coordinator-hostname" --auth-negotiate-delegate-whitelist="druid-coordinator-hostname"` + 2. `google-chrome --auth-server-whitelist="druid-overlord-hostname" --auth-negotiate-delegate-whitelist="druid-overlord-hostname"` +4. Internet Explorer - + 1. Configure trusted websites to include `"druid-coordinator-hostname"` and `"druid-overlord-hostname"` + 2. Allow negotiation for the UI website. + +## Sending Queries programmatically +Many HTTP client libraries, such as Apache Commons [HttpComponents](https://hc.apache.org/), already have support for performing SPNEGO authentication. You can use any of the available HTTP client library to communicate with druid cluster. diff --git a/docs/content/development/extensions.md b/docs/content/development/extensions.md index 6a2e996ffd4..45c116d22b0 100644 --- a/docs/content/development/extensions.md +++ b/docs/content/development/extensions.md @@ -29,6 +29,7 @@ Core extensions are maintained by Druid committers. |druid-kafka-eight|Kafka ingest firehose (high level consumer) for realtime nodes.|[link](../development/extensions-core/kafka-eight-firehose.html)| |druid-kafka-extraction-namespace|Kafka-based namespaced lookup. Requires namespace lookup extension.|[link](../development/extensions-core/kafka-extraction-namespace.html)| |druid-kafka-indexing-service|Supervised exactly-once Kafka ingestion for the indexing service.|[link](../development/extensions-core/kafka-ingestion.html)| +|druid-kerberos|Kerberos authentication for druid nodes.|[link](../development/extensions-core/druid-kerberos.html)| |druid-lookups-cached-global|A module for [lookups](../querying/lookups.html) providing a jvm-global eager caching for lookups. It provides JDBC and URI implementations for fetching lookup data.|[link](../development/extensions-core/lookups-cached-global.html)| |druid-lookups-cached-single| Per lookup caching module to support the use cases where a lookup need to be isolated from the global pool of lookups |[link](../development/extensions-core/druid-lookups.html)| |druid-s3-extensions|Interfacing with data in AWS S3, and using S3 as deep storage.|[link](../development/extensions-core/s3.html)| diff --git a/extensions-core/druid-kerberos/pom.xml b/extensions-core/druid-kerberos/pom.xml new file mode 100644 index 00000000000..74ba2b109fb --- /dev/null +++ b/extensions-core/druid-kerberos/pom.xml @@ -0,0 +1,182 @@ + + + + + 4.0.0 + + io.druid.extensions + druid-kerberos + druid-kerberos + druid-kerberos + + + io.druid + druid + 0.9.3-SNAPSHOT + ../../pom.xml + + + + + io.druid + druid-processing + ${project.parent.version} + provided + + + io.druid + druid-server + ${project.parent.version} + provided + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-util + + + org.eclipse.jetty + jetty-proxy + + + org.eclipse.jetty + jetty-servlet + + + org.eclipse.jetty + jetty-servlets + + + org.apache.hadoop + hadoop-common + ${hadoop.compile.version} + + + commons-cli + commons-cli + + + commons-httpclient + commons-httpclient + + + log4j + log4j + + + commons-codec + commons-codec + + + commons-logging + commons-logging + + + commons-io + commons-io + + + commons-lang + commons-lang + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-mapper-asl + + + org.apache.zookeeper + zookeeper + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-log4j12 + + + javax.ws.rs + jsr311-api + + + com.google.code.findbugs + jsr305 + + + org.mortbay.jetty + jetty-util + + + org.apache.hadoop + hadoop-annotations + + + javax.activation + activation + + + com.google.protobuf + protobuf-java + + + com.sun.jersey + jersey-core + + + + + + + io.druid + druid-processing + ${project.parent.version} + test + test-jar + + + junit + junit + test + + + org.easymock + easymock + test + + + + diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/AuthenticationKerberosConfig.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/AuthenticationKerberosConfig.java new file mode 100644 index 00000000000..7ed11fe59cc --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/AuthenticationKerberosConfig.java @@ -0,0 +1,78 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AuthenticationKerberosConfig +{ + @JsonProperty + private final String principal; + @JsonProperty + private final String keytab; + + @JsonCreator + public AuthenticationKerberosConfig(@JsonProperty("principal") String principal, @JsonProperty("keytab") String keytab) + { + this.principal = principal; + this.keytab = keytab; + } + + @JsonProperty + public String getPrincipal() + { + return principal; + } + + @JsonProperty + public String getKeytab() + { + return keytab; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (!(o instanceof AuthenticationKerberosConfig)) { + return false; + } + + AuthenticationKerberosConfig that = (AuthenticationKerberosConfig) o; + + if (getPrincipal() != null ? !getPrincipal().equals(that.getPrincipal()) : that.getPrincipal() != null) { + return false; + } + return getKeytab() != null ? getKeytab().equals(that.getKeytab()) : that.getKeytab() == null; + + } + + @Override + public int hashCode() + { + int result = getPrincipal() != null ? getPrincipal().hashCode() : 0; + result = 31 * result + (getKeytab() != null ? getKeytab().hashCode() : 0); + return result; + } +} diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/DruidKerberosModule.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/DruidKerberosModule.java new file mode 100644 index 00000000000..30d4df8b5ef --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/DruidKerberosModule.java @@ -0,0 +1,76 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +import com.fasterxml.jackson.databind.Module; +import com.google.common.collect.ImmutableList; +import com.google.inject.Binder; +import com.google.inject.multibindings.Multibinder; +import com.metamx.http.client.HttpClient; +import io.druid.guice.JsonConfigProvider; +import io.druid.guice.LazySingleton; +import io.druid.guice.annotations.Client; +import io.druid.guice.annotations.Global; +import io.druid.guice.http.HttpClientModule; +import io.druid.guice.http.JettyHttpClientModule; +import io.druid.initialization.DruidModule; +import io.druid.server.initialization.jetty.ServletFilterHolder; +import io.druid.server.router.Router; + +import java.util.List; + +/** + */ +public class DruidKerberosModule implements DruidModule +{ + + @Override + public List getJacksonModules() + { + return ImmutableList.of( + ); + } + + @Override + public void configure(Binder binder) + { + JsonConfigProvider.bind(binder, "druid.hadoop.security.kerberos", AuthenticationKerberosConfig.class); + JsonConfigProvider.bind(binder, "druid.hadoop.security.spnego", SpnegoFilterConfig.class); + + Multibinder.newSetBinder(binder, ServletFilterHolder.class) + .addBinding() + .to(SpnegoFilterHolder.class); + + binder.bind(HttpClient.class) + .annotatedWith(Global.class) + .toProvider(new KerberosHttpClientProvider(new HttpClientModule.HttpClientProvider(Global.class))) + .in(LazySingleton.class); + + binder.bind(HttpClient.class) + .annotatedWith(Client.class) + .toProvider(new KerberosHttpClientProvider(new HttpClientModule.HttpClientProvider(Client.class))) + .in(LazySingleton.class); + + binder.bind(org.eclipse.jetty.client.HttpClient.class) + .annotatedWith(Router.class) + .toProvider(new KerberosJettyHttpClientProvider(new JettyHttpClientModule.HttpClientProvider(Router.class))) + .in(LazySingleton.class); + } +} diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/DruidKerberosUtil.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/DruidKerberosUtil.java new file mode 100644 index 00000000000..d9596c8b474 --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/DruidKerberosUtil.java @@ -0,0 +1,148 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +import com.google.common.base.Strings; +import io.druid.java.util.common.ISE; +import io.druid.java.util.common.logger.Logger; +import org.apache.commons.codec.binary.Base64; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.authentication.client.AuthenticatedURL; +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.security.authentication.util.KerberosUtil; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +import java.io.IOException; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.URI; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +public class DruidKerberosUtil +{ + private static final Logger log = new Logger(DruidKerberosUtil.class); + + private static final Base64 base64codec = new Base64(0); + + // A fair reentrant lock + private static ReentrantLock kerberosLock = new ReentrantLock(true); + + /** + * This method always needs to be called within a doAs block so that the client's TGT credentials + * can be read from the Subject. + * + * @return Kerberos Challenge String + * + * @throws Exception + */ + + public static String kerberosChallenge(String server) throws AuthenticationException + { + kerberosLock.lock(); + try { + // This Oid for Kerberos GSS-API mechanism. + Oid mechOid = KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID"); + GSSManager manager = GSSManager.getInstance(); + // GSS name for server + GSSName serverName = manager.createName("HTTP@" + server, GSSName.NT_HOSTBASED_SERVICE); + // Create a GSSContext for authentication with the service. + // We're passing client credentials as null since we want them to be read from the Subject. + GSSContext gssContext = + manager.createContext(serverName.canonicalize(mechOid), mechOid, null, GSSContext.DEFAULT_LIFETIME); + gssContext.requestMutualAuth(true); + gssContext.requestCredDeleg(true); + // Establish context + byte[] inToken = new byte[0]; + byte[] outToken = gssContext.initSecContext(inToken, 0, inToken.length); + gssContext.dispose(); + // Base64 encoded and stringified token for server + return new String(base64codec.encode(outToken)); + } + catch (GSSException | IllegalAccessException | NoSuchFieldException | ClassNotFoundException e) { + throw new AuthenticationException(e); + } + finally { + kerberosLock.unlock(); + } + } + + public static void authenticateIfRequired(AuthenticationKerberosConfig config) + throws IOException + { + String principal = config.getPrincipal(); + String keytab = config.getKeytab(); + if (!Strings.isNullOrEmpty(principal) && !Strings.isNullOrEmpty(keytab)) { + Configuration conf = new Configuration(); + conf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos"); + UserGroupInformation.setConfiguration(conf); + try { + if (UserGroupInformation.getCurrentUser().hasKerberosCredentials() == false + || !UserGroupInformation.getCurrentUser().getUserName().equals(principal)) { + log.info("trying to authenticate user [%s] with keytab [%s]", principal, keytab); + UserGroupInformation.loginUserFromKeytab(principal, keytab); + } + } + catch (IOException e) { + throw new ISE(e, "Failed to authenticate user principal [%s] with keytab [%s]", principal, keytab); + } + } + } + + public static boolean needToSendCredentials(CookieStore cookieStore, URI uri){ + return getAuthCookie(cookieStore, uri) == null; + } + + public static HttpCookie getAuthCookie(CookieStore cookieStore, URI uri) + { + if (cookieStore == null) { + return null; + } + boolean isSSL = uri.getScheme().equals("https"); + List cookies = cookieStore.getCookies(); + + for (HttpCookie c : cookies) { + // If this is a secured cookie and the current connection is non-secured, + // then, skip this cookie. We need to skip this cookie because, the cookie + // replay will not be transmitted to the server. + if (c.getSecure() && !isSSL) { + continue; + } + if (c.getName().equals(AuthenticatedURL.AUTH_COOKIE)) { + return c; + } + } + return null; + } + + public static void removeAuthCookie(CookieStore cookieStore, URI uri) + { + HttpCookie authCookie = getAuthCookie(cookieStore, uri); + if (authCookie != null) { + cookieStore.remove(uri, authCookie); + } + } +} diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosHttpClient.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosHttpClient.java new file mode 100644 index 00000000000..4930006b181 --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosHttpClient.java @@ -0,0 +1,154 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +import com.google.common.base.Throwables; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.metamx.http.client.AbstractHttpClient; +import com.metamx.http.client.HttpClient; +import com.metamx.http.client.Request; +import com.metamx.http.client.response.HttpResponseHandler; +import io.druid.concurrent.Execs; +import io.druid.java.util.common.logger.Logger; +import org.apache.hadoop.security.UserGroupInformation; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.joda.time.Duration; + +import java.net.CookieManager; +import java.net.URI; +import java.security.PrivilegedExceptionAction; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +public class KerberosHttpClient extends AbstractHttpClient +{ + private static final Logger log = new Logger(KerberosHttpClient.class); + + private final HttpClient delegate; + private final AuthenticationKerberosConfig config; + private final CookieManager cookieManager; + private final Executor exec = Execs.singleThreaded("test-%s"); + + public KerberosHttpClient(HttpClient delegate, AuthenticationKerberosConfig config) + { + this.delegate = delegate; + this.config = config; + this.cookieManager = new CookieManager(); + } + + @Override + public ListenableFuture go( + Request request, HttpResponseHandler httpResponseHandler, Duration duration + ) + { + final SettableFuture retVal = SettableFuture.create(); + inner_go(request, httpResponseHandler, duration, retVal); + return retVal; + } + + + private void inner_go( + final Request request, + final HttpResponseHandler httpResponseHandler, + final Duration duration, + final SettableFuture future + ) + { + try { + final String host = request.getUrl().getHost(); + final URI uri = request.getUrl().toURI(); + + + Map> cookieMap = cookieManager.get(uri, Collections.>emptyMap()); + for (Map.Entry> entry : cookieMap.entrySet()) { + request.addHeaderValues(entry.getKey(), entry.getValue()); + } + final boolean should_retry_on_unauthorized_response; + + if (DruidKerberosUtil.needToSendCredentials(cookieManager.getCookieStore(), uri)) { + // No Cookies for requested URI, authenticate user and add authentication header + log.debug( + "No Auth Cookie found for URI[%s]. Existing Cookies[%s] Authenticating... ", + uri, + cookieManager.getCookieStore().getCookies() + ); + DruidKerberosUtil.authenticateIfRequired(config); + UserGroupInformation currentUser = UserGroupInformation.getCurrentUser(); + String challenge = currentUser.doAs(new PrivilegedExceptionAction() + { + @Override + public String run() throws Exception + { + return DruidKerberosUtil.kerberosChallenge(host); + } + }); + request.setHeader(HttpHeaders.Names.AUTHORIZATION, "Negotiate " + challenge); + should_retry_on_unauthorized_response = false; + } else { + should_retry_on_unauthorized_response = true; + log.debug("Found Auth Cookie found for URI[%s].", uri); + } + + ListenableFuture> internalFuture = delegate.go( + request, + new RetryIfUnauthorizedResponseHandler(new ResponseCookieHandler( + request.getUrl().toURI(), + cookieManager, + httpResponseHandler + )), + duration + ); + + Futures.addCallback(internalFuture, new FutureCallback>() + { + @Override + public void onSuccess(RetryResponseHolder result) + { + if (should_retry_on_unauthorized_response && result.shouldRetry()) { + log.info("Preparing for Retry"); + // remove Auth cookie + DruidKerberosUtil.removeAuthCookie(cookieManager.getCookieStore(), uri); + // clear existing cookie + request.setHeader("Cookie", ""); + inner_go(request.copy(), httpResponseHandler, duration, future); + } else { + log.info("Not retrying and returning future response"); + future.set(result.getObj()); + } + } + + @Override + public void onFailure(Throwable t) + { + future.setException(t); + } + }, exec); + } + catch (Throwable e) { + throw Throwables.propagate(e); + } + } + +} diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosHttpClientProvider.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosHttpClientProvider.java new file mode 100644 index 00000000000..6d4cb623456 --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosHttpClientProvider.java @@ -0,0 +1,55 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Provider; +import com.metamx.http.client.HttpClient; +import io.druid.guice.http.AbstractHttpClientProvider; + +public class KerberosHttpClientProvider extends AbstractHttpClientProvider +{ + private final Provider delegateProvider; + private AuthenticationKerberosConfig config; + + public KerberosHttpClientProvider( + Provider delegateProvider + ) + { + this.delegateProvider = delegateProvider; + } + + @Inject + @Override + public void configure(Injector injector) + { + if (delegateProvider instanceof AbstractHttpClientProvider) { + ((AbstractHttpClientProvider) delegateProvider).configure(injector); + } + config = injector.getInstance(AuthenticationKerberosConfig.class); + } + + @Override + public HttpClient get() + { + return new KerberosHttpClient(delegateProvider.get(), config); + } +} diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosJettyHttpClientProvider.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosJettyHttpClientProvider.java new file mode 100644 index 00000000000..cb8575bf48f --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/KerberosJettyHttpClientProvider.java @@ -0,0 +1,128 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + + +import com.google.common.base.Throwables; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Provider; +import io.druid.guice.http.AbstractHttpClientProvider; +import io.druid.java.util.common.logger.Logger; +import org.apache.hadoop.security.UserGroupInformation; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.util.Attributes; +import org.jboss.netty.handler.codec.http.HttpHeaders; + +import java.net.URI; +import java.security.PrivilegedExceptionAction; + +public class KerberosJettyHttpClientProvider extends AbstractHttpClientProvider +{ + private static final Logger log = new Logger(KerberosJettyHttpClientProvider.class); + + private final Provider delegateProvider; + private AuthenticationKerberosConfig config; + + + public KerberosJettyHttpClientProvider( + Provider delegateProvider + ) + { + this.delegateProvider = delegateProvider; + } + + @Inject + @Override + public void configure(Injector injector) + { + if (delegateProvider instanceof AbstractHttpClientProvider) { + ((AbstractHttpClientProvider) delegateProvider).configure(injector); + } + config = injector.getInstance(AuthenticationKerberosConfig.class); + } + + + @Override + public HttpClient get() + { + final HttpClient httpClient = delegateProvider.get(); + httpClient.getAuthenticationStore().addAuthentication(new Authentication() + { + @Override + public boolean matches(String type, URI uri, String realm) + { + return true; + } + + @Override + public Result authenticate( + final Request request, ContentResponse response, Authentication.HeaderInfo headerInfo, Attributes context + ) + { + return new Result() + { + @Override + public URI getURI() + { + return request.getURI(); + } + + @Override + public void apply(Request request) + { + try { + // No need to set cookies as they are handled by Jetty Http Client itself. + URI uri = request.getURI(); + if (DruidKerberosUtil.needToSendCredentials(httpClient.getCookieStore(), uri)) { + log.debug( + "No Auth Cookie found for URI[%s]. Existing Cookies[%s] Authenticating... ", + uri, + httpClient.getCookieStore().getCookies() + ); + final String host = request.getHost(); + DruidKerberosUtil.authenticateIfRequired(config); + UserGroupInformation currentUser = UserGroupInformation.getCurrentUser(); + String challenge = currentUser.doAs(new PrivilegedExceptionAction() + { + @Override + public String run() throws Exception + { + return DruidKerberosUtil.kerberosChallenge(host); + } + }); + request.getHeaders().add(HttpHeaders.Names.AUTHORIZATION, "Negotiate " + challenge); + } else { + log.debug("Found Auth Cookie found for URI[%s].", uri); + } + } + catch (Throwable e) { + Throwables.propagate(e); + } + } + }; + } + }); + return httpClient; + } +} diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/ResponseCookieHandler.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/ResponseCookieHandler.java new file mode 100644 index 00000000000..4e2bed41904 --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/ResponseCookieHandler.java @@ -0,0 +1,92 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +import com.google.common.base.Function; +import com.google.common.collect.Maps; +import com.metamx.http.client.response.ClientResponse; +import com.metamx.http.client.response.HttpResponseHandler; +import io.druid.java.util.common.logger.Logger; +import org.jboss.netty.handler.codec.http.HttpChunk; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpResponse; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.URI; +import java.util.List; + +public class ResponseCookieHandler implements HttpResponseHandler +{ + private static final Logger log = new Logger(ResponseCookieHandler.class); + + private final URI uri; + private final CookieManager manager; + private final HttpResponseHandler delegate; + + public ResponseCookieHandler(URI uri, CookieManager manager, HttpResponseHandler delegate) + { + this.uri = uri; + this.manager = manager; + this.delegate = delegate; + } + + @Override + public ClientResponse handleResponse(HttpResponse httpResponse) + { + try { + final HttpHeaders headers = httpResponse.headers(); + manager.put(uri, Maps.asMap(headers.names(), new Function>() + { + @Override + public List apply(String input) + { + return headers.getAll(input); + } + })); + } + catch (IOException e) { + log.error(e, "Error while processing Cookies from header"); + } + finally { + return delegate.handleResponse(httpResponse); + } + } + + @Override + public ClientResponse handleChunk( + ClientResponse clientResponse, HttpChunk httpChunk + ) + { + return delegate.handleChunk(clientResponse, httpChunk); + } + + @Override + public ClientResponse done(ClientResponse clientResponse) + { + return delegate.done(clientResponse); + } + + @Override + public void exceptionCaught(ClientResponse clientResponse, Throwable throwable) + { + delegate.exceptionCaught(clientResponse, throwable); + } +} diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/RetryIfUnauthorizedResponseHandler.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/RetryIfUnauthorizedResponseHandler.java new file mode 100644 index 00000000000..a2f960d9b5d --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/RetryIfUnauthorizedResponseHandler.java @@ -0,0 +1,104 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +import com.metamx.http.client.response.ClientResponse; +import com.metamx.http.client.response.HttpResponseHandler; +import io.druid.java.util.common.logger.Logger; +import org.jboss.netty.handler.codec.http.HttpChunk; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; + +public class RetryIfUnauthorizedResponseHandler + implements HttpResponseHandler, RetryResponseHolder> +{ + private static final Logger log = new Logger(RetryIfUnauthorizedResponseHandler.class); + private final HttpResponseHandler httpResponseHandler; + + + public RetryIfUnauthorizedResponseHandler( + HttpResponseHandler httpResponseHandler + ) + { + this.httpResponseHandler = httpResponseHandler; + } + + @Override + public ClientResponse> handleResponse(HttpResponse httpResponse) + { + log.debug("UnauthorizedResponseHandler - Got response status [%s]", httpResponse.getStatus()); + if (httpResponse.getStatus().equals(HttpResponseStatus.UNAUTHORIZED)) { + // Drain the buffer + httpResponse.getContent().toString(); + return ClientResponse.unfinished(RetryResponseHolder.retry()); + } else { + return wrap(httpResponseHandler.handleResponse(httpResponse)); + } + } + + @Override + public ClientResponse> handleChunk( + ClientResponse> clientResponse, HttpChunk httpChunk + ) + { + if (clientResponse.getObj().shouldRetry()) { + httpChunk.getContent().toString(); + return clientResponse; + } else { + return wrap(httpResponseHandler.handleChunk(unwrap(clientResponse), httpChunk)); + } + } + + @Override + public ClientResponse> done(ClientResponse> clientResponse) + { + if (clientResponse.getObj().shouldRetry()) { + return ClientResponse.finished(RetryResponseHolder.retry()); + } else { + return wrap(httpResponseHandler.done(unwrap(clientResponse))); + } + } + + @Override + public void exceptionCaught(ClientResponse> clientResponse, Throwable throwable) + { + httpResponseHandler.exceptionCaught(unwrap(clientResponse), throwable); + } + + private ClientResponse> wrap(ClientResponse response) + { + if (response.isFinished()) { + return ClientResponse.finished(new RetryResponseHolder(false, response.getObj())); + } else { + return ClientResponse.unfinished(new RetryResponseHolder(false, response.getObj())); + } + } + + private ClientResponse unwrap(ClientResponse> response) + { + if (response.isFinished()) { + return ClientResponse.finished(response.getObj().getObj()); + } else { + return ClientResponse.unfinished(response.getObj().getObj()); + } + } + + +} diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/RetryResponseHolder.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/RetryResponseHolder.java new file mode 100644 index 00000000000..ba6863b898d --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/RetryResponseHolder.java @@ -0,0 +1,47 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +public class RetryResponseHolder +{ + private final boolean shouldRetry; + private final T obj; + + public RetryResponseHolder(boolean shouldRetry, T obj) + { + this.shouldRetry = shouldRetry; + this.obj = obj; + } + + public static RetryResponseHolder retry() + { + return new RetryResponseHolder(true, null); + } + + public boolean shouldRetry() + { + return shouldRetry; + } + + public T getObj() + { + return obj; + } +} diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/SpnegoFilterConfig.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/SpnegoFilterConfig.java new file mode 100644 index 00000000000..9ce8f6ab098 --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/SpnegoFilterConfig.java @@ -0,0 +1,134 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Collections; +import java.util.List; + +public class SpnegoFilterConfig +{ + + public static final List DEFAULT_EXCLUDED_PATHS = Collections.emptyList(); + + @JsonProperty + private final String principal; + + @JsonProperty + private final String keytab; + + @JsonProperty + private final String authToLocal; + + @JsonProperty + private final List excludedPaths; + + @JsonProperty + private final String cookieSignatureSecret; + + @JsonCreator + public SpnegoFilterConfig( + @JsonProperty("principal") String principal, + @JsonProperty("keytab") String keytab, + @JsonProperty("authToLocal") String authToLocal, + @JsonProperty("excludedPaths") List excludedPaths, + @JsonProperty("cookieSignatureSecret") String cookieSignatureSecret + ) + { + this.principal = principal; + this.keytab = keytab; + this.authToLocal = authToLocal == null ? "DEFAULT" : authToLocal; + this.excludedPaths = excludedPaths == null ? DEFAULT_EXCLUDED_PATHS : excludedPaths; + this.cookieSignatureSecret = cookieSignatureSecret; + } + + @JsonProperty + public String getPrincipal() + { + return principal; + } + + @JsonProperty + public String getKeytab() + { + return keytab; + } + + @JsonProperty + public String getAuthToLocal() + { + return authToLocal; + } + + @JsonProperty + public List getExcludedPaths() + { + return excludedPaths; + } + + @JsonProperty + public String getCookieSignatureSecret() + { + return cookieSignatureSecret; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SpnegoFilterConfig that = (SpnegoFilterConfig) o; + + if (principal != null ? !principal.equals(that.principal) : that.principal != null) { + return false; + } + if (keytab != null ? !keytab.equals(that.keytab) : that.keytab != null) { + return false; + } + if (authToLocal != null ? !authToLocal.equals(that.authToLocal) : that.authToLocal != null) { + return false; + } + if (excludedPaths != null ? !excludedPaths.equals(that.excludedPaths) : that.excludedPaths != null) { + return false; + } + return cookieSignatureSecret != null + ? cookieSignatureSecret.equals(that.cookieSignatureSecret) + : that.cookieSignatureSecret == null; + + } + + @Override + public int hashCode() + { + int result = principal != null ? principal.hashCode() : 0; + result = 31 * result + (keytab != null ? keytab.hashCode() : 0); + result = 31 * result + (authToLocal != null ? authToLocal.hashCode() : 0); + result = 31 * result + (excludedPaths != null ? excludedPaths.hashCode() : 0); + result = 31 * result + (cookieSignatureSecret != null ? cookieSignatureSecret.hashCode() : 0); + return result; + } +} diff --git a/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/SpnegoFilterHolder.java b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/SpnegoFilterHolder.java new file mode 100644 index 00000000000..ab2a3b6d4f4 --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/java/io/druid/security/kerberos/SpnegoFilterHolder.java @@ -0,0 +1,139 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +import com.google.common.base.Throwables; +import com.google.inject.Inject; +import io.druid.guice.annotations.Self; +import io.druid.server.DruidNode; +import io.druid.server.initialization.jetty.ServletFilterHolder; +import org.apache.hadoop.security.SecurityUtil; +import org.apache.hadoop.security.authentication.server.AuthenticationFilter; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +public class SpnegoFilterHolder implements ServletFilterHolder +{ + private final SpnegoFilterConfig config; + private final DruidNode node; + + @Inject + public SpnegoFilterHolder(SpnegoFilterConfig config, @Self DruidNode node) + { + this.config = config; + this.node = node; + } + + @Override + public Filter getFilter() + { + return new AuthenticationFilter() + { + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + ClassLoader prevLoader = Thread.currentThread().getContextClassLoader(); + try { + // AuthenticationHandler is created during Authenticationfilter.init using reflection with thread context class loader. + // In case of druid since the class is actually loaded as an extension and filter init is done in main thread. + // We need to set the classloader explicitly to extension class loader. + Thread.currentThread().setContextClassLoader(AuthenticationFilter.class.getClassLoader()); + super.init(filterConfig); + } + finally { + Thread.currentThread().setContextClassLoader(prevLoader); + } + } + + @Override + public void doFilter( + ServletRequest request, ServletResponse response, FilterChain filterChain + ) throws IOException, ServletException + { + String path = ((HttpServletRequest) request).getRequestURI(); + if (isExcluded(path)) { + filterChain.doFilter(request, response); + } + super.doFilter(request, response, filterChain); + } + }; + } + + private boolean isExcluded(String path) + { + for (String excluded : config.getExcludedPaths()) { + if (path.startsWith(excluded)) { + return true; + } + } + return false; + } + + @Override + public Class getFilterClass() + { + return null; + } + + @Override + public Map getInitParameters() + { + Map params = new HashMap(); + try { + params.put( + "kerberos.principal", + SecurityUtil.getServerPrincipal(config.getPrincipal(), node.getHost()) + ); + params.put("kerberos.keytab", config.getKeytab()); + params.put(AuthenticationFilter.AUTH_TYPE, "kerberos"); + params.put("kerberos.name.rules", config.getAuthToLocal()); + if (config.getCookieSignatureSecret() != null) { + params.put("signature.secret", config.getCookieSignatureSecret()); + } + } + catch (IOException e) { + Throwables.propagate(e); + } + return params; + } + + @Override + public String getPath() + { + return "/*"; + } + + @Override + public EnumSet getDispatcherType() + { + return null; + } +} diff --git a/extensions-core/druid-kerberos/src/main/resources/META-INF/services/io.druid.initialization.DruidModule b/extensions-core/druid-kerberos/src/main/resources/META-INF/services/io.druid.initialization.DruidModule new file mode 100644 index 00000000000..c1d9b227def --- /dev/null +++ b/extensions-core/druid-kerberos/src/main/resources/META-INF/services/io.druid.initialization.DruidModule @@ -0,0 +1 @@ +io.druid.security.kerberos.DruidKerberosModule diff --git a/extensions-core/druid-kerberos/src/test/java/io/druid/security/kerberos/AuthenticationKerberosConfigTest.java b/extensions-core/druid-kerberos/src/test/java/io/druid/security/kerberos/AuthenticationKerberosConfigTest.java new file mode 100644 index 00000000000..23551303446 --- /dev/null +++ b/extensions-core/druid-kerberos/src/test/java/io/druid/security/kerberos/AuthenticationKerberosConfigTest.java @@ -0,0 +1,74 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.inject.Binder; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Provides; +import io.druid.guice.ConfigModule; +import io.druid.guice.DruidGuiceExtensions; +import io.druid.guice.JsonConfigProvider; +import io.druid.guice.LazySingleton; +import io.druid.guice.PropertiesModule; +import io.druid.jackson.DefaultObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Properties; + +public class AuthenticationKerberosConfigTest +{ + @Test + public void testserde() + { + Injector injector = Guice.createInjector( + new Module() + { + @Override + public void configure(Binder binder) + { + binder.install(new PropertiesModule(Arrays.asList("test.runtime.properties"))); + binder.install(new ConfigModule()); + binder.install(new DruidGuiceExtensions()); + JsonConfigProvider.bind(binder, "druid.hadoop.security.kerberos", AuthenticationKerberosConfig.class); + } + + @Provides + @LazySingleton + public ObjectMapper jsonMapper() + { + return new DefaultObjectMapper(); + } + } + ); + + Properties props = injector.getInstance(Properties.class); + AuthenticationKerberosConfig config = injector.getInstance(AuthenticationKerberosConfig.class); + + Assert.assertEquals(props.getProperty("druid.hadoop.security.kerberos.principal"), config.getPrincipal()); + Assert.assertEquals(props.getProperty("druid.hadoop.security.kerberos.keytab"), config.getKeytab()); + + + } +} diff --git a/extensions-core/druid-kerberos/src/test/java/io/druid/security/kerberos/SpnegoFilterConfigTest.java b/extensions-core/druid-kerberos/src/test/java/io/druid/security/kerberos/SpnegoFilterConfigTest.java new file mode 100644 index 00000000000..1c63d09f1da --- /dev/null +++ b/extensions-core/druid-kerberos/src/test/java/io/druid/security/kerberos/SpnegoFilterConfigTest.java @@ -0,0 +1,75 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.security.kerberos; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.inject.Binder; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Provides; +import io.druid.guice.ConfigModule; +import io.druid.guice.DruidGuiceExtensions; +import io.druid.guice.JsonConfigProvider; +import io.druid.guice.LazySingleton; +import io.druid.guice.PropertiesModule; +import io.druid.jackson.DefaultObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Properties; + +public class SpnegoFilterConfigTest +{ + @Test + public void testserde() + { + Injector injector = Guice.createInjector( + new Module() + { + @Override + public void configure(Binder binder) + { + binder.install(new PropertiesModule(Arrays.asList("test.runtime.properties"))); + binder.install(new ConfigModule()); + binder.install(new DruidGuiceExtensions()); + JsonConfigProvider.bind(binder, "druid.hadoop.security.spnego", SpnegoFilterConfig.class); + } + + @Provides + @LazySingleton + public ObjectMapper jsonMapper() + { + return new DefaultObjectMapper(); + } + } + ); + + Properties props = injector.getInstance(Properties.class); + SpnegoFilterConfig config = injector.getInstance(SpnegoFilterConfig.class); + + Assert.assertEquals(props.getProperty("druid.hadoop.security.spnego.principal"), config.getPrincipal()); + Assert.assertEquals(props.getProperty("druid.hadoop.security.spnego.keytab"), config.getKeytab()); + Assert.assertEquals(props.getProperty("druid.hadoop.security.spnego.authToLocal"), config.getAuthToLocal()); + + + } +} diff --git a/extensions-core/druid-kerberos/src/test/resources/test.runtime.properties b/extensions-core/druid-kerberos/src/test/resources/test.runtime.properties new file mode 100644 index 00000000000..edcece1ffb7 --- /dev/null +++ b/extensions-core/druid-kerberos/src/test/resources/test.runtime.properties @@ -0,0 +1,7 @@ +druid.hadoop.security.kerberos.principal=testPrincipal +druid.hadoop.security.kerberos.keytab=testKeytab +druid.hadoop.security.spnego.principal=spnegoPrincipal +druid.hadoop.security.spnego.keytab=spnegoKeytab +druid.hadoop.security.spnego.authToLocal=testAuthToLocal + + diff --git a/integration-tests/src/main/java/io/druid/testing/guice/DruidTestModule.java b/integration-tests/src/main/java/io/druid/testing/guice/DruidTestModule.java index 5755161e2ed..ee7cda2e684 100644 --- a/integration-tests/src/main/java/io/druid/testing/guice/DruidTestModule.java +++ b/integration-tests/src/main/java/io/druid/testing/guice/DruidTestModule.java @@ -30,20 +30,16 @@ import com.metamx.emitter.core.LoggingEmitterConfig; import com.metamx.emitter.service.ServiceEmitter; import com.metamx.http.client.CredentialedHttpClient; import com.metamx.http.client.HttpClient; -import com.metamx.http.client.HttpClientConfig; -import com.metamx.http.client.HttpClientInit; import com.metamx.http.client.auth.BasicCredentials; import io.druid.curator.CuratorConfig; import io.druid.guice.JsonConfigProvider; import io.druid.guice.LazySingleton; import io.druid.guice.ManageLifecycle; -import io.druid.guice.http.DruidHttpClientConfig; +import io.druid.guice.annotations.Client; import io.druid.testing.IntegrationTestingConfig; import io.druid.testing.IntegrationTestingConfigProvider; import io.druid.testing.IntegrationTestingCuratorConfig; -import javax.net.ssl.SSLContext; - /** */ public class DruidTestModule implements Module @@ -63,24 +59,16 @@ public class DruidTestModule implements Module @TestClient public HttpClient getHttpClient( IntegrationTestingConfig config, - DruidHttpClientConfig httpClientConfig, - Lifecycle lifecycle + Lifecycle lifecycle, + @Client HttpClient delegate ) throws Exception { - - final HttpClientConfig.Builder builder = HttpClientConfig - .builder() - .withNumConnections(httpClientConfig.getNumConnections()) - .withReadTimeout(httpClientConfig.getReadTimeout()) - .withWorkerCount(httpClientConfig.getNumMaxThreads()); - - builder.withSslContext(SSLContext.getDefault()); - HttpClient delegate = HttpClientInit.createClient(builder.build(), lifecycle); if (config.getUsername() != null) { return new CredentialedHttpClient(new BasicCredentials(config.getUsername(), config.getPassword()), delegate); + } else { + return delegate; } - return delegate; } @Provides diff --git a/pom.xml b/pom.xml index be94b9c89e7..6655219b14f 100644 --- a/pom.xml +++ b/pom.xml @@ -95,6 +95,7 @@ extensions-core/avro-extensions extensions-core/datasketches + extensions-core/druid-kerberos extensions-core/hdfs-storage extensions-core/histogram extensions-core/stats