Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/development/extensions-core/druid-kerberos.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>The DruidKerberosAuthenticationHandler creates tokens with:
* {@code new AuthenticationToken(userName, clientPrincipal, getType())}
* where userName is the result of KerberosName.getShortName() (applying authToLocal rules).
*
* <p>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());
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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());
}
}
Loading