This is an automated email from the ASF dual-hosted git repository.
elserj pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite-avatica.git
The following commit(s) were added to refs/heads/master by this push:
new c3a9192 [CALCITE-4152] Upgrade Avatica to use the configurable SPNEGO
Jetty implementation
c3a9192 is described below
commit c3a9192347b4354337a906838499ff25236d63bc
Author: Josh Elser <[email protected]>
AuthorDate: Thu Dec 31 23:28:15 2020 -0500
[CALCITE-4152] Upgrade Avatica to use the configurable SPNEGO Jetty
implementation
Jetty has deprecated the previously-used version of SPNEGO login code.
This change requires a few other changes to adopt:
1. Removal of automatic server login via JAAS (Jetty removed this and
expects explicit logins for the server).
2. Separation of Authentication and Authorization (we're required to
use a LoginService for authz to use the new SPNEGO authentication).
For the benefit of making this change, we automatically inherit the
Jetty Session logic which can skip SPNEGO authentication for the 2nd
to Nth call to Avatica. For a "workload" which previously took N HTTP calls
to Avatica to perform, this can now be done in (N/2)+1 HTTP calls
which, for average Avatica calls, results in a nearly 2x speed-up.
Jetty Sessions will cause a JSESSIONID cookie to be sent back on the
successful SPNEGO authentication handshake. As long as the client
resubmits this cookie for subsequent requests, the identity of the
client is kept intact.
To test this more easily, this change also includes updates to the
Avatica StandaloneServer, which more easily enables setup of Avatica
against any database (e.g. hsqldb with the SCOTT dataset).
---
gradle.properties | 2 +-
.../avatica/server/AvaticaServerConfiguration.java | 20 ++
.../calcite/avatica/server/AvaticaUserStore.java | 45 ++++
.../apache/calcite/avatica/server/HttpServer.java | 134 ++++++++---
.../apache/calcite/avatica/AvaticaSpnegoTest.java | 10 +-
.../org/apache/calcite/avatica/SpnegoTestUtil.java | 3 +-
.../avatica/server/CustomAuthHttpServerTest.java | 20 +-
...ueryStringParameterRemoteUserExtractorTest.java | 8 +
.../avatica/server/HttpServerBuilderTest.java | 146 ------------
.../server/HttpServerSpnegoWithJaasTest.java | 255 ---------------------
.../server/HttpServerSpnegoWithoutJaasTest.java | 6 +-
site/_docs/security.md | 50 +++-
standalone-server/build.gradle.kts | 5 +-
.../avatica/standalone/StandaloneServer.java | 38 ++-
.../src/main/resources/log4j.properties | 4 +-
15 files changed, 290 insertions(+), 456 deletions(-)
diff --git a/gradle.properties b/gradle.properties
index ddfbbbf..5de965a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -69,7 +69,7 @@ httpcore.version=4.4.11
jackson.version=2.10.0
jcip-annotations.version=1.0-1
jcommander.version=1.72
-jetty.version=9.4.42.v20210604
+jetty.version=9.4.44.v20210927
junit.version=4.12
kerby.version=1.1.1
mockito.version=2.23.4
diff --git
a/server/src/main/java/org/apache/calcite/avatica/server/AvaticaServerConfiguration.java
b/server/src/main/java/org/apache/calcite/avatica/server/AvaticaServerConfiguration.java
index 56860ec..f3ab986 100644
---
a/server/src/main/java/org/apache/calcite/avatica/server/AvaticaServerConfiguration.java
+++
b/server/src/main/java/org/apache/calcite/avatica/server/AvaticaServerConfiguration.java
@@ -18,6 +18,7 @@ package org.apache.calcite.avatica.server;
import org.apache.calcite.avatica.remote.AuthenticationType;
+import java.io.File;
import java.util.concurrent.Callable;
/**
@@ -40,6 +41,18 @@ public interface AvaticaServerConfiguration {
String getKerberosRealm();
/**
+ * Returns the "primary" component of the Kerberos principal for the Avatica
server,
+ * e.g. primary/instance@REALM
+ */
+ String getKerberosServiceName();
+
+ /**
+ * Returns the "instance" component of the Kerberos principal for the
Avatica server,
+ * e.g. primary/instance@REALM
+ */
+ String getKerberosHostName();
+
+ /**
* Returns the Kerberos principal that the Avatica server should log in as.
*
* @return A Kerberos principal, or null if not applicable.
@@ -47,6 +60,13 @@ public interface AvaticaServerConfiguration {
String getKerberosPrincipal();
/**
+ * Returns the file to the server's Kerberos keytab.
+ */
+ default File getKerberosKeytab() {
+ return null;
+ }
+
+ /**
* Returns the array of allowed roles for login. Only applicable when
* {@link #getAuthenticationType()} returns {@link AuthenticationType#BASIC}
or
* {@link AuthenticationType#DIGEST}.
diff --git
a/server/src/main/java/org/apache/calcite/avatica/server/AvaticaUserStore.java
b/server/src/main/java/org/apache/calcite/avatica/server/AvaticaUserStore.java
new file mode 100644
index 0000000..18104cf
--- /dev/null
+++
b/server/src/main/java/org/apache/calcite/avatica/server/AvaticaUserStore.java
@@ -0,0 +1,45 @@
+/*
+ * 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.calcite.avatica.server;
+
+import org.eclipse.jetty.security.UserStore;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.security.Credential;
+
+/**
+ * Implementation of UserStore which creates users when they do not already
exist.
+ */
+public class AvaticaUserStore extends UserStore {
+ private static final Credential USER_CREDENTIAL =
Credential.getCredential("");
+
+ public static final String AVATICA_USER_ROLE = "avatica-user";
+
+ private static final String[] USER_ROLES = new String[] {AVATICA_USER_ROLE};
+
+ @Override public UserIdentity getUserIdentity(String userName) {
+ UserIdentity userId = super.getUserIdentity(userName);
+ if (userId != null) {
+ return userId;
+ }
+
+ // Do we need to be concerned about the recursion?
+ addUser(userName, USER_CREDENTIAL, USER_ROLES);
+ return getUserIdentity(userName);
+ }
+}
+
+// End AvaticaUserStore.java
diff --git
a/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java
b/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java
index c094fc8..87dc73e 100644
--- a/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java
+++ b/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java
@@ -23,11 +23,14 @@ import org.apache.calcite.avatica.remote.Service;
import org.apache.calcite.avatica.remote.Service.RpcMetadataResponse;
import org.eclipse.jetty.security.Authenticator;
+import org.eclipse.jetty.security.ConfigurableSpnegoLoginService;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.LoginService;
+import org.eclipse.jetty.security.authentication.AuthorizationService;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
+import
org.eclipse.jetty.security.authentication.ConfigurableSpnegoAuthenticator;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.server.AbstractConnectionFactory;
import org.eclipse.jetty.server.Connector;
@@ -37,6 +40,8 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.server.session.DefaultSessionIdManager;
+import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@@ -48,7 +53,10 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.Principal;
import java.security.PrivilegedAction;
+import java.security.SecureRandom;
+import java.time.Duration;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -275,7 +283,10 @@ public class HttpServer {
if (null != config) {
ConstraintSecurityHandler securityHandler = getSecurityHandler();
securityHandler.setHandler(handler);
- avaticaHandler = securityHandler;
+ // SPNEGO requires a session
+ SessionHandler sessionHandler = new SessionHandler();
+ sessionHandler.setHandler(securityHandler);
+ avaticaHandler = sessionHandler;
}
handlerList.setHandlers(new Handler[] {avaticaHandler, new
DefaultHandler()});
@@ -337,31 +348,45 @@ public class HttpServer {
protected ConstraintSecurityHandler configureSpnego(Server server,
AvaticaServerConfiguration config) {
final String realm = Objects.requireNonNull(config.getKerberosRealm());
- final String principal =
Objects.requireNonNull(config.getKerberosPrincipal());
+
+ // DefaultSessionIdManager uses SecureRandom, but we can be explicit about
that.
+ server.setSessionIdManager(new DefaultSessionIdManager(server, new
SecureRandom()));
+
+ // We rely on SPNEGO to authenticate the users with valid Kerberos
identities. We
+ // do not require a _specific_ Kerberos identity in order to authenticate
with
+ // Avatica. AvaticaUserStore will assign the role "avatica-user" to every
SPNEGO-authenticated
+ // user, and then ConfigurableSpnegoAuthenticator will check that role.
+ //
+ // This setup adds nothing but complexity to Avatica, but Jetty removed the
+ // functionality to not have this layer of indirection. It paves the way
for
+ // flexibility in having "user" centric HTTP endpoints and "admin" centric
+ // HTTP endpoints which Avatica can authorize appropriately.
+ final AvaticaUserStore userStore = new AvaticaUserStore();
+ LOG.info("Instantiating HashLoginService with {}", realm);
+ // Passing the Kerberos Realm here was previously important, but is not
critical any longer.
+ final HashLoginService authz = new HashLoginService(realm);
+ authz.setUserStore(userStore);
// A customization of SpnegoLoginService to explicitly set the server's
principal, otherwise
// we would have to require a custom file to set the server's principal.
- PropertyBasedSpnegoLoginService spnegoLoginService =
- new PropertyBasedSpnegoLoginService(realm, principal);
-
- // Roles are "realms" for Kerberos/SPNEGO
- final String[] allowedRealms = getAllowedRealms(realm, config);
+ ConfigurableSpnegoLoginService spnegoLoginService =
+ new ConfigurableSpnegoLoginService(realm,
AuthorizationService.from(authz, ""));
+ // Why? The Jetty unit test does it.
+ spnegoLoginService.addBean(authz);
+ spnegoLoginService.setServiceName(config.getKerberosServiceName());
+ spnegoLoginService.setHostName(config.getKerberosHostName());
+ spnegoLoginService.setKeyTabPath(config.getKerberosKeytab().toPath());
+
+ // The Authenticator independently validates what role(s) the authenticated
+ // user has and authorizes them to access the HTTP resources. We use
"avatica-user"
+ // as the role to check.
+ final String[] allowedRealms = new String[]
{AvaticaUserStore.AVATICA_USER_ROLE};
+
+ final ConfigurableSpnegoAuthenticator spnegoAuthn = new
ConfigurableSpnegoAuthenticator();
+ spnegoAuthn.setAuthenticationDuration(Duration.ofMinutes(5));
return configureCommonAuthentication(Constraint.__SPNEGO_AUTH,
- allowedRealms, new AvaticaSpnegoAuthenticator(), realm,
spnegoLoginService);
- }
-
- protected String[] getAllowedRealms(String serverRealm,
AvaticaServerConfiguration config) {
- // Roles are "realms" for Kerberos/SPNEGO
- String[] allowedRealms = new String[] {serverRealm};
- // By default, only the server's realm is allowed, but other realms can
also be allowed.
- if (null != config.getAllowedRoles()) {
- allowedRealms = new String[config.getAllowedRoles().length + 1];
- allowedRealms[0] = serverRealm;
- System.arraycopy(config.getAllowedRoles(), 0, allowedRealms, 1,
- config.getAllowedRoles().length);
- }
- return allowedRealms;
+ allowedRealms, spnegoAuthn, realm, spnegoLoginService);
}
protected ConstraintSecurityHandler configureBasicAuthentication(Server
server,
@@ -404,13 +429,12 @@ public class HttpServer {
cm.setConstraint(constraint);
cm.setPathSpec("/*");
- ConstraintSecurityHandler sh = new ConstraintSecurityHandler();
- sh.setAuthenticator(authenticator);
- sh.setLoginService(loginService);
- sh.setConstraintMappings(new ConstraintMapping[]{cm});
- sh.setRealmName(realm);
+ ConstraintSecurityHandler securityHandler = new
ConstraintSecurityHandler();
+ securityHandler.setAuthenticator(authenticator);
+ securityHandler.setLoginService(loginService);
+ securityHandler.setConstraintMappings(new ConstraintMapping[]{cm});
- return sh;
+ return securityHandler;
}
/**
@@ -574,7 +598,9 @@ public class HttpServer {
* @param additionalAllowedRealms Any additional realms, other than the
server's realm, which
* should be allowed to authenticate against the server. Can be null.
* @return <code>this</code>
+ * @deprecated Since 1.20.0, because {@code additionalAllowedRealms} is no
longer considered.
*/
+ @Deprecated
public Builder<T> withSpnego(String principal, String[]
additionalAllowedRealms) {
int index = Objects.requireNonNull(principal).lastIndexOf('@');
if (-1 == index) {
@@ -613,12 +639,20 @@ public class HttpServer {
* @param additionalAllowedRealms Any additional realms, other than the
server's realm, which
* should be allowed to authenticate against the server. Can be null.
* @return <code>this</code>
+ * @deprecated since 1.20.0 because {@code additionalAllowedRealms} is no
longer considered.
*/
+ @Deprecated
public Builder<T> withSpnego(String principal, String realm, String[]
additionalAllowedRealms) {
this.authenticationType = AuthenticationType.SPNEGO;
this.kerberosPrincipal = Objects.requireNonNull(principal);
this.kerberosRealm = Objects.requireNonNull(realm);
+ if (additionalAllowedRealms != null) {
+ LOG.warn("Avatica no longer support additionalAllowedRealms as the
Jetty SPNEGO"
+ + " implementation does not adhere to it. All authenticateable
realms are allowed: {}",
+ Arrays.toString(additionalAllowedRealms));
+ }
this.loginServiceAllowedRoles = additionalAllowedRealms;
+
return this;
}
@@ -653,7 +687,7 @@ public class HttpServer {
* @return <code>this</code>
*/
- public Builder withRemoteUserExtractor(RemoteUserExtractor
remoteUserExtractor) {
+ public Builder<T> withRemoteUserExtractor(RemoteUserExtractor
remoteUserExtractor) {
this.remoteUserExtractor = Objects.requireNonNull(remoteUserExtractor);
return this;
}
@@ -781,13 +815,8 @@ public class HttpServer {
handler = buildHandler(this, serverConfig);
break;
case SPNEGO:
- if (null != keytab) {
- LOG.debug("Performing Kerberos login with {} as {}", keytab,
kerberosPrincipal);
- subject = loginViaKerberos(this);
- } else {
- LOG.debug("Not performing Kerberos login");
- subject = null;
- }
+ LOG.debug("Not performing Kerberos login, Jetty does this now");
+ subject = null;
serverConfig = buildSpnegoConfiguration(this);
handler = buildHandler(this, serverConfig);
break;
@@ -854,7 +883,22 @@ public class HttpServer {
*/
private AvaticaServerConfiguration buildSpnegoConfiguration(Builder b) {
final String principal = b.kerberosPrincipal;
+ final int separatorIndex = principal.indexOf('/');
+ if (separatorIndex < 1) {
+ throw new RuntimeException("Expected principal to be of the form
primary/instance"
+ + " but got " + principal);
+ }
+ final String primary = principal.substring(0, separatorIndex);
+ final int atSignIndex = principal.indexOf('@');
+ final String instance;
+ // Trim off the @REALM if it's present
+ if (atSignIndex == -1) {
+ instance = principal.substring(separatorIndex + 1);
+ } else {
+ instance = principal.substring(separatorIndex + 1, atSignIndex);
+ }
final String realm = b.kerberosRealm;
+ final File keytab = b.keytab;
final String[] additionalAllowedRealms = b.loginServiceAllowedRoles;
final DoAsRemoteUserCallback callback = b.remoteUserCallback;
final RemoteUserExtractor remoteUserExtractor = b.remoteUserExtractor;
@@ -872,6 +916,18 @@ public class HttpServer {
return principal;
}
+ @Override public String getKerberosServiceName() {
+ return primary;
+ }
+
+ @Override public String getKerberosHostName() {
+ return instance;
+ }
+
+ @Override public File getKerberosKeytab() {
+ return keytab;
+ }
+
@Override public boolean supportsImpersonation() {
return null != callback;
}
@@ -933,6 +989,14 @@ public class HttpServer {
return null;
}
+ @Override public String getKerberosServiceName() {
+ return null;
+ }
+
+ @Override public String getKerberosHostName() {
+ return null;
+ }
+
@Override public boolean supportsImpersonation() {
return false;
}
diff --git
a/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java
b/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java
index 0d1972f..66821c8 100644
--- a/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java
@@ -65,6 +65,12 @@ public class AvaticaSpnegoTest extends HttpBaseTest {
private static boolean isKdcStarted = false;
private static void setupKdc() throws Exception {
+ System.setProperty("sun.security.krb5.debug", "true");
+ System.setProperty("sun.security.jgss.debug", "true");
+ System.setProperty("sun.security.spnego.debug", "true");
+ System.setProperty("java.security.debug", "all");
+ System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
+
if (isKdcStarted) {
return;
}
@@ -108,9 +114,9 @@ public class AvaticaSpnegoTest extends HttpBaseTest {
clientConfig.setString(KrbConfigKey.DEFAULT_REALM, SpnegoTestUtil.REALM);
// Kerby sets "java.security.krb5.conf" for us!
+ // useSubjectCredsOnly is only important when we expect JAAS to go
"looking"
+ // for credentials on our behalf.
System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
- //System.setProperty("sun.security.spnego.debug", "true");
- //System.setProperty("sun.security.krb5.debug", "true");
}
@AfterClass public static void stopKdc() throws KrbException {
diff --git
a/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java
b/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java
index 2c5c763..03857d8 100644
--- a/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java
+++ b/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java
@@ -55,7 +55,8 @@ public class SpnegoTestUtil {
public static final String REALM = "EXAMPLE.COM";
public static final String KDC_HOST = "localhost";
- public static final String CLIENT_PRINCIPAL = "client@" + REALM;
+ public static final String CLIENT_NAME = "client";
+ public static final String CLIENT_PRINCIPAL = CLIENT_NAME + "@" + REALM;
public static final String SERVER_PRINCIPAL = "HTTP/" + KDC_HOST + "@" +
REALM;
private static final String TARGET_DIR_NAME =
System.getProperty("target.dir", "target");
diff --git
a/server/src/test/java/org/apache/calcite/avatica/server/CustomAuthHttpServerTest.java
b/server/src/test/java/org/apache/calcite/avatica/server/CustomAuthHttpServerTest.java
index 464da84..5a9ee03 100644
---
a/server/src/test/java/org/apache/calcite/avatica/server/CustomAuthHttpServerTest.java
+++
b/server/src/test/java/org/apache/calcite/avatica/server/CustomAuthHttpServerTest.java
@@ -22,7 +22,6 @@ import org.apache.calcite.avatica.remote.AuthenticationType;
import org.apache.calcite.avatica.remote.Driver;
import org.apache.calcite.avatica.remote.LocalService;
-import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.Connector;
@@ -33,6 +32,7 @@ import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -208,7 +208,7 @@ public class CustomAuthHttpServerTest extends HttpAuthBase {
Driver.Serialization.PROTOBUF, null, configuration);
if (isBasicAuth) {
- ConstraintSecurityHandler securityHandler =
+ HandlerWrapper securityHandler =
avaticaServer.configureBasicAuthentication(server,
configuration);
securityHandler.setHandler(avaticaHandler);
avaticaHandler = securityHandler;
@@ -238,6 +238,14 @@ public class CustomAuthHttpServerTest extends HttpAuthBase
{
return null;
}
+ @Override public String getKerberosServiceName() {
+ return null;
+ }
+
+ @Override public String getKerberosHostName() {
+ return null;
+ }
+
@Override public String[] getAllowedRoles() {
return new String[0];
}
@@ -289,6 +297,14 @@ public class CustomAuthHttpServerTest extends HttpAuthBase
{
return null;
}
+ @Override public String getKerberosServiceName() {
+ return null;
+ }
+
+ @Override public String getKerberosHostName() {
+ return null;
+ }
+
@Override public String[] getAllowedRoles() {
return new String[] { "users" };
}
diff --git
a/server/src/test/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractorTest.java
b/server/src/test/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractorTest.java
index 33d91bc..b6d992b 100644
---
a/server/src/test/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractorTest.java
+++
b/server/src/test/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractorTest.java
@@ -97,6 +97,14 @@ public class HttpQueryStringParameterRemoteUserExtractorTest
extends HttpAuthBas
return null;
}
+ @Override public String getKerberosServiceName() {
+ return null;
+ }
+
+ @Override public String getKerberosHostName() {
+ return null;
+ }
+
@Override public boolean supportsImpersonation() {
// Impersonation is allowed
return true;
diff --git
a/server/src/test/java/org/apache/calcite/avatica/server/HttpServerBuilderTest.java
b/server/src/test/java/org/apache/calcite/avatica/server/HttpServerBuilderTest.java
deleted file mode 100644
index 41bb88b..0000000
---
a/server/src/test/java/org/apache/calcite/avatica/server/HttpServerBuilderTest.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * 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.calcite.avatica.server;
-
-import org.apache.calcite.avatica.remote.Driver.Serialization;
-import org.apache.calcite.avatica.remote.Service;
-
-import org.junit.Test;
-import org.mockito.Mockito;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertNull;
-
-/**
- * Test class for {@link HttpServer}.
- */
-public class HttpServerBuilderTest {
-
- @Test public void extraAllowedRolesConfigured() {
- final String[] extraRoles = new String[] {"BAR.COM"};
- final Service mockService = Mockito.mock(Service.class);
- HttpServer server = new HttpServer.Builder()
- .withSpnego("HTTP/[email protected]", extraRoles)
- .withHandler(mockService, Serialization.JSON)
- .build();
-
- assertArrayEquals(extraRoles, server.getConfig().getAllowedRoles());
-
- assertArrayEquals(new String[] {"EXAMPLE.COM", "BAR.COM"},
- server.getAllowedRealms("EXAMPLE.COM", server.getConfig()));
- }
-
- @Test public void lotsOfExtraRoles() {
- final String[] extraRoles = new String[] {"BAR.COM", "BAZ.COM", "FOO.COM"};
- final Service mockService = Mockito.mock(Service.class);
- HttpServer server = new HttpServer.Builder()
- .withSpnego("HTTP/[email protected]", extraRoles)
- .withHandler(mockService, Serialization.JSON)
- .build();
-
- assertArrayEquals(extraRoles, server.getConfig().getAllowedRoles());
-
- assertArrayEquals(new String[] {"EXAMPLE.COM", "BAR.COM", "BAZ.COM",
"FOO.COM"},
- server.getAllowedRealms("EXAMPLE.COM", server.getConfig()));
- }
-
- @Test public void nullExtraRoles() {
- final String[] extraRoles = null;
- final Service mockService = Mockito.mock(Service.class);
- HttpServer server = new HttpServer.Builder()
- .withSpnego("HTTP/[email protected]", extraRoles)
- .withHandler(mockService, Serialization.JSON)
- .build();
-
- assertNull(server.getConfig().getAllowedRoles());
-
- assertArrayEquals(new String[] {"EXAMPLE.COM"},
- server.getAllowedRealms("EXAMPLE.COM", server.getConfig()));
- }
-
- @Test public void emptyExtraRoles() {
- final String[] extraRoles = new String[0];
- final Service mockService = Mockito.mock(Service.class);
- HttpServer server = new HttpServer.Builder()
- .withSpnego("HTTP/[email protected]", extraRoles)
- .withHandler(mockService, Serialization.JSON)
- .build();
-
- assertArrayEquals(extraRoles, server.getConfig().getAllowedRoles());
-
- assertArrayEquals(new String[] {"EXAMPLE.COM"},
- server.getAllowedRealms("EXAMPLE.COM", server.getConfig()));
- }
-
- @Test public void extraAllowedRolesConfiguredWithExplitRealm() {
- final String[] extraRoles = new String[] {"BAR.COM"};
- final Service mockService = Mockito.mock(Service.class);
- HttpServer server = new HttpServer.Builder()
- .withSpnego("HTTP/[email protected]", "EXAMPLE.COM",
extraRoles)
- .withHandler(mockService, Serialization.JSON)
- .build();
-
- assertArrayEquals(extraRoles, server.getConfig().getAllowedRoles());
-
- assertArrayEquals(new String[] {"EXAMPLE.COM", "BAR.COM"},
- server.getAllowedRealms("EXAMPLE.COM", server.getConfig()));
- }
-
- @Test public void lotsOfExtraRolesWithExplitRealm() {
- final String[] extraRoles = new String[] {"BAR.COM", "BAZ.COM", "FOO.COM"};
- final Service mockService = Mockito.mock(Service.class);
- HttpServer server = new HttpServer.Builder()
- .withSpnego("HTTP/[email protected]", "EXAMPLE.COM",
extraRoles)
- .withHandler(mockService, Serialization.JSON)
- .build();
-
- assertArrayEquals(extraRoles, server.getConfig().getAllowedRoles());
-
- assertArrayEquals(new String[] {"EXAMPLE.COM", "BAR.COM", "BAZ.COM",
"FOO.COM"},
- server.getAllowedRealms("EXAMPLE.COM", server.getConfig()));
- }
-
- @Test public void nullExtraRolesWithExplitRealm() {
- final String[] extraRoles = null;
- final Service mockService = Mockito.mock(Service.class);
- HttpServer server = new HttpServer.Builder()
- .withSpnego("HTTP/[email protected]", "EXAMPLE.COM",
extraRoles)
- .withHandler(mockService, Serialization.JSON)
- .build();
-
- assertNull(server.getConfig().getAllowedRoles());
-
- assertArrayEquals(new String[] {"EXAMPLE.COM"},
- server.getAllowedRealms("EXAMPLE.COM", server.getConfig()));
- }
-
- @Test public void emptyExtraRolesWithExplitRealm() {
- final String[] extraRoles = new String[0];
- final Service mockService = Mockito.mock(Service.class);
- HttpServer server = new HttpServer.Builder()
- .withSpnego("HTTP/[email protected]", "EXAMPLE.COM",
extraRoles)
- .withHandler(mockService, Serialization.JSON)
- .build();
-
- assertArrayEquals(extraRoles, server.getConfig().getAllowedRoles());
-
- assertArrayEquals(new String[] {"EXAMPLE.COM"},
- server.getAllowedRealms("EXAMPLE.COM", server.getConfig()));
- }
-}
-
-// End HttpServerBuilderTest.java
diff --git
a/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithJaasTest.java
b/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithJaasTest.java
deleted file mode 100644
index 93794b9..0000000
---
a/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithJaasTest.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * 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.calcite.avatica.server;
-
-import org.apache.calcite.avatica.ConnectionConfig;
-import org.apache.calcite.avatica.ConnectionConfigImpl;
-import org.apache.calcite.avatica.SpnegoTestUtil;
-import org.apache.calcite.avatica.remote.AvaticaCommonsHttpClientImpl;
-import org.apache.calcite.avatica.remote.CommonsHttpClientPoolCache;
-
-import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
-import org.apache.kerby.kerberos.kerb.KrbException;
-import org.apache.kerby.kerberos.kerb.client.KrbConfig;
-import org.apache.kerby.kerberos.kerb.client.KrbConfigKey;
-import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
-
-import org.ietf.jgss.GSSCredential;
-import org.ietf.jgss.GSSManager;
-import org.ietf.jgss.GSSName;
-import org.ietf.jgss.Oid;
-import org.junit.AfterClass;
-import org.junit.Assume;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.security.Principal;
-import java.security.PrivilegedExceptionAction;
-import java.util.Properties;
-import java.util.Set;
-import javax.security.auth.Subject;
-import javax.security.auth.kerberos.KerberosTicket;
-
-import static org.hamcrest.core.Is.is;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-/**
- * Test class for SPNEGO with Kerberos. Purely testing SPNEGO, not the Avatica
"protocol" on top
- * of that HTTP. This variant of the test requires that the user use JAAS
configuration to
- * perform server-side login.
- */
-public class HttpServerSpnegoWithJaasTest {
- private static final Logger LOG =
LoggerFactory.getLogger(HttpServerSpnegoWithJaasTest.class);
-
- private static SimpleKdcServer kdc;
- private static HttpServer httpServer;
-
- private static KrbConfig clientConfig;
-
- private static int kdcPort;
-
- private static File clientKeytab;
- private static File serverKeytab;
-
- private static File serverSpnegoConfigFile;
-
- private static boolean isKdcStarted = false;
- private static boolean isHttpServerStarted = false;
-
- private static URL httpServerUrl;
-
- @BeforeClass public static void setupKdc() throws Exception {
- kdc = new SimpleKdcServer();
- File target = SpnegoTestUtil.TARGET_DIR;
- assertTrue(target.exists());
-
- File kdcDir = new File(target,
HttpServerSpnegoWithJaasTest.class.getSimpleName());
- if (kdcDir.exists()) {
- SpnegoTestUtil.deleteRecursively(kdcDir);
- }
- kdcDir.mkdirs();
- kdc.setWorkDir(kdcDir);
-
- kdc.setKdcHost(SpnegoTestUtil.KDC_HOST);
- kdcPort = SpnegoTestUtil.getFreePort();
- kdc.setAllowTcp(true);
- kdc.setAllowUdp(false);
- kdc.setKdcTcpPort(kdcPort);
-
- LOG.info("Starting KDC server at {}:{}", SpnegoTestUtil.KDC_HOST, kdcPort);
-
- kdc.init();
- kdc.start();
- isKdcStarted = true;
-
- try (FileInputStream fis = new FileInputStream(new File(kdcDir,
"krb5.conf"));
- InputStreamReader isr = new InputStreamReader(fis,
StandardCharsets.UTF_8);
- BufferedReader r = new BufferedReader(isr)) {
- String line;
- while ((line = r.readLine()) != null) {
- LOG.debug("KRB5 Config line: {}", line);
- }
- }
-
- File keytabDir = new File(target,
HttpServerSpnegoWithJaasTest.class.getSimpleName()
- + "_keytabs");
- if (keytabDir.exists()) {
- SpnegoTestUtil.deleteRecursively(keytabDir);
- }
- keytabDir.mkdirs();
- setupUsers(keytabDir);
-
- clientConfig = new KrbConfig();
- clientConfig.setString(KrbConfigKey.KDC_HOST, SpnegoTestUtil.KDC_HOST);
- clientConfig.setInt(KrbConfigKey.KDC_TCP_PORT, kdcPort);
- clientConfig.setString(KrbConfigKey.DEFAULT_REALM, SpnegoTestUtil.REALM);
-
- serverSpnegoConfigFile = new File(kdcDir, "server-spnego.conf");
- SpnegoTestUtil.writeSpnegoConf(serverSpnegoConfigFile, serverKeytab);
-
- // Kerby sets "java.security.krb5.conf" for us!
- System.setProperty("java.security.auth.login.config",
serverSpnegoConfigFile.toString());
- // http://docs.oracle.com/javase/7/docs/technotes/guides/security/jgss/...
- // tutorials/BasicClientServer.html#useSub
- System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
- //System.setProperty("sun.security.spnego.debug", "true");
- //System.setProperty("sun.security.krb5.debug", "true");
-
- // Create and start an HTTP server configured only to allow SPNEGO requests
- // We're not using `withAutomaticLogin(File)` which means we're relying on
JAAS to log the
- // server in.
- httpServer = new HttpServer.Builder()
- .withPort(0)
- .withSpnego(SpnegoTestUtil.SERVER_PRINCIPAL, SpnegoTestUtil.REALM)
- .withHandler(new SpnegoTestUtil.AuthenticationRequiredAvaticaHandler())
- .build();
- httpServer.start();
- isHttpServerStarted = true;
-
- httpServerUrl = new URL("http://" + SpnegoTestUtil.KDC_HOST + ":" +
httpServer.getPort());
- LOG.info("HTTP server running at {}", httpServerUrl);
-
- SpnegoTestUtil.refreshJaasConfiguration();
- }
-
- @AfterClass public static void stopKdc() throws Exception {
- if (isHttpServerStarted) {
- LOG.info("Stopping HTTP server at {}", httpServerUrl);
- httpServer.stop();
- }
-
- if (isKdcStarted) {
- LOG.info("Stopping KDC on {}", kdcPort);
- kdc.stop();
- }
- }
-
- private static void setupUsers(File keytabDir) throws KrbException {
- String clientPrincipal = SpnegoTestUtil.CLIENT_PRINCIPAL.substring(0,
- SpnegoTestUtil.CLIENT_PRINCIPAL.indexOf('@'));
- clientKeytab = new File(keytabDir, clientPrincipal.replace('/', '_') +
".keytab");
- if (clientKeytab.exists()) {
- SpnegoTestUtil.deleteRecursively(clientKeytab);
- }
- LOG.info("Creating {} with keytab {}", clientPrincipal, clientKeytab);
- SpnegoTestUtil.setupUser(kdc, clientKeytab, clientPrincipal);
-
- String serverPrincipal = SpnegoTestUtil.SERVER_PRINCIPAL.substring(0,
- SpnegoTestUtil.SERVER_PRINCIPAL.indexOf('@'));
- serverKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') +
".keytab");
- if (serverKeytab.exists()) {
- SpnegoTestUtil.deleteRecursively(serverKeytab);
- }
- LOG.info("Creating {} with keytab {}", SpnegoTestUtil.SERVER_PRINCIPAL,
serverKeytab);
- SpnegoTestUtil.setupUser(kdc, serverKeytab,
SpnegoTestUtil.SERVER_PRINCIPAL);
- }
-
- @Test public void testNormalClientsDisallowed() throws Exception {
- LOG.info("Connecting to {}", httpServerUrl.toString());
- HttpURLConnection conn = (HttpURLConnection)
httpServerUrl.openConnection();
- conn.setRequestMethod("GET");
- // Authentication should fail because we didn't provide anything
- assertEquals(401, conn.getResponseCode());
- }
-
- @Test public void testAuthenticatedClientsAllowed() throws Exception {
- Assume.assumeThat("Test disabled on Windows", File.separatorChar, is('/'));
-
- // Create the subject for the client
- final Subject clientSubject = AvaticaJaasKrbUtil.loginUsingKeytab(
- SpnegoTestUtil.CLIENT_PRINCIPAL, clientKeytab);
- final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
- // Make sure the subject has a principal
- assertFalse(clientPrincipals.isEmpty());
-
- // Get a TGT for the subject (might have many, different encryption
types). The first should
- // be the default encryption type.
- Set<KerberosTicket> privateCredentials =
- clientSubject.getPrivateCredentials(KerberosTicket.class);
- assertFalse(privateCredentials.isEmpty());
- KerberosTicket tgt = privateCredentials.iterator().next();
- assertNotNull(tgt);
- LOG.info("Using TGT with etype: {}", tgt.getSessionKey().getAlgorithm());
-
- // The name of the principal
- final String principalName = clientPrincipals.iterator().next().getName();
-
- // Run this code, logged in as the subject (the client)
- byte[] response = Subject.doAs(clientSubject, new
PrivilegedExceptionAction<byte[]>() {
- @Override public byte[] run() throws Exception {
- // Logs in with Kerberos via GSS
- GSSManager gssManager = GSSManager.getInstance();
- Oid oid = new Oid(SpnegoTestUtil.JGSS_KERBEROS_TICKET_OID);
- GSSName gssClient = gssManager.createName(principalName,
GSSName.NT_USER_NAME);
- GSSCredential credential = gssManager.createCredential(gssClient,
- GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
-
- Properties props = new Properties();
- ConnectionConfig config = new ConnectionConfigImpl(props);
-
- PoolingHttpClientConnectionManager pool =
CommonsHttpClientPoolCache.getPool(config);
-
- // Passes the GSSCredential into the HTTP client implementation
- final AvaticaCommonsHttpClientImpl httpClient =
- new AvaticaCommonsHttpClientImpl(httpServerUrl);
- httpClient.setGSSCredential(credential);
- httpClient.setHttpClientPool(pool);
-
- return httpClient.send(new byte[0]);
- }
- });
-
- // We should get a response which is "OK" with our client's name
- assertNotNull(response);
- assertEquals("OK " + SpnegoTestUtil.CLIENT_PRINCIPAL,
- new String(response, StandardCharsets.UTF_8));
- }
-}
-
-// End HttpServerSpnegoWithJaasTest.java
diff --git
a/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java
b/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java
index 43d1895..318af48 100644
---
a/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java
+++
b/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java
@@ -129,8 +129,8 @@ public class HttpServerSpnegoWithoutJaasTest {
// Kerby sets "java.security.krb5.conf" for us!
System.clearProperty("java.security.auth.login.config");
System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
- //System.setProperty("sun.security.spnego.debug", "true");
- //System.setProperty("sun.security.krb5.debug", "true");
+ System.setProperty("sun.security.spnego.debug", "true");
+ System.setProperty("sun.security.krb5.debug", "true");
// Create and start an HTTP server configured only to allow SPNEGO requests
// We use `withAutomaticLogin(File)` here which should invalidate the need
to do JAAS config
@@ -234,7 +234,7 @@ public class HttpServerSpnegoWithoutJaasTest {
// We should get a response which is "OK" with our client's name
assertNotNull(response);
- assertEquals("OK " + SpnegoTestUtil.CLIENT_PRINCIPAL,
+ assertEquals("OK " + SpnegoTestUtil.CLIENT_NAME,
new String(response, StandardCharsets.UTF_8));
}
}
diff --git a/site/_docs/security.md b/site/_docs/security.md
index 70f644b..d7cf02c 100644
--- a/site/_docs/security.md
+++ b/site/_docs/security.md
@@ -9,6 +9,7 @@ auth_types:
- { name: "Kerberos with SPNEGO", anchor:
"kerberos-with-spnego-authentication" }
- { name: "Custom Authentication", anchor: "custom-authentication" }
- { name: "Client implementation", anchor: "client-implementation" }
+ - { name: "TLS", anchor: "tls" }
---
<!--
{% comment %}
@@ -164,6 +165,10 @@ HttpServer server = new HttpServer.Builder()
#### JAAS Configuration File Login
+**Since Avatica 1.20.0, Jetty has removed this functionality which means that
Avatica
+also does not support Avatica server login via JAAS configuration file. The
Avatica
+programmatic login is the only manner to do this.**
+
A JAAS configuration file can be set via the system property
`java.security.auth.login.config`.
The user must set this property when launching their Java application invoking
the Avatica server.
The presence of this file will automatically perform login as necessary in the
first use
@@ -191,6 +196,20 @@ com.sun.security.jgss.accept {
Ensure the `keyTab` and `principal` attributes are set correctly for your
system.
+#### Additional Allowed Realms
+
+Versions of Avatica prior to 1.20.0 provided API to specify a list of
`additionalAllowedRealms`.
+While this API could have been leveraged by other integrators of Avatica, the
only provided
+usage of this API was to specify additional Kerberos realms (realms other than
the kerberos
+realm which the server's principal was a part of) which should be allowed to
authenticate
+against the Avatica server.
+
+With the Jetty update in Avatica 1.20.0, this functionality was removed
without replacement.
+Any user with valid Kerberos credentials which can be validated based on the
krb5.conf file
+on the host where the Avatica server runs should be capable of authenticating
against Avatica.
+Consult your JVM to determine where the default krb5.conf file is loaded from
and the Java
+system property to use if you need to override this file.
+
### Impersonation
Impersonation is a feature of the Avatica server which allows the Avatica
clients
@@ -286,8 +305,21 @@ these implementations as it is likely correct.
### SPNEGO
For information on building SPNEGO support by hand, consult
[RFC-4559](https://tools.ietf.org/html/rfc4559)
-which describes how the authentication handshake, through use of the
"WWW-authenticate=Negotiate"
-HTTP header, is used to authenticate a client.
+which describes how the authentication handshake, through use of the
`WWW-Authenticate=Negotiate`
+HTTP header, is used to authenticate a client. Prior to Avatica 1.20.0, this
handshake is done
+for every HTTP call to the Avatica server.
+
+Starting in Avatica 1.20.0, Avatica was updated to use a newer version of
Jetty which includes
+the ability to perform one SPNEGO-based authentication handshake but then set
a cookie which
+can be used to re-identify the client without performing subsequent SPNEGO
handshakes.
+
+This is a notable change because it will effectively reduce the number of HTTP
calls that an Avatica
+client has to make to the server which, for often results in a near 2x
performance improvement (as there
+is a lower-bound of 1's of milliseconds per HTTP call). However, if the cookie
is compromised, another
+client could potentially access Avatica as the user for whom the cookie was
set for. Because of this, it
+is important to configure the Avatica server to [use TLS](#tls) to
authenticate its clients.
+
+See more information in
[CALCITE-4152](https://issues.apache.org/jira/browse/CALCITE-4152).
### Password-based
@@ -298,3 +330,17 @@ properties are used to identify the client with the
server. If the underlying da
these are set via the traditional "user" and "password" properties in the
Avatica
JDBC driver. This also implies that adding HTTP-level authentication in
Avatica is likely
superfluous.
+
+## TLS
+
+Deploying the Avatica server with TLS is common practice, like it is for any
HTTP server. To do this,
+use the method `withTls(File, String, File, String)` to provide the server's
TLS private key (a.k.a keystore)
+and the certificate authority's public key (a.k.a. truststore) as a Java Key
Store (JKS) files, along with
+passwords to validate that the JKS files have not been tampered with.
+
+{% highlight java %}
+HttpServer server = new HttpServer.Builder()
+ .withTLS(new File("/avatica/server.jks"), "MyKeystorePassword",
+ new File("/avatica/truststore.jks"), "MyTruststorePassword")
+ .build();
+{% endhighlight %}
diff --git a/standalone-server/build.gradle.kts
b/standalone-server/build.gradle.kts
index b3b0bde..fa9678e 100644
--- a/standalone-server/build.gradle.kts
+++ b/standalone-server/build.gradle.kts
@@ -84,7 +84,7 @@ tasks {
shadowJar {
manifest {
- attributes["Main-Class"] =
"org.apache.calcite.avatica.server.StandaloneServer"
+ attributes["Main-Class"] =
"org.apache.calcite.avatica.standalone.StandaloneServer"
}
archiveClassifier.set("shadow")
configurations = listOf(shaded)
@@ -97,9 +97,6 @@ tasks {
"com.google.common",
"com.google.protobuf",
"javax.servlet",
- "org.apache.log4j",
- "org.eclipse.jetty",
- "org.slf4j",
"org.apache.http",
"org.apache.commons"
).forEach {
diff --git
a/standalone-server/src/main/java/org/apache/calcite/avatica/standalone/StandaloneServer.java
b/standalone-server/src/main/java/org/apache/calcite/avatica/standalone/StandaloneServer.java
index 1b94c55..686ffa6 100644
---
a/standalone-server/src/main/java/org/apache/calcite/avatica/standalone/StandaloneServer.java
+++
b/standalone-server/src/main/java/org/apache/calcite/avatica/standalone/StandaloneServer.java
@@ -29,6 +29,7 @@ import com.beust.jcommander.Parameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
import java.util.Locale;
/**
@@ -53,6 +54,14 @@ public class StandaloneServer {
description = "Print the help message")
private boolean help = false;
+ @Parameter(names = { "--principal" }, required = false,
+ description = "Kerberos principal (optiona)")
+ private String kerberosPrincipal = null;
+
+ @Parameter(names = { "--keytab" }, required = false,
+ description = "Kerberos keytab (optional)", converter =
ToFileConverter.class)
+ private File kerberosKeytab = null;
+
private HttpServer server;
public void start() {
@@ -67,10 +76,19 @@ public class StandaloneServer {
LocalService service = new LocalService(meta);
// Construct the server
- this.server = new HttpServer.Builder()
+ HttpServer.Builder builder = new HttpServer.Builder()
.withHandler(service, serialization)
- .withPort(port)
- .build();
+ .withPort(port);
+
+ if (kerberosPrincipal != null && kerberosKeytab != null) {
+ System.out.println("Configuring Avatica to use SPENGO");
+ builder.withSpnego(kerberosPrincipal)
+ .withAutomaticLogin(kerberosKeytab);
+ } else {
+ System.out.println("Not configuring Avatica authentication");
+ }
+
+ server = builder.build();
// Then start it
server.start();
@@ -136,6 +154,20 @@ public class StandaloneServer {
}
/**
+ * Converter from String to a File.
+ */
+ public static class ToFileConverter implements IStringConverter<File> {
+ @Override public File convert(String value) {
+ File f = new File(value);
+ if (!f.isFile()) {
+ String msg = "Kerberos keytab '" + value + "' appears to not be a
file";
+ throw new IllegalArgumentException(msg);
+ }
+ return f;
+ }
+ }
+
+ /**
* Codes for exit conditions
*/
private enum ExitCodes {
diff --git a/standalone-server/src/main/resources/log4j.properties
b/standalone-server/src/main/resources/log4j.properties
index c2a9652..c8f6c5b 100644
--- a/standalone-server/src/main/resources/log4j.properties
+++ b/standalone-server/src/main/resources/log4j.properties
@@ -19,8 +19,8 @@
log4j.rootLogger=INFO, A1
# A1 goes to the console
-log4j.appender.A1=org.apache.calcite.avatica.standalone.shaded.org.apache.log4j.ConsoleAppender
+log4j.appender.A1=org.apache.log4j.ConsoleAppender
# Set the pattern for each log message
-log4j.appender.A1.layout=org.apache.calcite.avatica.standalone.shaded.org.apache.log4j.PatternLayout
+log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d{ISO8601} [%t] %-5p %c{2} - %m%n