From 5604ac796367eb7b0523247fcedd82f3d00cf3c0 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Mon, 23 Mar 2020 18:15:45 -0700 Subject: [PATCH] druid extension for OpenID Connect auth using pac4j lib (#8992) * druid pac4j security extension for OpenID Connect OAuth 2.0 authentication * update version in druid-pac4j pom * introducing unauthorized resource filter * authenticated but authorized /unified-webconsole.html * use httpReq.getRequestURI() for matching callback path * add documentation * minor doc addition * licesne file updates * make dependency analyze succeed * fix doc build * hopefully fixes doc build * hopefully fixes license check build * yet another try on fixing license build * revert unintentional changes to website folder * update version to 0.18.0-SNAPSHOT * check session and its expiry on each request * add crypto service * code for encrypting the cookie * update doc with cookiePassphrase * update license yaml * make sessionstore in Pac4jFilter private non static * make Pac4jFilter fields final * okta: use sha256 for hmac * remove incubating * add UTs for crypto util and session store impl * use standard charsets * add license header * remove unused file * add org.objenesis.objenesis to license.yaml * a bit of nit changes in CryptoService and embedding EncryptionResult for clarity * rename alg to cipherAlgName * take cipher alg name, mode and padding as input * add java doc for CryptoService and make it more understandable * another UT for CryptoService * cache pac4j Config * use generics clearly in Pac4jSessionStore * update cookiePassphrase doc to mention PasswordProvider * mark stuff Nullable where appropriate in Pac4jSessionStore * update doc to mention jdbc * add error log on reaching callback resource * javadoc for Pac4jCallbackResource * introduce NOOP_HTTP_ACTION_ADAPTER * add correct module name in license file * correct extensions folder name in licenses.yaml * replace druid-kubernetes-extensions to druid-pac4j * cache SecureRandom instance * rename UnauthorizedResourceFilter to AuthenticationOnlyResourceFilter --- .../apache/druid/crypto/CryptoService.java | 217 ++++++++++++++++++ .../druid/crypto/CryptoServiceTest.java | 70 ++++++ distribution/bin/check-licenses.py | 1 + distribution/pom.xml | 2 + .../extensions-core/druid-pac4j.md | 45 ++++ docs/development/extensions.md | 1 + extensions-core/druid-pac4j/pom.xml | 117 ++++++++++ .../druid/security/pac4j/OIDCConfig.java | 78 +++++++ .../security/pac4j/Pac4jAuthenticator.java | 127 ++++++++++ .../security/pac4j/Pac4jCallbackResource.java | 57 +++++ .../security/pac4j/Pac4jDruidModule.java | 51 ++++ .../druid/security/pac4j/Pac4jFilter.java | 120 ++++++++++ .../security/pac4j/Pac4jSessionStore.java | 211 +++++++++++++++++ ...rg.apache.druid.initialization.DruidModule | 16 ++ .../security/pac4j/Pac4jSessionStoreTest.java | 60 +++++ licenses.yaml | 120 ++++++++++ pom.xml | 1 + .../AuthenticationOnlyResourceFilter.java | 56 +++++ .../server/security/AuthenticationUtils.java | 7 + .../cli/WebConsoleJettyServerInitializer.java | 6 +- website/.spelling | 6 + 21 files changed, 1368 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/apache/druid/crypto/CryptoService.java create mode 100644 core/src/test/java/org/apache/druid/crypto/CryptoServiceTest.java create mode 100644 docs/development/extensions-core/druid-pac4j.md create mode 100644 extensions-core/druid-pac4j/pom.xml create mode 100644 extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/OIDCConfig.java create mode 100644 extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jAuthenticator.java create mode 100644 extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jCallbackResource.java create mode 100644 extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jDruidModule.java create mode 100644 extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jFilter.java create mode 100644 extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jSessionStore.java create mode 100644 extensions-core/druid-pac4j/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule create mode 100644 extensions-core/druid-pac4j/src/test/java/org/apache/druid/security/pac4j/Pac4jSessionStoreTest.java create mode 100644 server/src/main/java/org/apache/druid/server/security/AuthenticationOnlyResourceFilter.java 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 00000000000..306ae7038dc --- /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 00000000000..23fcde3b277 --- /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 9f71fa8ba58..f90ecd817b3 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 fc15e9343be..446e79a0de7 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 00000000000..57ec4c9c8ad --- /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 96b00b4347e..21bd8508b77 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 00000000000..927e8c4b5bc --- /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 00000000000..72ef4657653 --- /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 00000000000..a22de73a688 --- /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 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 00000000000..e975b7cfdd3 --- /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 00000000000..af9b40283ba --- /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 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 00000000000..7a9eff76b03 --- /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 00000000000..069a4ff2eb9 --- /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 00000000000..e0a77f8254f --- /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 00000000000..0349a98a7cc --- /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 62802831e45..7af75261947 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 49252e06ac8..6d28e2f4265 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 00000000000..9a8a32ef7eb --- /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 924f23ea3f6..d4fba7388ab 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 class AuthenticationUtils } } + 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 440ac1fc44d..d29e729f73e 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 @@ class WebConsoleJettyServerInitializer 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 24a8ce5336b..26aac1c9f79 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