diff --git a/core/src/main/java/org/apache/druid/crypto/CryptoService.java b/core/src/main/java/org/apache/druid/crypto/CryptoService.java
new file mode 100644
index 000000000000..306ae7038dce
--- /dev/null
+++ b/core/src/main/java/org/apache/druid/crypto/CryptoService.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.druid.crypto;
+
+import com.google.common.base.Preconditions;
+import org.apache.druid.java.util.common.StringUtils;
+
+import javax.annotation.Nullable;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.security.spec.KeySpec;
+
+/**
+ * Utility class for symmetric key encryption (i.e. same secret is used for encryption and decryption) of byte[]
+ * using javax.crypto package.
+ *
+ * To learn about possible algorithms supported and their names,
+ * See https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
+ */
+public class CryptoService
+{
+ // Based on Javadocs on SecureRandom, It is threadsafe as well.
+ private static final SecureRandom SECURE_RANDOM_INSTANCE = new SecureRandom();
+
+ // User provided secret phrase used for encrypting data
+ private final char[] passPhrase;
+
+ // Variables for algorithm used to generate a SecretKey based on user provided passPhrase
+ private final String secretKeyFactoryAlg;
+ private final int saltSize;
+ private final int iterationCount;
+ private final int keyLength;
+
+ // Cipher algorithm information
+ private final String cipherAlgName;
+ private final String cipherAlgMode;
+ private final String cipherAlgPadding;
+
+ // transformation = "cipherAlgName/cipherAlgMode/cipherAlgPadding" used in Cipher.getInstance(transformation)
+ private final String transformation;
+
+ public CryptoService(
+ String passPhrase,
+ @Nullable String cipherAlgName,
+ @Nullable String cipherAlgMode,
+ @Nullable String cipherAlgPadding,
+ @Nullable String secretKeyFactoryAlg,
+ @Nullable Integer saltSize,
+ @Nullable Integer iterationCount,
+ @Nullable Integer keyLength
+ )
+ {
+ Preconditions.checkArgument(
+ passPhrase != null && !passPhrase.isEmpty(),
+ "null/empty passPhrase"
+ );
+ this.passPhrase = passPhrase.toCharArray();
+
+ this.cipherAlgName = cipherAlgName == null ? "AES" : cipherAlgName;
+ this.cipherAlgMode = cipherAlgMode == null ? "CBC" : cipherAlgMode;
+ this.cipherAlgPadding = cipherAlgPadding == null ? "PKCS5Padding" : cipherAlgPadding;
+ this.transformation = StringUtils.format("%s/%s/%s", this.cipherAlgName, this.cipherAlgMode, this.cipherAlgPadding);
+
+ this.secretKeyFactoryAlg = secretKeyFactoryAlg == null ? "PBKDF2WithHmacSHA256" : secretKeyFactoryAlg;
+ this.saltSize = saltSize == null ? 8 : saltSize;
+ this.iterationCount = iterationCount == null ? 65536 : iterationCount;
+ this.keyLength = keyLength == null ? 128 : keyLength;
+
+ // encrypt/decrypt a test string to ensure all params are valid
+ String testString = "duh! !! !!!";
+ Preconditions.checkState(
+ testString.equals(StringUtils.fromUtf8(decrypt(encrypt(StringUtils.toUtf8(testString))))),
+ "decrypt(encrypt(testString)) failed"
+ );
+ }
+
+ public byte[] encrypt(byte[] plain)
+ {
+ try {
+ byte[] salt = new byte[saltSize];
+ SECURE_RANDOM_INSTANCE.nextBytes(salt);
+
+ SecretKey tmp = getKeyFromPassword(passPhrase, salt);
+ SecretKey secret = new SecretKeySpec(tmp.getEncoded(), cipherAlgName);
+ Cipher ecipher = Cipher.getInstance(transformation);
+ ecipher.init(Cipher.ENCRYPT_MODE, secret);
+ return new EncryptedData(
+ salt,
+ ecipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV(),
+ ecipher.doFinal(plain)
+ ).toByteAray();
+ }
+ catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidParameterSpecException | IllegalBlockSizeException | BadPaddingException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public byte[] decrypt(byte[] data)
+ {
+ try {
+ EncryptedData encryptedData = EncryptedData.fromByteArray(data);
+
+ SecretKey tmp = getKeyFromPassword(passPhrase, encryptedData.getSalt());
+ SecretKey secret = new SecretKeySpec(tmp.getEncoded(), cipherAlgName);
+
+ Cipher dcipher = Cipher.getInstance(transformation);
+ dcipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(encryptedData.getIv()));
+ return dcipher.doFinal(encryptedData.getCipher());
+ }
+ catch (InvalidKeySpecException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private SecretKey getKeyFromPassword(char[] passPhrase, byte[] salt)
+ throws NoSuchAlgorithmException, InvalidKeySpecException
+ {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKeyFactoryAlg);
+ KeySpec spec = new PBEKeySpec(passPhrase, salt, iterationCount, keyLength);
+ return factory.generateSecret(spec);
+ }
+
+ private static class EncryptedData
+ {
+ private final byte[] salt;
+ private final byte[] iv;
+ private final byte[] cipher;
+
+ public EncryptedData(byte[] salt, byte[] iv, byte[] cipher)
+ {
+ this.salt = salt;
+ this.iv = iv;
+ this.cipher = cipher;
+ }
+
+ public byte[] getSalt()
+ {
+ return salt;
+ }
+
+ public byte[] getIv()
+ {
+ return iv;
+ }
+
+ public byte[] getCipher()
+ {
+ return cipher;
+ }
+
+ public byte[] toByteAray()
+ {
+ int headerLength = 12;
+ ByteBuffer bb = ByteBuffer.allocate(salt.length + iv.length + cipher.length + headerLength);
+ bb.putInt(salt.length)
+ .putInt(iv.length)
+ .putInt(cipher.length)
+ .put(salt)
+ .put(iv)
+ .put(cipher);
+ bb.flip();
+
+ return bb.array();
+ }
+
+ public static EncryptedData fromByteArray(byte[] array)
+ {
+ ByteBuffer bb = ByteBuffer.wrap(array);
+
+ int saltSize = bb.getInt();
+ int ivSize = bb.getInt();
+ int cipherSize = bb.getInt();
+
+ byte[] salt = new byte[saltSize];
+ bb.get(salt);
+
+ byte[] iv = new byte[ivSize];
+ bb.get(iv);
+
+ byte[] cipher = new byte[cipherSize];
+ bb.get(cipher);
+
+ return new EncryptedData(salt, iv, cipher);
+ }
+ }
+}
diff --git a/core/src/test/java/org/apache/druid/crypto/CryptoServiceTest.java b/core/src/test/java/org/apache/druid/crypto/CryptoServiceTest.java
new file mode 100644
index 000000000000..23fcde3b2771
--- /dev/null
+++ b/core/src/test/java/org/apache/druid/crypto/CryptoServiceTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.druid.crypto;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.nio.charset.StandardCharsets;
+
+public class CryptoServiceTest
+{
+ @Test
+ public void testEncryptDecrypt()
+ {
+ CryptoService cryptoService = new CryptoService(
+ "random-passphrase",
+ "AES",
+ "CBC",
+ "PKCS5Padding",
+ "PBKDF2WithHmacSHA256",
+ 8,
+ 65536,
+ 128
+ );
+
+ byte[] original = "i am a test string".getBytes(StandardCharsets.UTF_8);
+
+ byte[] decrypted = cryptoService.decrypt(cryptoService.encrypt(original));
+
+ Assert.assertArrayEquals(original, decrypted);
+ }
+
+ @Test
+ public void testInvalidParamsConstructorFailure()
+ {
+ try {
+ new CryptoService(
+ "random-passphrase",
+ "ABCD",
+ "EFGH",
+ "PAXXDDING",
+ "QWERTY",
+ 8,
+ 65536,
+ 128
+ );
+ Assert.fail("Must Fail!!!");
+ }
+ catch (RuntimeException ex) {
+ // expected
+ }
+ }
+}
diff --git a/distribution/bin/check-licenses.py b/distribution/bin/check-licenses.py
index 9f71fa8ba588..f90ecd817b3b 100755
--- a/distribution/bin/check-licenses.py
+++ b/distribution/bin/check-licenses.py
@@ -250,6 +250,7 @@ def build_compatible_license_names():
compatible_licenses['CDDL/GPLv2+CE'] = 'CDDL 1.1'
compatible_licenses['CDDL + GPLv2 with classpath exception'] = 'CDDL 1.1'
compatible_licenses['CDDL License'] = 'CDDL 1.1'
+ compatible_licenses['COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0'] = 'CDDL 1.0'
compatible_licenses['Eclipse Public License 1.0'] = 'Eclipse Public License 1.0'
compatible_licenses['The Eclipse Public License, Version 1.0'] = 'Eclipse Public License 1.0'
diff --git a/distribution/pom.xml b/distribution/pom.xml
index fc15e9343beb..446e79a0de7a 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -240,6 +240,8 @@
org.apache.druid.extensions:simple-client-sslcontext
-c
org.apache.druid.extensions:druid-basic-security
+ -c
+ org.apache.druid.extensions:druid-pac4j
${druid.distribution.pulldeps.opts}
diff --git a/docs/development/extensions-core/druid-pac4j.md b/docs/development/extensions-core/druid-pac4j.md
new file mode 100644
index 000000000000..57ec4c9c8ad7
--- /dev/null
+++ b/docs/development/extensions-core/druid-pac4j.md
@@ -0,0 +1,45 @@
+---
+id: druid-pac4j
+title: "Druid pac4j based Security extension"
+---
+
+
+
+
+Apache Druid Extension to enable [OpenID Connect](https://openid.net/connect/) based Authentication for Druid Processes using [pac4j](https://github.com/pac4j/pac4j) as the underlying client library.
+This can be used with any authentication server that supports same e.g. [Okta](https://developer.okta.com/).
+This extension should only be used at the router node to enable a group of users in existing authentication server to interact with Druid cluster, using the [Web Console](../../operations/druid-console.html). This extension does not support JDBC client authentication.
+
+## Configuration
+
+### Creating an Authenticator
+```
+druid.auth.authenticatorChain=["pac4j"]
+druid.auth.authenticator.pac4j.type=pac4j
+```
+
+### Properties
+|Property|Description|Default|required|
+|--------|---------------|-----------|-------|--------|
+|`druid.auth.pac4j.oidc.clientID`|OAuth Client Application id.|none|Yes|
+|`druid.auth.pac4j.oidc.clientSecret`|OAuth Client Application secret. It can be provided as plaintext string or The [Password Provider](../../operations/password-provider.md).|none|Yes|
+|`druid.auth.pac4j.oidc.discoveryURI`|discovery URI for fetching OP metadata [see this](http://openid.net/specs/openid-connect-discovery-1_0.html).|none|Yes|
+|`druid.auth.pac4j.oidc.cookiePassphrase`|passphrase for encrypting the cookies used to manage authentication session with browser. It can be provided as plaintext string or The [Password Provider](../../operations/password-provider.md).|none|Yes|
+
diff --git a/docs/development/extensions.md b/docs/development/extensions.md
index 96b00b4347e3..21bd8508b770 100644
--- a/docs/development/extensions.md
+++ b/docs/development/extensions.md
@@ -60,6 +60,7 @@ Core extensions are maintained by Druid committers.
|mysql-metadata-storage|MySQL metadata store.|[link](../development/extensions-core/mysql.md)|
|postgresql-metadata-storage|PostgreSQL metadata store.|[link](../development/extensions-core/postgresql.md)|
|simple-client-sslcontext|Simple SSLContext provider module to be used by Druid's internal HttpClient when talking to other Druid processes over HTTPS.|[link](../development/extensions-core/simple-client-sslcontext.md)|
+|druid-pac4j|OpenID Connect authentication for druid processes.|[link](../development/extensions-core/druid-pac4j.md)|
## Community extensions
diff --git a/extensions-core/druid-pac4j/pom.xml b/extensions-core/druid-pac4j/pom.xml
new file mode 100644
index 000000000000..927e8c4b5bc5
--- /dev/null
+++ b/extensions-core/druid-pac4j/pom.xml
@@ -0,0 +1,117 @@
+
+
+
+
+ 4.0.0
+
+ org.apache.druid.extensions
+ druid-pac4j
+ druid-pac4j
+ druid-pac4j
+
+
+ org.apache.druid
+ druid
+ 0.18.0-SNAPSHOT
+ ../../pom.xml
+
+
+
+ 3.8.3
+
+
+
+
+ org.apache.druid
+ druid-server
+ ${project.parent.version}
+ provided
+
+
+ org.pac4j
+ pac4j-oidc
+ ${pac4j.version}
+
+
+
+ com.google.code.findbugs
+ jsr305
+ provided
+
+
+ com.google.guava
+ guava
+ provided
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ provided
+
+
+ com.google.inject
+ guice
+ provided
+
+
+ javax.servlet
+ javax.servlet-api
+ provided
+
+
+ commons-io
+ commons-io
+ provided
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ provided
+
+
+ org.apache.druid
+ druid-core
+ ${project.parent.version}
+ provided
+
+
+ org.pac4j
+ pac4j-core
+ ${pac4j.version}
+
+
+ javax.ws.rs
+ jsr311-api
+ provided
+
+
+
+ junit
+ junit
+ test
+
+
+ org.easymock
+ easymock
+ test
+
+
+
+
diff --git a/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/OIDCConfig.java b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/OIDCConfig.java
new file mode 100644
index 000000000000..72ef4657653e
--- /dev/null
+++ b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/OIDCConfig.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.druid.security.pac4j;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import org.apache.druid.metadata.PasswordProvider;
+
+public class OIDCConfig
+{
+ @JsonProperty
+ private final String clientID;
+
+ @JsonProperty
+ private final PasswordProvider clientSecret;
+
+ @JsonProperty
+ private final String discoveryURI;
+
+ @JsonProperty
+ private final PasswordProvider cookiePassphrase;
+
+ @JsonCreator
+ public OIDCConfig(
+ @JsonProperty("clientID") String clientID,
+ @JsonProperty("clientSecret") PasswordProvider clientSecret,
+ @JsonProperty("discoveryURI") String discoveryURI,
+ @JsonProperty("cookiePassphrase") PasswordProvider cookiePassphrase
+ )
+ {
+ this.clientID = Preconditions.checkNotNull(clientID, "null clientID");
+ this.clientSecret = Preconditions.checkNotNull(clientSecret, "null clientSecret");
+ this.discoveryURI = Preconditions.checkNotNull(discoveryURI, "null discoveryURI");
+ this.cookiePassphrase = Preconditions.checkNotNull(cookiePassphrase, "null cookiePassphrase");
+ }
+
+ @JsonProperty
+ public String getClientID()
+ {
+ return clientID;
+ }
+
+ @JsonProperty
+ public PasswordProvider getClientSecret()
+ {
+ return clientSecret;
+ }
+
+ @JsonProperty
+ public String getDiscoveryURI()
+ {
+ return discoveryURI;
+ }
+
+ @JsonProperty
+ public PasswordProvider getCookiePassphrase()
+ {
+ return cookiePassphrase;
+ }
+}
diff --git a/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jAuthenticator.java b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jAuthenticator.java
new file mode 100644
index 000000000000..a22de73a6888
--- /dev/null
+++ b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jAuthenticator.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.druid.security.pac4j;
+
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import org.apache.druid.server.security.AuthenticationResult;
+import org.apache.druid.server.security.Authenticator;
+import org.pac4j.core.config.Config;
+import org.pac4j.core.http.callback.NoParameterCallbackUrlResolver;
+import org.pac4j.core.http.url.DefaultUrlResolver;
+import org.pac4j.oidc.client.OidcClient;
+import org.pac4j.oidc.config.OidcConfiguration;
+
+import javax.annotation.Nullable;
+import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import java.util.EnumSet;
+import java.util.Map;
+
+@JsonTypeName("pac4j")
+public class Pac4jAuthenticator implements Authenticator
+{
+ private final String name;
+ private final String authorizerName;
+ private final Supplier pac4jConfigSupplier;
+ private final OIDCConfig oidcConfig;
+
+ @JsonCreator
+ public Pac4jAuthenticator(
+ @JsonProperty("name") String name,
+ @JsonProperty("authorizerName") String authorizerName,
+ @JacksonInject OIDCConfig oidcConfig
+ )
+ {
+ this.name = name;
+ this.authorizerName = authorizerName;
+ this.oidcConfig = oidcConfig;
+ this.pac4jConfigSupplier = Suppliers.memoize(() -> createPac4jConfig(oidcConfig));
+ }
+
+ @Override
+ public Filter getFilter()
+ {
+ return new Pac4jFilter(
+ name,
+ authorizerName,
+ pac4jConfigSupplier.get(),
+ oidcConfig.getCookiePassphrase().getPassword()
+ );
+ }
+
+ @Override
+ public String getAuthChallengeHeader()
+ {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public AuthenticationResult authenticateJDBCContext(Map context)
+ {
+ return null;
+ }
+
+
+ @Override
+ public Class extends Filter> getFilterClass()
+ {
+ return null;
+ }
+
+ @Override
+ public Map getInitParameters()
+ {
+ return null;
+ }
+
+ @Override
+ public String getPath()
+ {
+ return "/*";
+ }
+
+ @Override
+ public EnumSet getDispatcherType()
+ {
+ return null;
+ }
+
+ private Config createPac4jConfig(OIDCConfig oidcConfig)
+ {
+ OidcConfiguration oidcConf = new OidcConfiguration();
+ oidcConf.setClientId(oidcConfig.getClientID());
+ oidcConf.setSecret(oidcConfig.getClientSecret().getPassword());
+ oidcConf.setDiscoveryURI(oidcConfig.getDiscoveryURI());
+ oidcConf.setExpireSessionWithToken(true);
+ oidcConf.setUseNonce(true);
+
+ OidcClient oidcClient = new OidcClient(oidcConf);
+ oidcClient.setUrlResolver(new DefaultUrlResolver(true));
+ oidcClient.setCallbackUrlResolver(new NoParameterCallbackUrlResolver());
+
+ return new Config(Pac4jCallbackResource.SELF_URL, oidcClient);
+ }
+}
diff --git a/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jCallbackResource.java b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jCallbackResource.java
new file mode 100644
index 000000000000..e975b7cfdd3c
--- /dev/null
+++ b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jCallbackResource.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.druid.security.pac4j;
+
+import com.google.inject.Inject;
+import org.apache.druid.guice.LazySingleton;
+import org.apache.druid.java.util.common.logger.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Response;
+
+/**
+ * Fixed Callback endpoint used after successful login with Identity Provider e.g. OAuth server.
+ * See https://www.pac4j.org/blog/understanding-the-callback-endpoint.html
+ */
+@Path(Pac4jCallbackResource.SELF_URL)
+@LazySingleton
+public class Pac4jCallbackResource
+{
+ public static final String SELF_URL = "/druid-ext/druid-pac4j/callback";
+
+ private static final Logger LOGGER = new Logger(Pac4jCallbackResource.class);
+
+ @Inject
+ public Pac4jCallbackResource()
+ {
+ }
+
+ @GET
+ public Response callback()
+ {
+ LOGGER.error(
+ new RuntimeException(),
+ "This endpoint is to be handled by the pac4j filter to redirect users, request should never reach here."
+ );
+ return Response.serverError().build();
+ }
+}
+
diff --git a/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jDruidModule.java b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jDruidModule.java
new file mode 100644
index 000000000000..af9b40283bab
--- /dev/null
+++ b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jDruidModule.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.druid.security.pac4j;
+
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Binder;
+import org.apache.druid.guice.Jerseys;
+import org.apache.druid.guice.JsonConfigProvider;
+import org.apache.druid.initialization.DruidModule;
+
+import java.util.List;
+
+public class Pac4jDruidModule implements DruidModule
+{
+ @Override
+ public List extends Module> getJacksonModules()
+ {
+ return ImmutableList.of(
+ new SimpleModule("Pac4jDruidSecurity").registerSubtypes(
+ Pac4jAuthenticator.class
+ )
+ );
+ }
+
+ @Override
+ public void configure(Binder binder)
+ {
+ JsonConfigProvider.bind(binder, "druid.auth.pac4j.oidc", OIDCConfig.class);
+
+ Jerseys.addResource(binder, Pac4jCallbackResource.class);
+ }
+}
diff --git a/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jFilter.java b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jFilter.java
new file mode 100644
index 000000000000..7a9eff76b03d
--- /dev/null
+++ b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jFilter.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.druid.security.pac4j;
+
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.server.security.AuthConfig;
+import org.apache.druid.server.security.AuthenticationResult;
+import org.pac4j.core.config.Config;
+import org.pac4j.core.context.J2EContext;
+import org.pac4j.core.context.session.SessionStore;
+import org.pac4j.core.engine.CallbackLogic;
+import org.pac4j.core.engine.DefaultCallbackLogic;
+import org.pac4j.core.engine.DefaultSecurityLogic;
+import org.pac4j.core.engine.SecurityLogic;
+import org.pac4j.core.http.adapter.HttpActionAdapter;
+import org.pac4j.core.profile.CommonProfile;
+
+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 javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Collection;
+
+public class Pac4jFilter implements Filter
+{
+ private static final Logger LOGGER = new Logger(Pac4jFilter.class);
+
+ private static final HttpActionAdapter NOOP_HTTP_ACTION_ADAPTER = (int code, J2EContext ctx) -> null;
+
+ private final Config pac4jConfig;
+ private final SecurityLogic securityLogic;
+ private final CallbackLogic callbackLogic;
+ private final SessionStore sessionStore;
+
+ private final String name;
+ private final String authorizerName;
+
+ public Pac4jFilter(String name, String authorizerName, Config pac4jConfig, String cookiePassphrase)
+ {
+ this.pac4jConfig = pac4jConfig;
+ this.securityLogic = new DefaultSecurityLogic<>();
+ this.callbackLogic = new DefaultCallbackLogic<>();
+
+ this.name = name;
+ this.authorizerName = authorizerName;
+
+ this.sessionStore = new Pac4jSessionStore<>(cookiePassphrase);
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig)
+ {
+ }
+
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
+ throws IOException, ServletException
+ {
+ HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
+ HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
+ J2EContext context = new J2EContext(httpServletRequest, httpServletResponse, sessionStore);
+
+ if (Pac4jCallbackResource.SELF_URL.equals(httpServletRequest.getRequestURI())) {
+ callbackLogic.perform(
+ context,
+ pac4jConfig,
+ NOOP_HTTP_ACTION_ADAPTER,
+ "/",
+ true, false, false, null);
+ } else {
+ String uid = securityLogic.perform(
+ context,
+ pac4jConfig,
+ (J2EContext ctx, Collection profiles, Object... parameters) -> {
+ if (profiles.isEmpty()) {
+ LOGGER.warn("No profiles found after OIDC auth.");
+ return null;
+ } else {
+ return profiles.iterator().next().getId();
+ }
+ },
+ NOOP_HTTP_ACTION_ADAPTER,
+ null, null, null, null);
+
+ if (uid != null) {
+ AuthenticationResult authenticationResult = new AuthenticationResult(uid, authorizerName, name, null);
+ servletRequest.setAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT, authenticationResult);
+ filterChain.doFilter(servletRequest, servletResponse);
+ }
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+}
diff --git a/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jSessionStore.java b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jSessionStore.java
new file mode 100644
index 000000000000..069a4ff2eb9a
--- /dev/null
+++ b/extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jSessionStore.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.druid.security.pac4j;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.druid.crypto.CryptoService;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.pac4j.core.context.ContextHelper;
+import org.pac4j.core.context.Cookie;
+import org.pac4j.core.context.Pac4jConstants;
+import org.pac4j.core.context.WebContext;
+import org.pac4j.core.context.session.SessionStore;
+import org.pac4j.core.exception.TechnicalException;
+import org.pac4j.core.profile.CommonProfile;
+import org.pac4j.core.util.JavaSerializationHelper;
+
+import javax.annotation.Nullable;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * Code here is slight adaptation from KnoxSessionStore
+ * for storing oauth session information in cookies.
+ */
+public class Pac4jSessionStore implements SessionStore
+{
+
+ private static final Logger LOGGER = new Logger(Pac4jSessionStore.class);
+
+ public static final String PAC4J_SESSION_PREFIX = "pac4j.session.";
+
+ private final JavaSerializationHelper javaSerializationHelper;
+ private final CryptoService cryptoService;
+
+ public Pac4jSessionStore(String cookiePassphrase)
+ {
+ javaSerializationHelper = new JavaSerializationHelper();
+ cryptoService = new CryptoService(
+ cookiePassphrase,
+ "AES",
+ "CBC",
+ "PKCS5Padding",
+ "PBKDF2WithHmacSHA256",
+ 8,
+ 65536,
+ 128
+ );
+ }
+
+ @Override
+ public String getOrCreateSessionId(WebContext context)
+ {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Object get(WebContext context, String key)
+ {
+ final Cookie cookie = ContextHelper.getCookie(context, PAC4J_SESSION_PREFIX + key);
+ Object value = null;
+ if (cookie != null) {
+ value = uncompressDecryptBase64(cookie.getValue());
+ }
+ LOGGER.debug("Get from session: [%s] = [%s]", key, value);
+ return value;
+ }
+
+ @Override
+ public void set(WebContext context, String key, @Nullable Object value)
+ {
+ Object profile = value;
+ Cookie cookie;
+
+ if (value == null) {
+ cookie = new Cookie(PAC4J_SESSION_PREFIX + key, null);
+ } else {
+ if (key.contentEquals(Pac4jConstants.USER_PROFILES)) {
+ /* trim the profile object */
+ profile = clearUserProfile(value);
+ }
+ LOGGER.debug("Save in session: [%s] = [%s]", key, profile);
+ cookie = new Cookie(
+ PAC4J_SESSION_PREFIX + key,
+ compressEncryptBase64(profile)
+ );
+ }
+
+ cookie.setDomain("");
+ cookie.setHttpOnly(true);
+ cookie.setSecure(ContextHelper.isHttpsOrSecure(context));
+ cookie.setPath("/");
+ cookie.setMaxAge(900);
+
+ context.addResponseCookie(cookie);
+ }
+
+ @Nullable
+ private String compressEncryptBase64(final Object o)
+ {
+ if (o == null || "".equals(o)
+ || (o instanceof Map, ?> && ((Map, ?>) o).isEmpty())) {
+ return null;
+ } else {
+ byte[] bytes = javaSerializationHelper.serializeToBytes((Serializable) o);
+
+ bytes = compress(bytes);
+ if (bytes.length > 3000) {
+ LOGGER.warn("Cookie too big, it might not be properly set");
+ }
+
+ return StringUtils.encodeBase64String(cryptoService.encrypt(bytes));
+ }
+ }
+
+ @Nullable
+ private Serializable uncompressDecryptBase64(final String v)
+ {
+ if (v != null && !v.isEmpty()) {
+ byte[] bytes = StringUtils.decodeBase64String(v);
+ if (bytes != null) {
+ return javaSerializationHelper.unserializeFromBytes(unCompress(cryptoService.decrypt(bytes)));
+ }
+ }
+ return null;
+ }
+
+ private byte[] compress(final byte[] data)
+ {
+ try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(data.length)) {
+ try (GZIPOutputStream gzip = new GZIPOutputStream(byteStream)) {
+ gzip.write(data);
+ }
+ return byteStream.toByteArray();
+ }
+ catch (IOException ex) {
+ throw new TechnicalException(ex);
+ }
+ }
+
+ private byte[] unCompress(final byte[] data)
+ {
+ try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
+ GZIPInputStream gzip = new GZIPInputStream(inputStream)) {
+ return IOUtils.toByteArray(gzip);
+ }
+ catch (IOException ex) {
+ throw new TechnicalException(ex);
+ }
+ }
+
+ private Object clearUserProfile(final Object value)
+ {
+ if (value instanceof Map, ?>) {
+ final Map profiles = (Map) value;
+ profiles.forEach((name, profile) -> profile.clearSensitiveData());
+ return profiles;
+ } else {
+ final CommonProfile profile = (CommonProfile) value;
+ profile.clearSensitiveData();
+ return profile;
+ }
+ }
+
+ @Override
+ public SessionStore buildFromTrackableSession(WebContext arg0, Object arg1)
+ {
+ return null;
+ }
+
+ @Override
+ public boolean destroySession(WebContext arg0)
+ {
+ return false;
+ }
+
+ @Override
+ public Object getTrackableSession(WebContext arg0)
+ {
+ return null;
+ }
+
+ @Override
+ public boolean renewSession(final WebContext context)
+ {
+ return false;
+ }
+}
diff --git a/extensions-core/druid-pac4j/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule b/extensions-core/druid-pac4j/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule
new file mode 100644
index 000000000000..e0a77f8254f3
--- /dev/null
+++ b/extensions-core/druid-pac4j/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF 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.
+
+org.apache.druid.security.pac4j.Pac4jDruidModule
diff --git a/extensions-core/druid-pac4j/src/test/java/org/apache/druid/security/pac4j/Pac4jSessionStoreTest.java b/extensions-core/druid-pac4j/src/test/java/org/apache/druid/security/pac4j/Pac4jSessionStoreTest.java
new file mode 100644
index 000000000000..0349a98a7ccd
--- /dev/null
+++ b/extensions-core/druid-pac4j/src/test/java/org/apache/druid/security/pac4j/Pac4jSessionStoreTest.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.druid.security.pac4j;
+
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Test;
+import org.pac4j.core.context.Cookie;
+import org.pac4j.core.context.WebContext;
+
+import java.util.Collections;
+
+public class Pac4jSessionStoreTest
+{
+ @Test
+ public void testSetAndGet()
+ {
+ Pac4jSessionStore sessionStore = new Pac4jSessionStore("test-cookie-passphrase");
+
+ WebContext webContext1 = EasyMock.mock(WebContext.class);
+ EasyMock.expect(webContext1.getScheme()).andReturn("https");
+ Capture cookieCapture = EasyMock.newCapture();
+
+ webContext1.addResponseCookie(EasyMock.capture(cookieCapture));
+ EasyMock.replay(webContext1);
+
+ sessionStore.set(webContext1, "key", "value");
+
+ Cookie cookie = cookieCapture.getValue();
+ Assert.assertTrue(cookie.isSecure());
+ Assert.assertTrue(cookie.isHttpOnly());
+ Assert.assertTrue(cookie.isSecure());
+ Assert.assertEquals(900, cookie.getMaxAge());
+
+
+ WebContext webContext2 = EasyMock.mock(WebContext.class);
+ EasyMock.expect(webContext2.getRequestCookies()).andReturn(Collections.singletonList(cookie));
+ EasyMock.replay(webContext2);
+
+ Assert.assertEquals("value", sessionStore.get(webContext2, "key"));
+ }
+}
diff --git a/licenses.yaml b/licenses.yaml
index 62802831e45b..7af752619474 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -136,6 +136,16 @@ source_paths:
---
+name: code adapted from Apache Knox KnoxSessionStore and ConfigurableEncryptor
+license_category: source
+module: extensions/druid-pac4j
+license_name: Apache License version 2.0
+source_paths:
+ - extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jSessionStore.java
+ - core/src/main/java/org/apache/druid/crypto/CryptoService.java
+
+---
+
name: AWS SDK for Java
license_category: binary
module: java-core
@@ -654,6 +664,116 @@ libraries:
---
+name: pac4j-oidc java security library
+license_category: binary
+module: extensions/druid-pac4j
+license_name: Apache License version 2.0
+version: 3.8.3
+libraries:
+ - org.pac4j: pac4j-oidc
+
+---
+
+name: pac4j-core java security library
+license_category: binary
+module: extensions/druid-pac4j
+license_name: Apache License version 2.0
+version: 3.8.3
+libraries:
+ - org.pac4j: pac4j-core
+
+---
+
+name: org.objenesis objenesis
+license_category: binary
+module: extensions/druid-pac4j
+license_name: Apache License version 2.0
+version: 3.0.1
+libraries:
+ - org.objenesis: objenesis
+
+---
+
+name: com.nimbusds lang-tag
+license_category: binary
+module: extensions/druid-pac4j
+license_name: Apache License version 2.0
+version: 1.4.4
+libraries:
+ - com.nimbusds: lang-tag
+
+---
+
+name: com.nimbusds nimbus-jose-jwt
+license_category: binary
+module: extensions/druid-pac4j
+license_name: Apache License version 2.0
+version: 7.9
+libraries:
+ - com.nimbusds: nimbus-jose-jwt
+
+---
+
+name: com.nimbusds oauth2-oidc-sdk
+license_category: binary
+module: extensions/druid-pac4j
+license_name: Apache License version 2.0
+version: 6.5
+libraries:
+ - com.nimbusds: oauth2-oidc-sdk
+
+---
+
+name: net.bytebuddy byte-buddy
+license_category: binary
+module: extensions/druid-pac4j
+license_name: Apache License version 2.0
+version: 1.9.10
+libraries:
+ - net.bytebuddy: byte-buddy
+
+---
+
+name: net.bytebuddy byte-buddy-agent
+license_category: binary
+module: extensions/druid-pac4j
+license_name: Apache License version 2.0
+version: 1.9.10
+libraries:
+ - net.bytebuddy: byte-buddy-agent
+
+---
+
+name: org.mockito mockito-core
+license_category: binary
+module: extensions/druid-pac4j
+license_name: MIT License
+version: 2.28.2
+libraries:
+ - org.mockito: mockito-core
+
+---
+
+name: javax.activation activation
+license_category: binary
+module: extensions/druid-pac4j
+license_name: COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0
+version: 1.1.1
+libraries:
+ - javax.activation: activation
+
+---
+
+name: com.sun.mail javax.mail
+license_category: binary
+module: extensions/druid-pac4j
+license_name: CDDL 1.1
+version: 1.6.1
+libraries:
+ - com.sun.mail: javax.mail
+
+---
+
name: Netty
license_category: binary
module: java-core
diff --git a/pom.xml b/pom.xml
index 49252e06ac84..6d28e2f42658 100644
--- a/pom.xml
+++ b/pom.xml
@@ -143,6 +143,7 @@
extensions-core/datasketches
extensions-core/druid-bloom-filter
extensions-core/druid-kerberos
+ extensions-core/druid-pac4j
extensions-core/hdfs-storage
extensions-core/histogram
extensions-core/stats
diff --git a/server/src/main/java/org/apache/druid/server/security/AuthenticationOnlyResourceFilter.java b/server/src/main/java/org/apache/druid/server/security/AuthenticationOnlyResourceFilter.java
new file mode 100644
index 000000000000..9a8a32ef7eb0
--- /dev/null
+++ b/server/src/main/java/org/apache/druid/server/security/AuthenticationOnlyResourceFilter.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.druid.server.security;
+
+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 java.io.IOException;
+
+/**
+ * Sets necessary request attributes for requests sent to endpoints that need authentication but no authorization.
+ */
+public class AuthenticationOnlyResourceFilter implements Filter
+{
+ @Override
+ public void init(FilterConfig filterConfig)
+ {
+
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
+ throws IOException, ServletException
+ {
+ // This request will not go to an Authorizer, so we need to set this for PreResponseAuthorizationCheckFilter
+ servletRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
+
+ filterChain.doFilter(servletRequest, servletResponse);
+ }
+
+ @Override
+ public void destroy()
+ {
+
+ }
+}
diff --git a/server/src/main/java/org/apache/druid/server/security/AuthenticationUtils.java b/server/src/main/java/org/apache/druid/server/security/AuthenticationUtils.java
index 924f23ea3f6d..d4fba7388abf 100644
--- a/server/src/main/java/org/apache/druid/server/security/AuthenticationUtils.java
+++ b/server/src/main/java/org/apache/druid/server/security/AuthenticationUtils.java
@@ -64,6 +64,13 @@ public static void addNoopAuthenticationAndAuthorizationFilters(ServletContextHa
}
}
+ public static void addNoopAuthorizationFilters(ServletContextHandler root, List unsecuredPaths)
+ {
+ for (String unsecuredPath : unsecuredPaths) {
+ root.addFilter(new FilterHolder(new AuthenticationOnlyResourceFilter()), unsecuredPath, null);
+ }
+ }
+
public static void addSecuritySanityCheckFilter(
ServletContextHandler root,
ObjectMapper jsonMapper
diff --git a/services/src/main/java/org/apache/druid/cli/WebConsoleJettyServerInitializer.java b/services/src/main/java/org/apache/druid/cli/WebConsoleJettyServerInitializer.java
index 440ac1fc44d9..d29e729f73e6 100644
--- a/services/src/main/java/org/apache/druid/cli/WebConsoleJettyServerInitializer.java
+++ b/services/src/main/java/org/apache/druid/cli/WebConsoleJettyServerInitializer.java
@@ -39,10 +39,13 @@ class WebConsoleJettyServerInitializer
"/favicon.png",
"/assets/*",
"/public/*",
- WEB_CONSOLE_ROOT,
"/console-config.js"
);
+ private static final List UNAUTHORIZED_PATHS_FOR_UI = ImmutableList.of(
+ WEB_CONSOLE_ROOT
+ );
+
static void intializeServerForWebConsoleRoot(ServletContextHandler root)
{
root.setInitParameter("org.eclipse.jetty.servlet.Default.redirectWelcome", "true");
@@ -51,6 +54,7 @@ static void intializeServerForWebConsoleRoot(ServletContextHandler root)
root.setBaseResource(Resource.newClassPathResource("org/apache/druid/console"));
AuthenticationUtils.addNoopAuthenticationAndAuthorizationFilters(root, UNSECURED_PATHS_FOR_UI);
+ AuthenticationUtils.addNoopAuthorizationFilters(root, UNAUTHORIZED_PATHS_FOR_UI);
}
static Handler createWebConsoleRewriteHandler()
diff --git a/website/.spelling b/website/.spelling
index 24a8ce5336bc..26aac1c9f794 100644
--- a/website/.spelling
+++ b/website/.spelling
@@ -225,6 +225,7 @@ deserialization
deserialize
deserialized
downtimes
+druid
e.g.
encodings
endian
@@ -645,6 +646,11 @@ maximumSize
onHeapPolling
pollPeriod
reverseLoadingCacheSpec
+ - ../docs/development/extensions-core/druid-pac4j.md
+OAuth
+Okta
+OpenID
+pac4j
- ../docs/development/extensions-core/google.md
GCS
StaticGoogleBlobStoreFirehose