Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#1185] feat(server-common): Server supports Kerberos authentication #1614

Merged
merged 36 commits into from
Jan 25, 2024
Merged
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
4 changes: 4 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@
Apache Hadoop
./dev/docker/hive/hadoop-env.sh
./dev/docker/hive/yarn-env.sh
./common/src/main/java/com/datastrato/gravitino/auth/KerberosUtils.java
./server-common/src/main/java/com/datastrato/gravitino/server/auth/KerberosAuthenticator.java
./server-common/src/test/java/com/datastrato/gravitino/server/auth/KerberosTestUtils.java
./server-common/src/main/java/com/datastrato/gravitino/server/auth/KerberosServerUtils.java

Apache Kafka
./core/src/main/java/com/datastrato/gravitino/utils/Bytes.java
Expand Down
1 change: 1 addition & 0 deletions LICENSE.bin
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@
Apache Groovy
Apache Yetus
Apache Yetus - Audience Annotations
Apache Kerby
Jackson JSON processor
DataNucleus
Modernizer Maven Plugin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@

package com.datastrato.gravitino.exceptions;

import com.google.common.collect.Lists;
import java.util.List;

/** Exception thrown when a user is not authorized to perform an action. */
public class UnauthorizedException extends GravitinoRuntimeException {

private final List<String> challenges = Lists.newArrayList();

public UnauthorizedException(String message) {
super(message);
}

public UnauthorizedException(String message, Throwable cause) {
super(message, cause);
}

public UnauthorizedException(String message, String challenge) {
super(message);
challenges.add(challenge);
}

public List<String> getChallenges() {
return challenges;
}
}
1 change: 1 addition & 0 deletions bin/gravitino.sh
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ if [ "$JVM_VERSION" -eq 17 ]; then
JAVA_OPTS+=" --add-opens java.base/sun.nio.cs=ALL-UNNAMED"
JAVA_OPTS+=" --add-opens java.base/sun.security.action=ALL-UNNAMED"
JAVA_OPTS+=" --add-opens java.base/sun.util.calendar=ALL-UNNAMED"
JAVA_OPTS+=" --add-opens", "java.security.jgss/sun.security.krb5=ALL-UNNAMED"
fi

addJarInDir "${GRAVITINO_HOME}/libs"
Expand Down
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ project.extra["extraJvmArgs"] = if (extra["jdkVersion"] in listOf("8", "11")) {
"--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED",
"--add-opens", "java.base/sun.nio.cs=ALL-UNNAMED",
"--add-opens", "java.base/sun.security.action=ALL-UNNAMED",
"--add-opens", "java.base/sun.util.calendar=ALL-UNNAMED"
"--add-opens", "java.base/sun.util.calendar=ALL-UNNAMED",
"--add-opens", "java.security.jgss/sun.security.krb5=ALL-UNNAMED"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should also update the start script under "bin".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ public interface AuthConstants {

String AUTHORIZATION_BASIC_HEADER = "Basic ";

String NEGOTIATE = "Negotiate";
String AUTHORIZATION_NEGOTIATE_HEADER = NEGOTIATE + " ";

String HTTP_CHALLENGE_HEADER = "WWW-Authenticate";

String ANONYMOUS_USER = "anonymous";

// Refer to the style of `AuthenticationFilter#AuthenticatedRoleAttributeName` of Apache Pulsar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
public enum AuthenticatorType {
NONE,
SIMPLE,
OAUTH
OAUTH,
KERBEROS
}
126 changes: 126 additions & 0 deletions common/src/main/java/com/datastrato/gravitino/auth/KerberosUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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 com.datastrato.gravitino.auth;

import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.Oid;

// Referred from Apache Hadoop KerberosTestUtils.java
// Remove the part methods
// Remove the support the IBM JDK
// Split method `doAs` into two methods
// hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/\
// authentication/KerberosTestUtils.java
public class KerberosUtils {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still the same thing here, please point out which part did you modify for what purpose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


private KerberosUtils() {}

public static final Oid GSS_SPNEGO_MECH_OID = getNumericOidInstance("1.3.6.1.5.5.2");
public static final Oid GSS_KRB5_MECH_OID = getNumericOidInstance("1.2.840.113554.1.2.2");
public static final Oid NT_GSS_KRB5_PRINCIPAL_OID =
getNumericOidInstance("1.2.840.113554.1.2.2.1");

// Numeric oids will never generate a GSSException for a malformed oid.
// Use to initialize statics.
private static Oid getNumericOidInstance(String oidName) {
try {
return new Oid(oidName);
} catch (GSSException ex) {
throw new IllegalArgumentException(ex);
}
}

public static LoginContext login(String principal, String keyTabFile) throws LoginException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need a logout method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will call LoginContext.logout

LoginContext loginContext = null;
Set<Principal> principals = new HashSet<>();
principals.add(new KerberosPrincipal(principal));
Subject subject = new Subject(false, principals, new HashSet<>(), new HashSet<>());
loginContext =
new LoginContext("", subject, null, new KerberosConfiguration(principal, keyTabFile));
loginContext.login();
return loginContext;
}

public static <T> T doAs(Subject subject, final Callable<T> callable) throws Exception {
try {
return Subject.doAs(
subject,
new PrivilegedExceptionAction<T>() {
@Override
public T run() throws Exception {
return callable.call();
}
});
} catch (PrivilegedActionException ex) {
throw ex.getException();
}
}

private static class KerberosConfiguration extends Configuration {
private final String principal;
private final String keyTabFile;

public KerberosConfiguration(String principal, String keyTabFile) {
this.principal = principal;
this.keyTabFile = keyTabFile;
}

@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, String> options = new HashMap<String, String>();

options.put("keyTab", keyTabFile);
options.put("principal", principal);
options.put("useKeyTab", "true");
options.put("storeKey", "true");
options.put("doNotPrompt", "true");
options.put("useTicketCache", "true");
options.put("renewTGT", "true");
options.put("refreshKrb5Config", "true");
options.put("isInitiator", "true");
String ticketCache = System.getenv("KRB5CCNAME");
if (ticketCache != null) {
options.put("ticketCache", ticketCache);
}
options.put("debug", "true");

return new AppConfigurationEntry[] {
new AppConfigurationEntry(
getKrb5LoginModuleName(),
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
options),
};
}
}

/* Return the Kerberos login module name */
public static String getKrb5LoginModuleName() {
return "com.sun.security.auth.module.Krb5LoginModule";
}
}
4 changes: 4 additions & 0 deletions docs/open-api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,7 @@ components:
type: http
scheme: basic

