diff --git a/docs/development/extensions-core/druid-kerberos.md b/docs/development/extensions-core/druid-kerberos.md index 8858e535488e..3b4459dead10 100644 --- a/docs/development/extensions-core/druid-kerberos.md +++ b/docs/development/extensions-core/druid-kerberos.md @@ -121,5 +121,19 @@ To access Coordinator/Overlord console from browser you will need to configure y 1. Configure trusted websites to include `"druid-coordinator-hostname"` and `"druid-overlord-hostname"` 2. Allow negotiation for the UI website. +## User identity and `authToLocal` rules + +The Kerberos authenticator uses the short name produced by `authToLocal` rules as the authenticated user identity. +For example, if the Kerberos principal is `user@EXAMPLE.COM` and the `authToLocal` rule maps it to `user`, then the +identity used for authorization is `user`, not the full principal `user@EXAMPLE.COM`. + +When configuring authorizer permissions, use the short (local) user name rather than the full Kerberos principal. + +:::info +In previous versions, the authenticated identity was incorrectly set to the full Kerberos principal +(e.g., `user@EXAMPLE.COM`) instead of the short name (e.g., `user`). If you have authorizer rules that +reference full Kerberos principals, update them to use the short name after upgrading. +::: + ## Sending Queries programmatically Many HTTP client libraries, such as Apache Commons [HttpComponents](https://hc.apache.org/), already have support for performing SPNEGO authentication. You can use any of the available HTTP client library to communicate with druid cluster. diff --git a/extensions-core/druid-kerberos/src/main/java/org/apache/druid/security/kerberos/KerberosAuthenticator.java b/extensions-core/druid-kerberos/src/main/java/org/apache/druid/security/kerberos/KerberosAuthenticator.java index 6ef391610c31..8f995b4d5d95 100644 --- a/extensions-core/druid-kerberos/src/main/java/org/apache/druid/security/kerberos/KerberosAuthenticator.java +++ b/extensions-core/druid-kerberos/src/main/java/org/apache/druid/security/kerberos/KerberosAuthenticator.java @@ -321,7 +321,7 @@ public Principal getUserPrincipal() // Since this request is validated also set DRUID_AUTHENTICATION_RESULT request.setAttribute( AuthConfig.DRUID_AUTHENTICATION_RESULT, - new AuthenticationResult(token.getName(), authorizerName, name, null) + createAuthenticationResult(token) ); doFilter(filterChain, httpRequest, httpResponse); } @@ -594,4 +594,9 @@ private static String tokenToCookieString( sb.append("; HttpOnly"); return sb.toString(); } + + AuthenticationResult createAuthenticationResult(AuthenticationToken token) + { + return new AuthenticationResult(token.getUserName(), authorizerName, name, null); + } } diff --git a/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/DruidKerberosAuthenticationHandlerTokenTest.java b/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/DruidKerberosAuthenticationHandlerTokenTest.java new file mode 100644 index 000000000000..78034691cc27 --- /dev/null +++ b/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/DruidKerberosAuthenticationHandlerTokenTest.java @@ -0,0 +1,171 @@ +/* + * 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.kerberos; + +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.hadoop.security.authentication.server.AuthenticationToken; +import org.apache.hadoop.security.authentication.util.KerberosName; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests verifying that AuthenticationToken userName (short name) is used for + * AuthenticationResult identity, not the full Kerberos principal. + * + *

The DruidKerberosAuthenticationHandler creates tokens with: + * {@code new AuthenticationToken(userName, clientPrincipal, getType())} + * where userName is the result of KerberosName.getShortName() (applying authToLocal rules). + * + *

