Skip to content

Commit

Permalink
[#2711] feat(filesystem): Support Kerberos client authentication in g…
Browse files Browse the repository at this point in the history
…vfs (#3340)

### What changes were proposed in this pull request?

Support using Kerberos authentication type to initialize Gravitino
client in gvfs.

### Why are the changes needed?

Fix: #2711 

### How was this patch tested?

Add some uts for: 
1. use principal and keytab to auth.
2. kerberos configs for gvfs.
3. some invalid kerberos case.

Test locally and use `kerberos ticket cache` to initialize gvfs. The
steps are as follows:
1. Deploy the KDC server locally, refer to the doc:
https://blog.csdn.net/lo085213/article/details/105057186.
2. Register the service account `HTTP/localhost@HADOOP.COM` and client
account `client@HADOOP.COM` in the KDC server.
3. Execute the `kinit -kt client.keytab client@HADOOP.COM` command
locally.
4. Use the `klist` command to check the environment for tickets
containing `client@HADOOP.COM`.
5. Write a unit test to load metalake through gvfs with the kerberos
ticket cache.

![image](https://github.com/datastrato/gravitino/assets/26177232/f655e687-8412-4000-bb07-bd9ccadd8387)

![image](https://github.com/datastrato/gravitino/assets/26177232/a3d36646-37ad-44b9-8cca-129a18196663)

![image](https://github.com/datastrato/gravitino/assets/26177232/df7504a2-046d-45fa-9da3-7b681ebfd7e1)

Co-authored-by: xloya <982052490@qq.com>
Co-authored-by: xiaojiebao <xiaojiebao@xiaomi.com>
  • Loading branch information
3 people committed May 11, 2024
1 parent abba6ef commit 636a43e
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 16 deletions.
1 change: 1 addition & 0 deletions clients/filesystem-hadoop3/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
testImplementation(libs.hadoop3.common)
testImplementation(libs.junit.jupiter.api)
testImplementation(libs.junit.jupiter.params)
testImplementation(libs.minikdc)
testImplementation(libs.mockito.core)
testImplementation(libs.mockserver.netty) {
exclude("com.google.guava", "guava")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.datastrato.gravitino.NameIdentifier;
import com.datastrato.gravitino.client.DefaultOAuth2TokenProvider;
import com.datastrato.gravitino.client.GravitinoClient;
import com.datastrato.gravitino.client.KerberosTokenProvider;
import com.datastrato.gravitino.file.Fileset;
import com.datastrato.gravitino.shaded.com.google.common.annotations.VisibleForTesting;
import com.datastrato.gravitino.shaded.com.google.common.base.Preconditions;
Expand All @@ -17,6 +18,7 @@
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Scheduler;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
Expand Down Expand Up @@ -205,6 +207,36 @@ private void initializeClient(Configuration configuration) {
.withMetalake(metalakeName)
.withOAuth(authDataProvider)
.build();
} else if (authType.equalsIgnoreCase(
GravitinoVirtualFileSystemConfiguration.KERBEROS_AUTH_TYPE)) {
String principal =
configuration.get(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_PRINCIPAL_KEY);
checkAuthConfig(
GravitinoVirtualFileSystemConfiguration.KERBEROS_AUTH_TYPE,
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_PRINCIPAL_KEY,
principal);
String keytabFilePath =
configuration.get(
GravitinoVirtualFileSystemConfiguration
.FS_GRAVITINO_CLIENT_KERBEROS_KEYTAB_FILE_PATH_KEY);
KerberosTokenProvider authDataProvider;
if (StringUtils.isNotBlank(keytabFilePath)) {
// Using principal and keytab to create auth provider
authDataProvider =
KerberosTokenProvider.builder()
.withClientPrincipal(principal)
.withKeyTabFile(new File(keytabFilePath))
.build();
} else {
// Using ticket cache to create auth provider
authDataProvider = KerberosTokenProvider.builder().withClientPrincipal(principal).build();
}
this.client =
GravitinoClient.builder(serverUri)
.withMetalake(metalakeName)
.withKerberosAuth(authDataProvider)
.build();
} else {
throw new IllegalArgumentException(
String.format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class GravitinoVirtualFileSystemConfiguration {

public static final String SIMPLE_AUTH_TYPE = "simple";
public static final String OAUTH2_AUTH_TYPE = "oauth2";

public static final String KERBEROS_AUTH_TYPE = "kerberos";
// oauth2
/** The configuration key for the URI of the default OAuth server. */
public static final String FS_GRAVITINO_CLIENT_OAUTH2_SERVER_URI_KEY =
Expand All @@ -38,6 +38,14 @@ class GravitinoVirtualFileSystemConfiguration {
public static final String FS_GRAVITINO_CLIENT_OAUTH2_SCOPE_KEY =
"fs.gravitino.client.oauth2.scope";

/** The configuration key for the principal. */
public static final String FS_GRAVITINO_CLIENT_KERBEROS_PRINCIPAL_KEY =
"fs.gravitino.client.kerberos.principal";

/** The configuration key for the keytab file path corresponding to the principal. */
public static final String FS_GRAVITINO_CLIENT_KERBEROS_KEYTAB_FILE_PATH_KEY =
"fs.gravitino.client.kerberos.keytabFilePath";

/** The configuration key for the maximum capacity of the Gravitino fileset cache. */
public static final String FS_GRAVITINO_FILESET_CACHE_MAX_CAPACITY_KEY =
"fs.gravitino.fileset.cache.maxCapacity";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
public abstract class GravitinoMockServerBase {
private static final ObjectMapper MAPPER = JsonUtils.objectMapper();
private static ClientAndServer mockServer;
private static final String MOCK_SERVER_HOST = "http://127.0.0.1:";
private static final String MOCK_SERVER_HOST = "http://localhost:";
private static int port;
protected static final String metalakeName = "metalake_1";
protected static final String catalogName = "fileset_catalog_1";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2024 Datastrato Pvt Ltd.
* This software is licensed under the Apache License version 2.
*/

package com.datastrato.gravitino.filesystem.hadoop;

import java.io.File;
import java.util.UUID;
import org.apache.hadoop.minikdc.KerberosSecurityTestcase;

public class KdcServerBase extends KerberosSecurityTestcase {
private static final KerberosSecurityTestcase INSTANCE = new KerberosSecurityTestcase();
private static final String CLIENT_PRINCIPAL = "client@EXAMPLE.COM";
private static final String SERVER_PRINCIPAL = "HTTP/localhost@EXAMPLE.COM";
private static final String KEYTAB_FILE =
new File(System.getProperty("test.dir", "target"), UUID.randomUUID().toString())
.getAbsolutePath();

static {
try {
INSTANCE.startMiniKdc();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private KdcServerBase() {}

public static void stopKdc() {
INSTANCE.stopMiniKdc();
}

public static void initKeyTab() throws Exception {
File keytabFile = new File(KEYTAB_FILE);
String clientPrincipal = removeRealm(CLIENT_PRINCIPAL);
String serverPrincipal = removeRealm(SERVER_PRINCIPAL);
INSTANCE.getKdc().createPrincipal(keytabFile, clientPrincipal, serverPrincipal);
}

private static String removeRealm(String principal) {
return principal.substring(0, principal.lastIndexOf("@"));
}

public static String getServerPrincipal() {
return SERVER_PRINCIPAL;
}

public static String getClientPrincipal() {
return CLIENT_PRINCIPAL;
}

public static String getKeytabFile() {
return KEYTAB_FILE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Copyright 2024 Datastrato Pvt Ltd.
* This software is licensed under the Apache License version 2.
*/
package com.datastrato.gravitino.filesystem.hadoop;

import static com.datastrato.gravitino.server.authentication.KerberosConfig.KEYTAB;
import static com.datastrato.gravitino.server.authentication.KerberosConfig.PRINCIPAL;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockserver.model.HttpResponse.response;

import com.datastrato.gravitino.Config;
import com.datastrato.gravitino.server.authentication.KerberosAuthenticator;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Method;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockserver.matchers.Times;
import org.mockserver.model.Header;
import org.mockserver.model.HttpRequest;

public class TestKerberosClient extends TestGvfsBase {

@BeforeAll
public static void setup() {
try {
KdcServerBase.initKeyTab();
} catch (Exception e) {
throw new RuntimeException(e);
}
TestGvfsBase.setup();
conf.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_AUTH_TYPE_KEY,
GravitinoVirtualFileSystemConfiguration.KERBEROS_AUTH_TYPE);
conf.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_PRINCIPAL_KEY,
KdcServerBase.getClientPrincipal());
conf.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_KEYTAB_FILE_PATH_KEY,
KdcServerBase.getKeytabFile());
}

@AfterAll
public static void teardown() {
KdcServerBase.stopKdc();
}

@Test
public void testAuthConfigs() {
// init conf
Configuration configuration = new Configuration();
configuration.set(
String.format(
"fs.%s.impl.disable.cache", GravitinoVirtualFileSystemConfiguration.GVFS_SCHEME),
"true");
configuration.set("fs.gvfs.impl", GVFS_IMPL_CLASS);
configuration.set("fs.AbstractFileSystem.gvfs.impl", GVFS_ABSTRACT_IMPL_CLASS);
configuration.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_METALAKE_KEY, metalakeName);
configuration.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_SERVER_URI_KEY,
GravitinoMockServerBase.serverUri());

// set auth type, but do not set other configs
configuration.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_AUTH_TYPE_KEY,
GravitinoVirtualFileSystemConfiguration.KERBEROS_AUTH_TYPE);
assertThrows(
IllegalArgumentException.class, () -> managedFilesetPath.getFileSystem(configuration));

// set not exist keytab path
configuration.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_KEYTAB_FILE_PATH_KEY,
"file://tmp/test.keytab");
assertThrows(
IllegalArgumentException.class, () -> managedFilesetPath.getFileSystem(configuration));
}

@Test
public void testAuthWithPrincipalAndKeytabNormally() throws Exception {
KerberosAuthenticator kerberosAuthenticator = new KerberosAuthenticator();
Config config = new Config(false) {};
config.set(PRINCIPAL, KdcServerBase.getServerPrincipal());
config.set(KEYTAB, KdcServerBase.getKeytabFile());
kerberosAuthenticator.initialize(config);

Configuration configuration = new Configuration(conf);
configuration.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_PRINCIPAL_KEY,
KdcServerBase.getClientPrincipal());
configuration.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_KEYTAB_FILE_PATH_KEY,
KdcServerBase.getKeytabFile());

// mock load metalake with principal and keytab
String testMetalake = "test_kerberos_normally";
HttpRequest mockRequest =
HttpRequest.request("/api/metalakes/" + testMetalake)
.withMethod(Method.GET.name())
.withQueryStringParameters(Collections.emptyMap());
GravitinoMockServerBase.mockServer()
.when(mockRequest, Times.unlimited())
.respond(
httpRequest -> {
List<Header> headers = httpRequest.getHeaders().getEntries();
for (Header header : headers) {
if (header.getName().equalsIgnoreCase("Authorization")) {
byte[] tokenValue =
header.getValues().get(0).getValue().getBytes(StandardCharsets.UTF_8);
kerberosAuthenticator.authenticateToken(tokenValue);
}
}
return response().withStatusCode(HttpStatus.SC_OK);
});
Path newPath = new Path(managedFilesetPath.toString().replace(metalakeName, testMetalake));
// Should auth successfully
newPath.getFileSystem(configuration);
}

@Test
public void testAuthWithInvalidInfo() throws Exception {
KerberosAuthenticator kerberosAuthenticator = new KerberosAuthenticator();
Config config = new Config(false) {};
config.set(PRINCIPAL, KdcServerBase.getServerPrincipal());
config.set(KEYTAB, KdcServerBase.getKeytabFile());
kerberosAuthenticator.initialize(config);

// test with invalid principal and keytab
Configuration conf1 = new Configuration(conf);
conf1.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_PRINCIPAL_KEY,
"invalid@EXAMPLE.COM");
conf1.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_KEYTAB_FILE_PATH_KEY,
KdcServerBase.getKeytabFile());

String testMetalake = "test_invalid";
HttpRequest mockRequest =
HttpRequest.request("/api/metalakes/" + testMetalake)
.withMethod(Method.GET.name())
.withQueryStringParameters(Collections.emptyMap());
GravitinoMockServerBase.mockServer()
.when(mockRequest, Times.unlimited())
.respond(
httpRequest -> {
List<Header> headers = httpRequest.getHeaders().getEntries();
for (Header header : headers) {
if (header.getName().equalsIgnoreCase("Authorization")) {
byte[] tokenValue =
header.getValues().get(0).getValue().getBytes(StandardCharsets.UTF_8);
kerberosAuthenticator.authenticateToken(tokenValue);
}
}
return response().withStatusCode(HttpStatus.SC_OK);
});
Path newPath = new Path(managedFilesetPath.toString().replace(metalakeName, testMetalake));
Assertions.assertThrows(IllegalStateException.class, () -> newPath.getFileSystem(conf1));

// test with principal and invalid keytab
File invalidKeytabFile =
new File(System.getProperty("test.dir", "target"), UUID.randomUUID().toString());
if (invalidKeytabFile.exists()) {
invalidKeytabFile.delete();
}
invalidKeytabFile.createNewFile();

Configuration conf2 = new Configuration(conf);
conf2.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_PRINCIPAL_KEY,
KdcServerBase.getClientPrincipal());
conf2.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_KEYTAB_FILE_PATH_KEY,
invalidKeytabFile.getAbsolutePath());
Assertions.assertThrows(IllegalStateException.class, () -> newPath.getFileSystem(conf2));
invalidKeytabFile.delete();

// test with principal and no keytab
Configuration conf3 = new Configuration(conf);
conf3.set(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_PRINCIPAL_KEY,
KdcServerBase.getClientPrincipal());
// remove keytab configuration
conf3.unset(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_CLIENT_KERBEROS_KEYTAB_FILE_PATH_KEY);
Assertions.assertThrows(IllegalStateException.class, () -> newPath.getFileSystem(conf3));
}
}
Loading

0 comments on commit 636a43e

Please sign in to comment.