KerberosAuth:
type: http
scheme: negotiate

20 changes: 11 additions & 9 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,17 @@ GravitinoClient client = GravitinoClient.builder(uri)

### Server configuration

| Configuration item | Description | Default value | Required | Since version |
|---------------------------------------------------|-----------------------------------------------------------------------------|-------------------|-----------------------------------------|---------------|
| `gravitino.authenticator` | The authenticator which Gravitino uses, setting as `simple` or `oauth`. | `simple` | No | 0.3.0 |
| `gravitino.authenticator.oauth.serviceAudience` | The audience name when Gravitino uses OAuth as the authenticator. | `GravitinoServer` | No | 0.3.0 |
| `gravitino.authenticator.oauth.allowSkewSecs` | The JWT allows skew seconds when Gravitino uses OAuth as the authenticator. | `0` | No | 0.3.0 |
| `gravitino.authenticator.oauth.defaultSignKey` | The signing key of JWT when Gravitino uses OAuth as the authenticator. | (none) | Yes if use `oauth` as the authenticator | 0.3.0 |
| `gravitino.authenticator.oauth.signAlgorithmType` | The signature algorithm when Gravitino uses OAuth as the authenticator. | `RS256` | No | 0.3.0 |
| `gravitino.authenticator.oauth.serverUri` | The URI of the default OAuth server. | (none) | Yes if use `oauth` as the authenticator | 0.3.0 |
| `gravitino.authenticator.oauth.tokenPath` | The path for token of the default OAuth server. | (none) | Yes if use `oauth` as the authenticator | 0.3.0 |
| Configuration item | Description | Default value | Required | Since version |
|---------------------------------------------------|------------------------------------------------------------------------------------|-------------------|--------------------------------------------|---------------|
| `gravitino.authenticator` | The authenticator which Gravitino uses, setting as `simple`,`oauth` or `kerberos`. | `simple` | No | 0.3.0 |
| `gravitino.authenticator.oauth.serviceAudience` | The audience name when Gravitino uses OAuth as the authenticator. | `GravitinoServer` | No | 0.3.0 |
| `gravitino.authenticator.oauth.allowSkewSecs` | The JWT allows skew seconds when Gravitino uses OAuth as the authenticator. | `0` | No | 0.3.0 |
| `gravitino.authenticator.oauth.defaultSignKey` | The signing key of JWT when Gravitino uses OAuth as the authenticator. | (none) | Yes if use `oauth` as the authenticator | 0.3.0 |
| `gravitino.authenticator.oauth.signAlgorithmType` | The signature algorithm when Gravitino uses OAuth as the authenticator. | `RS256` | No | 0.3.0 |
| `gravitino.authenticator.oauth.serverUri` | The URI of the default OAuth server. | (none) | Yes if use `oauth` as the authenticator | 0.3.0 |
| `gravitino.authenticator.oauth.tokenPath` | The path for token of the default OAuth server. | (none) | Yes if use `oauth` as the authenticator | 0.3.0 |
| `gravitino.authenticator.kerberos.principal` | Indicates the Kerberos principal to be used for HTTP endpoint. | (none) | Yes if use `kerberos` as the authenticator | 0.4.0 |
| `gravitino.authenticator.kerberos.keytab` | Location of the keytab file with the credentials for the principal. | (none) | Yes if use `kerberos` as the authenticator | 0.4.0 |