The KerberosAuthenticator must use token.getUserName() (not token.getName()) when + * constructing AuthenticationResult so that the identity matches the short name + * produced by authToLocal rules. + */ +public class DruidKerberosAuthenticationHandlerTokenTest +{ + private static final String SAFE_RULES = "RULE:[1:$1] RULE:[2:$1]"; + + @Before + public void setUp() + { + KerberosName.setRules(SAFE_RULES); + } + + @After + public void tearDown() + { + KerberosName.setRules(SAFE_RULES); + } + + @Test + public void testSimplePrincipalMapping() + { + final String shortName = "druid"; + final String principal = "druid@EXAMPLE.COM"; + verifyTokenIdentity(shortName, principal); + } + + @Test + public void testServicePrincipalMapping() + { + final String shortName = "druid"; + final String principal = "druid/host.example.com@EXAMPLE.COM"; + verifyTokenIdentity(shortName, principal); + } + + @Test + public void testPrincipalWithDifferentShortName() + { + // Simulates authToLocal rule mapping a principal to a different local name + final String shortName = "admin"; + final String principal = "admin/secure@CORP.EXAMPLE.COM"; + verifyTokenIdentity(shortName, principal); + } + + @Test + public void testAuthToLocalRuleExtractsShortName() throws Exception + { + // Use an explicit rule that strips the realm, matching what DEFAULT does with a krb5.conf + KerberosName.setRules("RULE:[1:$1@$0](.*@EXAMPLE.COM)s/@.*//"); + final KerberosName kerberosName = new KerberosName("user@EXAMPLE.COM"); + final String shortName = kerberosName.getShortName(); + + Assert.assertEquals("user", shortName); + + // This is what DruidKerberosAuthenticationHandler does + final AuthenticationToken token = new AuthenticationToken(shortName, "user@EXAMPLE.COM", "kerberos"); + + // And KerberosAuthenticator must use getUserName() for identity + final AuthenticationResult result = new AuthenticationResult( + token.getUserName(), + "authorizer", + "kerberos", + null + ); + Assert.assertEquals("user", result.getIdentity()); + } + + @Test + public void testCustomAuthToLocalRule() throws Exception + { + // Test with a custom rule that maps druid@EXAMPLE.COM -> druid + KerberosName.setRules("RULE:[1:$1@$0](.*@EXAMPLE.COM)s/@.*// DEFAULT"); + final KerberosName kerberosName = new KerberosName("myuser@EXAMPLE.COM"); + final String shortName = kerberosName.getShortName(); + + Assert.assertEquals("myuser", shortName); + + final AuthenticationToken token = new AuthenticationToken(shortName, "myuser@EXAMPLE.COM", "kerberos"); + + final AuthenticationResult result = new AuthenticationResult( + token.getUserName(), + "authorizer", + "kerberos", + null + ); + Assert.assertEquals("myuser", result.getIdentity()); + } + + @Test + public void testTwoComponentPrincipalRule() throws Exception + { + // Rule for two-component principals: extract the first component (service name) + KerberosName.setRules("RULE:[2:$1@$0](.*@EXAMPLE.COM)s/@.*//"); + final KerberosName kerberosName = new KerberosName("HTTP/host.example.com@EXAMPLE.COM"); + final String shortName = kerberosName.getShortName(); + + Assert.assertEquals("HTTP", shortName); + + final AuthenticationToken token = new AuthenticationToken( + shortName, + "HTTP/host.example.com@EXAMPLE.COM", + "kerberos" + ); + + final AuthenticationResult result = new AuthenticationResult( + token.getUserName(), + "authorizer", + "kerberos", + null + ); + Assert.assertEquals("HTTP", result.getIdentity()); + Assert.assertNotEquals("HTTP/host.example.com@EXAMPLE.COM", result.getIdentity()); + } + + private void verifyTokenIdentity(String expectedShortName, String fullPrincipal) + { + final AuthenticationToken token = new AuthenticationToken(expectedShortName, fullPrincipal, "kerberos"); + + // getUserName() should return the short name + Assert.assertEquals(expectedShortName, token.getUserName()); + // getName() should return the full principal + Assert.assertEquals(fullPrincipal, token.getName()); + + // AuthenticationResult identity must use the short name + final AuthenticationResult result = new AuthenticationResult( + token.getUserName(), + "testAuthorizer", + "kerberos", + null + ); + Assert.assertEquals(expectedShortName, result.getIdentity()); + } +} diff --git a/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorFilterTest.java b/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorFilterTest.java new file mode 100644 index 000000000000..4972bee1262d --- /dev/null +++ b/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorFilterTest.java @@ -0,0 +1,102 @@ +/* + * 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.kerberos; + +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.hadoop.security.authentication.server.AuthenticationToken; +import org.junit.Assert; +import org.junit.Test; + +public class KerberosAuthenticatorFilterTest +{ + @Test + public void testAuthenticationTokenGetUserNameReturnsShortName() + { + final String shortName = "druid"; + final String fullPrincipal = "druid@EXAMPLE.COM"; + final AuthenticationToken token = new AuthenticationToken(shortName, fullPrincipal, "kerberos"); + + Assert.assertEquals(shortName, token.getUserName()); + Assert.assertEquals(fullPrincipal, token.getName()); + } + + @Test + public void testAuthenticationResultUsesShortName() + { + final String shortName = "druid"; + final String fullPrincipal = "druid@EXAMPLE.COM"; + final String authorizerName = "testAuthorizer"; + final String authenticatorName = "kerberos"; + + final AuthenticationToken token = new AuthenticationToken(shortName, fullPrincipal, "kerberos"); + + // This mirrors the fix: using getUserName() instead of getName() + final AuthenticationResult result = new AuthenticationResult( + token.getUserName(), + authorizerName, + authenticatorName, + null + ); + + Assert.assertEquals( + "Identity should be the short name, not the full Kerberos principal", + shortName, + result.getIdentity() + ); + Assert.assertNotEquals( + "Identity should not be the full Kerberos principal", + fullPrincipal, + result.getIdentity() + ); + } + + @Test + public void testAuthenticationResultWithMultiComponentPrincipal() + { + final String shortName = "druid"; + final String fullPrincipal = "druid/admin@EXAMPLE.COM"; + final String authorizerName = "testAuthorizer"; + final String authenticatorName = "kerberos"; + + final AuthenticationToken token = new AuthenticationToken(shortName, fullPrincipal, "kerberos"); + + final AuthenticationResult result = new AuthenticationResult( + token.getUserName(), + authorizerName, + authenticatorName, + null + ); + + Assert.assertEquals(shortName, result.getIdentity()); + } + + @Test + public void testGetNameReturnsFullPrincipalNotShortName() + { + final String shortName = "user"; + final String fullPrincipal = "user@REALM.COM"; + final AuthenticationToken token = new AuthenticationToken(shortName, fullPrincipal, "kerberos"); + + // Verify getName() returns the full principal — this is what was being used before the fix + Assert.assertEquals(fullPrincipal, token.getName()); + // Verify getUserName() returns the short name — this is what the fix uses + Assert.assertEquals(shortName, token.getUserName()); + } +} diff --git a/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java b/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java index 406e34557f81..aa4dd1182002 100644 --- a/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java +++ b/extensions-core/druid-kerberos/src/test/java/org/apache/druid/security/kerberos/KerberosAuthenticatorTest.java @@ -21,6 +21,8 @@ import org.apache.druid.error.DruidException; import org.apache.druid.server.DruidNode; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.hadoop.security.authentication.server.AuthenticationToken; import org.junit.Assert; import org.junit.Test; @@ -98,4 +100,26 @@ public void testConstructorWithEmptyCookieSignatureSecret() exception.getMessage().contains("is not set") ); } + + @Test + public void testCreateAuthenticationResultUsesShortName() + { + KerberosAuthenticator authenticator = new KerberosAuthenticator( + TEST_SERVER_PRINCIPAL, + TEST_SERVER_KEYTAB, + TEST_AUTH_TO_LOCAL, + TEST_COOKIE_SECRET, + TEST_AUTHORIZER_NAME, + TEST_NAME, + createTestNode() + ); + + AuthenticationToken token = new AuthenticationToken("druid", "druid@EXAMPLE.COM", "kerberos"); + + AuthenticationResult result = authenticator.createAuthenticationResult(token); + + Assert.assertEquals("druid", result.getIdentity()); + Assert.assertEquals(TEST_AUTHORIZER_NAME, result.getAuthorizerName()); + Assert.assertEquals(TEST_NAME, result.getAuthenticatedBy()); + } }