The signature algorithms that Gravitino supports follows:

Expand Down
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ airlift-units = "1.8"
airlift-log = "231"
hive2 = "2.3.9"
hadoop2 = "2.10.2"
hadoop-minikdc = "3.3.6"
httpclient5 = "5.2.1"
mockserver = "5.15.0"
commons-lang3 = "3.14.0"
Expand Down Expand Up @@ -49,6 +50,7 @@ gradle-extensions-plugin = '1.74'
publish-plugin = '1.2.0'
rat-plugin = '0.8.0'
shadow-plugin = "8.1.1"
kerby = "2.0.3"
node-plugin = "7.0.1"
commons-cli = "1.2"

Expand All @@ -60,6 +62,8 @@ jackson-annotations = { group = "com.fasterxml.jackson.core", name = "jackson-an
jackson-datatype-jdk8 = { group = "com.fasterxml.jackson.datatype", name = "jackson-datatype-jdk8", version.ref = "jackson" }
jackson-datatype-jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-datatype-jsr310", version.ref = "jackson" }
guava = { group = "com.google.guava", name = "guava", version.ref = "guava" }
kerby-core = { group = "org.apache.kerby", name = "kerb-core", version.ref = "kerby"}
kerby-simplekdc = { group = "org.apache.kerby", name = "kerb-simplekdc", version.ref = "kerby"}
lombok = { group = "org.projectlombok", name = "lombok", version.ref = "lombok" }
junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
junit-jupiter-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
Expand Down Expand Up @@ -135,6 +139,7 @@ prometheus-servlet = { group = "io.prometheus", name = "simpleclient_servlet", v
jsqlparser = { group = "com.github.jsqlparser", name = "jsqlparser", version.ref = "jsqlparser" }
mysql-driver = { group = "mysql", name = "mysql-connector-java", version.ref = "mysql" }
postgresql-driver = { group = "org.postgresql", name = "postgresql", version.ref = "postgresql" }
minikdc = { group = "org.apache.hadoop", name = "hadoop-minikdc", version.ref = "hadoop-minikdc"}
immutables-value = { module = "org.immutables:value", version.ref = "immutables-value" }
commons-cli = { group = "commons-cli", name = "commons-cli", version.ref = "commons-cli" }

Expand All @@ -146,6 +151,7 @@ iceberg = ["iceberg-core", "iceberg-api"]
jwt = ["jwt-api", "jwt-impl", "jwt-gson"]
metrics = ["metrics-core", "metrics-jersey2", "metrics-jvm", "metrics-jmx", "metrics-servlets"]
prometheus = ["prometheus-servlet", "prometheus-dropwizard", "prometheus-client"]
kerby = ["kerby-core", "kerby-simplekdc"]

[plugins]
protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" }
Expand Down
4 changes: 4 additions & 0 deletions server-common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ dependencies {
implementation(libs.bundles.jetty)
implementation(libs.bundles.jwt)
implementation(libs.bundles.metrics)
implementation(libs.bundles.kerby)
implementation(libs.prometheus.servlet)

testImplementation(libs.junit.jupiter.api)
testImplementation(libs.junit.jupiter.params)
testRuntimeOnly(libs.junit.jupiter.engine)
testImplementation(libs.mockito.core)
testImplementation(libs.commons.io)
testImplementation(libs.minikdc) {
exclude("org.apache.directory.api", "api-ldap-schema-data")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
chain.doFilter(request, response);
} catch (UnauthorizedException ue) {
HttpServletResponse resp = (HttpServletResponse) response;
if (!ue.getChallenges().isEmpty()) {
// For some authentication, HTTP response can provide some challenge information
// to let client to create correct authenticated request.
// Refer to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate
for (String challenge : ue.getChallenges()) {
resp.setHeader(AuthConstants.HTTP_CHALLENGE_HEADER, challenge);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add more comment here about the reason of this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, ue.getMessage());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ public class AuthenticatorFactory {
AuthenticatorType.SIMPLE.name().toLowerCase(),
SimpleAuthenticator.class.getCanonicalName(),
AuthenticatorType.OAUTH.name().toLowerCase(),
OAuth2TokenAuthenticator.class.getCanonicalName());
OAuth2TokenAuthenticator.class.getCanonicalName(),
AuthenticatorType.KERBEROS.name().toLowerCase(),
KerberosAuthenticator.class.getCanonicalName());

private AuthenticatorFactory() {}

Expand Down
Loading
Loading