Repository: aurora Updated Branches: refs/heads/master 638471036 -> 7e3e7c9ad
Add Kerberos support to the scheduler Support authenticating to the scheduler API with Kerberos. Testing Done: ./gradlew -Pq build ./src/test/sh/org/apache/aurora/test_kerberos_end_to_end.sh Bugs closed: AURORA-812 Reviewed at https://reviews.apache.org/r/32559/ Project: http://git-wip-us.apache.org/repos/asf/aurora/repo Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/7e3e7c9a Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/7e3e7c9a Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/7e3e7c9a Branch: refs/heads/master Commit: 7e3e7c9ad9fedd117b9885c32ada9e5a04ed3357 Parents: 6384710 Author: Kevin Sweeney <[email protected]> Authored: Thu Apr 9 11:44:27 2015 -0700 Committer: Kevin Sweeney <[email protected]> Committed: Thu Apr 9 11:44:27 2015 -0700 ---------------------------------------------------------------------- config/findbugs/excludeFilter.xml | 6 + examples/vagrant/provision-dev-cluster.sh | 1 + .../upstart/aurora-scheduler-kerberos.conf | 56 ++++++ .../http/api/security/ApiSecurityModule.java | 41 +++- .../http/api/security/AuthorizeHeaderToken.java | 66 +++++++ .../http/api/security/IniShiroRealmModule.java | 12 +- .../http/api/security/Kerberos5Realm.java | 101 ++++++++++ .../api/security/Kerberos5ShiroRealmModule.java | 198 +++++++++++++++++++ .../api/security/KerberosPrincipalParser.java | 27 +++ .../ShiroKerberosAuthenticationFilter.java | 91 +++++++++ .../scheduler/http/api/security/ShiroUtils.java | 41 ++++ .../api/security/AuthorizeHeaderTokenTest.java | 44 +++++ .../security/Kerberos5ShiroRealmModuleTest.java | 71 +++++++ .../security/KerberosPrincipalParserTest.java | 35 ++++ .../ShiroKerberosAuthenticationFilterTest.java | 148 ++++++++++++++ .../sh/org/apache/aurora/e2e/test_end_to_end.sh | 2 + .../aurora/e2e/test_kerberos_end_to_end.sh | 108 ++++++++++ 17 files changed, 1037 insertions(+), 11 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/config/findbugs/excludeFilter.xml ---------------------------------------------------------------------- diff --git a/config/findbugs/excludeFilter.xml b/config/findbugs/excludeFilter.xml index 5ff5f87..0bff71c 100644 --- a/config/findbugs/excludeFilter.xml +++ b/config/findbugs/excludeFilter.xml @@ -37,6 +37,12 @@ limitations under the License. </Or> </Match> + <!-- We don't make use of Java serialization and this can prevent, for example, declaring an + HttpServlet as an anonymous inner class for testing. --> + <Match> + <Bug pattern="SE_BAD_FIELD" /> + </Match> + <!-- Method is intentionally only callable by EventBus. --> <Match> <Class name="org.apache.aurora.scheduler.events.PubsubEventModule$1" /> http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/examples/vagrant/provision-dev-cluster.sh ---------------------------------------------------------------------- diff --git a/examples/vagrant/provision-dev-cluster.sh b/examples/vagrant/provision-dev-cluster.sh index 0fbbfdc..e88efc3 100755 --- a/examples/vagrant/provision-dev-cluster.sh +++ b/examples/vagrant/provision-dev-cluster.sh @@ -17,6 +17,7 @@ apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8 echo deb https://get.docker.com/ubuntu docker main > /etc/apt/sources.list.d/docker.list apt-get update apt-get -y install \ + bison \ curl \ git \ libapr1-dev \ http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/examples/vagrant/upstart/aurora-scheduler-kerberos.conf ---------------------------------------------------------------------- diff --git a/examples/vagrant/upstart/aurora-scheduler-kerberos.conf b/examples/vagrant/upstart/aurora-scheduler-kerberos.conf new file mode 100644 index 0000000..0a809e8 --- /dev/null +++ b/examples/vagrant/upstart/aurora-scheduler-kerberos.conf @@ -0,0 +1,56 @@ +# Licensed 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. +# +description "aurora scheduler (kerberos e2e test)" +start on stopped rc RUNLEVEL=[2345] +respawn +post-stop exec sleep 5 + +# Environment variables control the behavior of the Mesos scheduler driver (libmesos). +env GLOG_v=0 +env LIBPROCESS_PORT=8083 +env LIBPROCESS_IP=192.168.33.7 +env AURORA_HOME=/usr/local/aurora +env DIST_DIR=/home/vagrant/aurora/dist + +# Flags that control the behavior of the JVM. +env JAVA_OPTS='-Djava.library.path=/usr/lib + -Dlog4j.configuration="file:///etc/zookeeper/conf/log4j.properties" + -Djava.security.krb5.conf=/home/vagrant/src/krb5-1.13.1/build/testdir/krb5.conf + -Dsun.security.krb5.debug=true + -Dsun.security.jgss.debug=true + -Djavax.net.debug=all + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 +' + +exec $DIST_DIR/install/aurora-scheduler/bin/aurora-scheduler \ + -cluster_name=example \ + -http_port=8081 \ + -native_log_quorum_size=1 \ + -zk_endpoints=localhost:2181 \ + -mesos_master_address=zk://localhost:2181/mesos/master \ + -serverset_path=/aurora/scheduler \ + -native_log_zk_group_path=/aurora/replicated-log \ + -native_log_file_path=$AURORA_HOME/scheduler/db \ + -backup_dir=$AURORA_HOME/scheduler/backups \ + -thermos_executor_path=$DIST_DIR/thermos_executor.pex \ + -thermos_executor_flags="--announcer-enable --announcer-ensemble localhost:2181" \ + -gc_executor_path=$DIST_DIR/gc_executor.pex \ + -vlog=INFO \ + -logtostderr \ + -allowed_container_types=MESOS,DOCKER \ + -enable_api_security=true \ + -shiro_realm_modules=org.apache.aurora.scheduler.http.api.security.Kerberos5ShiroRealmModule,org.apache.aurora.scheduler.http.api.security.IniShiroRealmModule \ + -shiro_ini_path=/home/vagrant/aurora/src/test/resources/org/apache/aurora/scheduler/http/api/security/shiro-example.ini \ + -http_authentication_mechanism=NEGOTIATE \ + -kerberos_server_keytab=/home/vagrant/krb5-1.13.1/build/testdir/HTTP-localhost.keytab \ + -kerberos_server_principal=HTTP/[email protected] http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java index 1f773ca..0265e2a 100644 --- a/src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java +++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java @@ -16,10 +16,9 @@ package org.apache.aurora.scheduler.http.api.security; import java.lang.reflect.Method; import java.util.Set; -import javax.inject.Singleton; - import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; +import com.google.inject.Key; import com.google.inject.Module; import com.google.inject.Provides; import com.google.inject.matcher.Matcher; @@ -40,7 +39,6 @@ import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin; import org.apache.shiro.SecurityUtils; import org.apache.shiro.guice.aop.ShiroAopModule; import org.apache.shiro.guice.web.ShiroWebModule; -import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; @@ -82,6 +80,22 @@ public class ApiSecurityModule extends ServletModule { static final Matcher<Method> AURORA_ADMIN_SERVICE = GuiceUtils.interfaceMatcher(AuroraAdmin.Iface.class, true); + public static enum HttpAuthenticationMechanism { + /** + * HTTP Basic Authentication, produces {@link org.apache.shiro.authc.UsernamePasswordToken}s. + */ + BASIC, + + /** + * Use GSS-Negotiate. Only Kerberos and SPNEGO-with-Kerberos GSS mechanisms are supported. + */ + NEGOTIATE, + } + + @CmdLine(name = "http_authentication_mechanism", help = "HTTP Authentication mechanism to use.") + private static final Arg<HttpAuthenticationMechanism> HTTP_AUTHENTICATION_MECHANISM = + Arg.create(HttpAuthenticationMechanism.BASIC); + private final boolean enableApiSecurity; private final Set<Module> shiroConfigurationModules; @@ -118,13 +132,26 @@ public class ApiSecurityModule extends ServletModule { install(module); } - addFilterChain("/**", - ShiroWebModule.NO_SESSION_CREATION, - config(ShiroWebModule.AUTHC_BASIC, BasicHttpAuthenticationFilter.PERMISSIVE)); + switch (HTTP_AUTHENTICATION_MECHANISM.get()) { + case BASIC: + addFilterChain("/**", + ShiroWebModule.NO_SESSION_CREATION, + config(ShiroWebModule.AUTHC_BASIC, BasicHttpAuthenticationFilter.PERMISSIVE)); + break; + + case NEGOTIATE: + addFilterChain("/**", + ShiroWebModule.NO_SESSION_CREATION, + Key.get(ShiroKerberosAuthenticationFilter.class)); + break; + + default: + addError("Unrecognized HTTP authentication mechanism."); + break; + } } }); - bind(IniRealm.class).in(Singleton.class); bindConstant().annotatedWith(Names.named("shiro.applicationName")).to(HTTP_REALM_NAME); // TODO(ksweeney): Disable session cookie. http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/main/java/org/apache/aurora/scheduler/http/api/security/AuthorizeHeaderToken.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/AuthorizeHeaderToken.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/AuthorizeHeaderToken.java new file mode 100644 index 0000000..be719d1 --- /dev/null +++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/AuthorizeHeaderToken.java @@ -0,0 +1,66 @@ +/** + * Licensed 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.aurora.scheduler.http.api.security; + +import java.util.List; + +import com.google.common.base.Splitter; +import com.google.common.io.BaseEncoding; + +import org.apache.shiro.authc.AuthenticationToken; + +import static java.util.Objects.requireNonNull; + +/** + * Parser for the unsanitized input data from the client WWW-Authenticate header. See RFC 4559. + */ +class AuthorizeHeaderToken implements AuthenticationToken { + private final byte[] authorizeHeaderValue; + + private static final Splitter SPLITTER = Splitter.on(" "); + private static final BaseEncoding BASE64 = BaseEncoding.base64(); + + AuthorizeHeaderToken(String authorizeHeaderValue) throws IllegalArgumentException { + requireNonNull(authorizeHeaderValue); + List<String> parts = SPLITTER.splitToList(authorizeHeaderValue); + if (parts.size() != 2 || !ShiroKerberosAuthenticationFilter.NEGOTIATE.equals(parts.get(0))) { + throw new IllegalArgumentException("Malformed Authorize header: " + authorizeHeaderValue); + } + + this.authorizeHeaderValue = BASE64.decode(parts.get(1)); + } + + @Override + public Object getPrincipal() { + // We don't know the principal that we're attempting to authenticate as until we've actually + // succeeded - this data is encapsulated in the credentials we pass to GssManager. + return null; + } + + /** + * Required by the API, but in-package consumers should use the type-safe + * {@link #getAuthorizeHeaderValue} method instead. + */ + @Override + public Object getCredentials() { + return getAuthorizeHeaderValue(); + } + + /** + * The decoded value of the {@code Authorize} header. + */ + byte[] getAuthorizeHeaderValue() { + return authorizeHeaderValue; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/main/java/org/apache/aurora/scheduler/http/api/security/IniShiroRealmModule.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/IniShiroRealmModule.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/IniShiroRealmModule.java index 58b559e..f4decde 100644 --- a/src/main/java/org/apache/aurora/scheduler/http/api/security/IniShiroRealmModule.java +++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/IniShiroRealmModule.java @@ -13,15 +13,15 @@ */ package org.apache.aurora.scheduler.http.api.security; +import javax.inject.Singleton; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.inject.AbstractModule; -import com.google.inject.multibindings.Multibinder; import com.twitter.common.args.Arg; import com.twitter.common.args.CmdLine; import org.apache.shiro.config.Ini; -import org.apache.shiro.realm.Realm; import org.apache.shiro.realm.text.IniRealm; /** @@ -29,6 +29,10 @@ import org.apache.shiro.realm.text.IniRealm; * authentication and authorization. Should be used in conjunction with the * {@link org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter} or other filter that * produces {@link org.apache.shiro.authc.UsernamePasswordToken}s. + * + * <p> + * Another filter may still be used for authentication, in which case the ini file can still be + * used to provide authorization configuration and the passwords will be ignored. */ public class IniShiroRealmModule extends AbstractModule { @CmdLine(name = "shiro_ini_path", @@ -59,10 +63,10 @@ public class IniShiroRealmModule extends AbstractModule { } try { - Multibinder.newSetBinder(binder(), Realm.class).addBinding() - .toConstructor(IniRealm.class.getConstructor(Ini.class)); + ShiroUtils.addRealmBinding(binder()).toConstructor(IniRealm.class.getConstructor(Ini.class)); } catch (NoSuchMethodException e) { addError(e); } + bind(IniRealm.class).in(Singleton.class); } } http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/main/java/org/apache/aurora/scheduler/http/api/security/Kerberos5Realm.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/Kerberos5Realm.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/Kerberos5Realm.java new file mode 100644 index 0000000..b224983 --- /dev/null +++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/Kerberos5Realm.java @@ -0,0 +1,101 @@ +/** + * Licensed 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.aurora.scheduler.http.api.security; + +import javax.inject.Inject; +import javax.security.auth.kerberos.KerberosPrincipal; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.realm.Realm; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; + +import static java.util.Objects.requireNonNull; + +/** + * Authentication-only realm for Kerberos V5. + */ +class Kerberos5Realm implements Realm { + private static final Splitter AT_SPLITTER = Splitter.on("@"); + + private final GSSManager gssManager; + private final GSSCredential serverCredential; + + @Inject + Kerberos5Realm(GSSManager gssManager, GSSCredential serverCredential) { + this.gssManager = requireNonNull(gssManager); + this.serverCredential = requireNonNull(serverCredential); + } + + @Override + public String getName() { + return getClass().getName(); + } + + @Override + public boolean supports(AuthenticationToken token) { + return token instanceof AuthorizeHeaderToken; + } + + @Override + public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) + throws AuthenticationException { + + byte[] tokenFromInitiator = ((AuthorizeHeaderToken) token).getAuthorizeHeaderValue(); + GSSContext context; + try { + context = gssManager.createContext(serverCredential); + context.acceptSecContext(tokenFromInitiator, 0, tokenFromInitiator.length); + } catch (GSSException e) { + throw new AuthenticationException(e); + } + + // Technically the GSS-API requires us to continue sending data back and forth in a loop + // until the context is established, but we can short-circuit here since we know we're using + // Kerberos V5 directly or Kerberos V5-backed SPNEGO. This is important because it means we + // don't need to keep state between requests. + // From http://docs.oracle.com/javase/7/docs/technotes/guides/security/jgss/single-signon.html + // "In the case of the Kerberos V5 mechanism, there is no more than one round trip of + // tokens during context establishment." + if (context.isEstablished()) { + try { + KerberosPrincipal kerberosPrincipal = + new KerberosPrincipal(context.getSrcName().toString()); + return new SimpleAuthenticationInfo( + new SimplePrincipalCollection( + ImmutableList.<Object>of( + // We assume there's a single Kerberos realm in use here. Most Authorizer + // implementations care about the "simple" username instead of the full + // principal. + AT_SPLITTER.splitToList(kerberosPrincipal.getName()).get(0), + kerberosPrincipal), + getName()), + null /* There are no credentials that can be cached. */); + } catch (GSSException | IndexOutOfBoundsException e) { + throw new AuthenticationException(e); + } + } else { + throw new AuthenticationException("GSSContext was not established with a single message."); + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/main/java/org/apache/aurora/scheduler/http/api/security/Kerberos5ShiroRealmModule.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/Kerberos5ShiroRealmModule.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/Kerberos5ShiroRealmModule.java new file mode 100644 index 0000000..4ec531d --- /dev/null +++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/Kerberos5ShiroRealmModule.java @@ -0,0 +1,198 @@ +/** + * Licensed 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.aurora.scheduler.http.api.security; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.PrivilegedAction; +import java.util.logging.Logger; + +import javax.inject.Singleton; +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.io.Files; +import com.google.inject.AbstractModule; +import com.google.inject.PrivateModule; +import com.sun.security.auth.module.Krb5LoginModule; +import com.twitter.common.args.Arg; +import com.twitter.common.args.CmdLine; + +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.Oid; + +/** + * Configures and provides a Shiro {@link org.apache.shiro.realm.Realm}. + * + * @see org.apache.aurora.scheduler.http.api.security.Kerberos5Realm + */ +public class Kerberos5ShiroRealmModule extends AbstractModule { + private static final Logger LOG = Logger.getLogger(Kerberos5ShiroRealmModule.class.getName()); + + private static final String JAVA_SECURITY_LOGIN_KEY = "java.security.auth.login.config"; + + /** + * Standard Object Identifier for the Kerberos 5 GSS-API mechanism. + */ + private static final String GSS_KRB5_MECH_OID = "1.2.840.113554.1.2.2"; + + /** + * Standard Object Identifier for the SPNEGO GSS-API mechanism. + */ + private static final String GSS_SPNEGO_MECH_OID = "1.3.6.1.5.5.2"; + + private static final String SERVER_KEYTAB_ARGNAME = "kerberos_server_keytab"; + private static final String SERVER_PRINCIPAL_ARGNAME = "kerberos_server_principal"; + + private static final String JAAS_CONF_TEMPLATE = + "%s {\n" + + Krb5LoginModule.class.getName() + + " required useKeyTab=true storeKey=true doNotPrompt=true isInitiator=false " + + "keyTab=\"%s\" principal=\"%s\" debug=%s;\n" + + "};"; + + @CmdLine(name = SERVER_KEYTAB_ARGNAME, help = "Path to the server keytab.") + private static final Arg<File> SERVER_KEYTAB = Arg.create(null); + + @CmdLine(name = SERVER_PRINCIPAL_ARGNAME, + help = "Kerberos server principal to use, usually of the form " + + "HTTP/[email protected]") + private static final Arg<KerberosPrincipal> SERVER_PRINCIPAL = Arg.create(null); + + @CmdLine(name = "kerberos_debug", help = "Produce additional Kerberos debugging output.") + private static final Arg<Boolean> DEBUG = Arg.create(false); + + private final Optional<File> serverKeyTab; + private final Optional<KerberosPrincipal> serverPrincipal; + private final GSSManager gssManager; + private final boolean kerberosDebugEnabled; + + public Kerberos5ShiroRealmModule() { + this( + Optional.fromNullable(SERVER_KEYTAB.get()), + Optional.fromNullable(SERVER_PRINCIPAL.get()), + GSSManager.getInstance(), + DEBUG.get()); + } + + @VisibleForTesting + Kerberos5ShiroRealmModule( + File serverKeyTab, + KerberosPrincipal serverPrincipal, + GSSManager gssManager) { + + this( + Optional.of(serverKeyTab), + Optional.of(serverPrincipal), + gssManager, + true); + } + + private Kerberos5ShiroRealmModule( + Optional<File> serverKeyTab, + Optional<KerberosPrincipal> serverPrincipal, + GSSManager gssManager, + boolean kerberosDebugEnabled) { + + this.serverKeyTab = serverKeyTab; + this.serverPrincipal = serverPrincipal; + this.gssManager = gssManager; + this.kerberosDebugEnabled = kerberosDebugEnabled; + } + + @Override + protected void configure() { + if (!serverKeyTab.isPresent()) { + addError("No -" + SERVER_KEYTAB_ARGNAME + " specified."); + return; + } + + if (!serverPrincipal.isPresent()) { + addError("No -" + SERVER_PRINCIPAL_ARGNAME + " specified."); + return; + } + + // TODO(ksweeney): Find a better way to configure JAAS in code. + String jaasConf = String.format( + JAAS_CONF_TEMPLATE, + getClass().getName(), + serverKeyTab.get().getAbsolutePath(), + serverPrincipal.get().getName(), + kerberosDebugEnabled); + LOG.fine("Generated jaas.conf: " + jaasConf); + + File jaasConfFile; + try { + jaasConfFile = File.createTempFile("jaas", "conf"); + jaasConfFile.deleteOnExit(); + Files.write(jaasConf, jaasConfFile, StandardCharsets.UTF_8); + } catch (IOException e) { + addError(e); + return; + } + + final GSSCredential serverCredential; + + Optional<String> oldJavaSecurityLoginValue = + Optional.fromNullable(System.getProperty(JAVA_SECURITY_LOGIN_KEY)); + try { + System.setProperty(JAVA_SECURITY_LOGIN_KEY, jaasConfFile.getAbsolutePath()); + LoginContext loginContext = new LoginContext(getClass().getName()); + loginContext.login(); + serverCredential = Subject.doAs( + loginContext.getSubject(), + new PrivilegedAction<GSSCredential>() { + @Override + public GSSCredential run() { + try { + return gssManager.createCredential( + null /* Use the service principal name defined in jaas.conf */, + GSSCredential.INDEFINITE_LIFETIME, + new Oid[] {new Oid(GSS_SPNEGO_MECH_OID), new Oid(GSS_KRB5_MECH_OID)}, + GSSCredential.ACCEPT_ONLY); + } catch (GSSException e) { + throw Throwables.propagate(e); + } + } + }); + } catch (LoginException e) { + addError(e); + return; + } finally { + if (oldJavaSecurityLoginValue.isPresent()) { + System.setProperty(JAVA_SECURITY_LOGIN_KEY, oldJavaSecurityLoginValue.get()); + } + } + + install(new PrivateModule() { + @Override + protected void configure() { + bind(GSSManager.class).toInstance(gssManager); + bind(GSSCredential.class).toInstance(serverCredential); + + bind(Kerberos5Realm.class).in(Singleton.class); + expose(Kerberos5Realm.class); + } + }); + ShiroUtils.addRealmBinding(binder()).to(Kerberos5Realm.class); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/main/java/org/apache/aurora/scheduler/http/api/security/KerberosPrincipalParser.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/KerberosPrincipalParser.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/KerberosPrincipalParser.java new file mode 100644 index 0000000..e6e2f1e --- /dev/null +++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/KerberosPrincipalParser.java @@ -0,0 +1,27 @@ +/** + * Licensed 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.aurora.scheduler.http.api.security; + +import javax.security.auth.kerberos.KerberosPrincipal; + +import com.twitter.common.args.ArgParser; +import com.twitter.common.args.parsers.NonParameterizedTypeParser; + +@ArgParser +class KerberosPrincipalParser extends NonParameterizedTypeParser<KerberosPrincipal> { + @Override + public KerberosPrincipal doParse(String raw) throws IllegalArgumentException { + return new KerberosPrincipal(raw); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroKerberosAuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroKerberosAuthenticationFilter.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroKerberosAuthenticationFilter.java new file mode 100644 index 0000000..28e6b98 --- /dev/null +++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroKerberosAuthenticationFilter.java @@ -0,0 +1,91 @@ +/** + * Licensed 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.aurora.scheduler.http.api.security; + +import java.io.IOException; +import java.util.logging.Logger; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.HttpHeaders; + +import com.google.common.base.Optional; + +import org.apache.aurora.scheduler.http.AbstractFilter; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authz.UnauthenticatedException; +import org.apache.shiro.subject.Subject; + +import static java.util.Objects.requireNonNull; + +public class ShiroKerberosAuthenticationFilter extends AbstractFilter { + private static final Logger LOG = + Logger.getLogger(ShiroKerberosAuthenticationFilter.class.getName()); + + /** + * From http://tools.ietf.org/html/rfc4559. + */ + public static final String NEGOTIATE = "Negotiate"; + + private final Provider<Subject> subjectProvider; + + @Inject + ShiroKerberosAuthenticationFilter(Provider<Subject> subjectProvider) { + this.subjectProvider = requireNonNull(subjectProvider); + } + + @Override + protected void doFilter( + HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) throws IOException, ServletException { + + Optional<String> authorizationHeaderValue = + Optional.fromNullable(request.getHeader(HttpHeaders.AUTHORIZATION)); + if (authorizationHeaderValue.isPresent()) { + LOG.fine("Authorization header is present"); + AuthorizeHeaderToken token; + try { + token = new AuthorizeHeaderToken(authorizationHeaderValue.get()); + } catch (IllegalArgumentException e) { + LOG.info("Malformed Authorize header: " + e.getMessage()); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + try { + subjectProvider.get().login(token); + chain.doFilter(request, response); + } catch (AuthenticationException e) { + LOG.warning("Login failed: " + e.getMessage()); + sendChallenge(response); + } + } else { + // Incoming request is unauthenticated, but some RPCs might be okay with that. + try { + chain.doFilter(request, response); + } catch (UnauthenticatedException e) { + sendChallenge(response); + } + } + } + + private void sendChallenge(HttpServletResponse response) throws IOException { + response.setHeader(HttpHeaders.WWW_AUTHENTICATE, NEGOTIATE); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroUtils.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroUtils.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroUtils.java new file mode 100644 index 0000000..7cba269 --- /dev/null +++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroUtils.java @@ -0,0 +1,41 @@ +/** + * Licensed 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.aurora.scheduler.http.api.security; + +import com.google.inject.Binder; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.multibindings.Multibinder; + +import org.apache.shiro.realm.Realm; + +/** + * Utilities for configuring Shiro. + */ +public final class ShiroUtils { + private ShiroUtils() { + // Utility class. + } + + /** + * Enable a new Shiro Realm. All realms enabled this way are passed to a + * {@link org.apache.shiro.authc.pam.ModularRealmAuthenticator}, which is used as the primary + * {@link org.apache.shiro.realm.Realm} for the application. + * + * @param binder The current module's binder. + * @return A binding builder that can be used to add a realm to the injector. + */ + public static LinkedBindingBuilder<Realm> addRealmBinding(Binder binder) { + return Multibinder.newSetBinder(binder, Realm.class).addBinding(); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/test/java/org/apache/aurora/scheduler/http/api/security/AuthorizeHeaderTokenTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/AuthorizeHeaderTokenTest.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/AuthorizeHeaderTokenTest.java new file mode 100644 index 0000000..9e956de --- /dev/null +++ b/src/test/java/org/apache/aurora/scheduler/http/api/security/AuthorizeHeaderTokenTest.java @@ -0,0 +1,44 @@ +/** + * Licensed 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.aurora.scheduler.http.api.security; + +import java.nio.charset.StandardCharsets; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNull; + +public class AuthorizeHeaderTokenTest { + private static final String ALADDIN_OPEN_SESAME = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="; + private static final byte[] ALADDIN_OPEN_SESAME_DECODED = + "Aladdin:open sesame".getBytes(StandardCharsets.US_ASCII); + + @Test(expected = IllegalArgumentException.class) + public void testInvalidScheme() { + new AuthorizeHeaderToken("Basic " + ALADDIN_OPEN_SESAME).getAuthorizeHeaderValue(); + } + + @Test(expected = IllegalArgumentException.class) + public void testTooManyParts() { + new AuthorizeHeaderToken("Negotiate " + ALADDIN_OPEN_SESAME + " more stuff"); + } + + @Test + public void testValidScheme() { + AuthorizeHeaderToken token = new AuthorizeHeaderToken("Negotiate " + ALADDIN_OPEN_SESAME); + assertArrayEquals(ALADDIN_OPEN_SESAME_DECODED, token.getAuthorizeHeaderValue()); + assertNull(token.getPrincipal()); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/test/java/org/apache/aurora/scheduler/http/api/security/Kerberos5ShiroRealmModuleTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/Kerberos5ShiroRealmModuleTest.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/Kerberos5ShiroRealmModuleTest.java new file mode 100644 index 0000000..fda1644 --- /dev/null +++ b/src/test/java/org/apache/aurora/scheduler/http/api/security/Kerberos5ShiroRealmModuleTest.java @@ -0,0 +1,71 @@ +/** + * Licensed 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.aurora.scheduler.http.api.security; + +import java.io.File; + +import javax.security.auth.kerberos.KerberosPrincipal; + +import com.google.inject.Guice; +import com.google.inject.Module; +import com.twitter.common.testing.easymock.EasyMockTest; + +import org.easymock.EasyMock; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; +import org.junit.Before; +import org.junit.Test; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; + +public class Kerberos5ShiroRealmModuleTest extends EasyMockTest { + private static final KerberosPrincipal SERVER_PRINCIPAL = + new KerberosPrincipal("HTTP/[email protected]"); + + private File serverKeytab; + private GSSManager gssManager; + + private GSSCredential gssCredential; + + private Module module; + + @Before + public void setUp() { + serverKeytab = createMock(File.class); + gssManager = createMock(GSSManager.class); + gssCredential = createMock(GSSCredential.class); + + module = new Kerberos5ShiroRealmModule(serverKeytab, SERVER_PRINCIPAL, gssManager); + } + + @Test + public void testConfigure() throws Exception { + expect(serverKeytab.getAbsolutePath()).andReturn("path.keytab"); + expect( + gssManager.createCredential( + EasyMock.<GSSName>isNull(), + eq(GSSCredential.INDEFINITE_LIFETIME), + anyObject(Oid[].class), + eq(GSSCredential.ACCEPT_ONLY))) + .andReturn(gssCredential); + + control.replay(); + + Guice.createInjector(module).getInstance(Kerberos5Realm.class); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/test/java/org/apache/aurora/scheduler/http/api/security/KerberosPrincipalParserTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/KerberosPrincipalParserTest.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/KerberosPrincipalParserTest.java new file mode 100644 index 0000000..7e55ef8 --- /dev/null +++ b/src/test/java/org/apache/aurora/scheduler/http/api/security/KerberosPrincipalParserTest.java @@ -0,0 +1,35 @@ +/** + * Licensed 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.aurora.scheduler.http.api.security; + +import javax.security.auth.kerberos.KerberosPrincipal; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class KerberosPrincipalParserTest { + @Test + public void testValidPrincipal() { + String principal = "HTTP/[email protected]"; + assertEquals( + new KerberosPrincipal(principal), + new KerberosPrincipalParser().doParse(principal)); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidPrincipal() { + new KerberosPrincipalParser().doParse("@HTTP/example.com"); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroKerberosAuthenticationFilterTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroKerberosAuthenticationFilterTest.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroKerberosAuthenticationFilterTest.java new file mode 100644 index 0000000..e335a43 --- /dev/null +++ b/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroKerberosAuthenticationFilterTest.java @@ -0,0 +1,148 @@ +/** + * Licensed 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.aurora.scheduler.http.api.security; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.HttpHeaders; + +import com.google.inject.Module; +import com.google.inject.servlet.ServletModule; +import com.google.inject.util.Providers; +import com.sun.jersey.api.client.ClientResponse; + +import org.apache.aurora.scheduler.http.JettyServerModuleTest; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authz.UnauthenticatedException; +import org.apache.shiro.subject.Subject; +import org.junit.Before; +import org.junit.Test; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.isA; +import static org.junit.Assert.assertEquals; + +public class ShiroKerberosAuthenticationFilterTest extends JettyServerModuleTest { + private static final String PATH = "/test"; + + private Subject subject; + private HttpServlet mockServlet; + + private ShiroKerberosAuthenticationFilter filter; + + @Before + public void setUp() { + subject = createMock(Subject.class); + mockServlet = createMock(HttpServlet.class); + + filter = new ShiroKerberosAuthenticationFilter(Providers.of(subject)); + } + + private HttpServlet getMockServlet() { + return mockServlet; + } + + @Override + public Module getChildServletModule() { + return new ServletModule() { + @Override + protected void configureServlets() { + filter(PATH).through(filter); + serve(PATH).with(new HttpServlet() { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + getMockServlet().service(req, resp); + resp.setStatus(HttpServletResponse.SC_OK); + } + }); + } + }; + } + + @Test + public void testPermitsUnauthenticated() throws ServletException, IOException { + mockServlet.service(anyObject(HttpServletRequest.class), anyObject(HttpServletResponse.class)); + + replayAndStart(); + + ClientResponse clientResponse = getRequestBuilder(PATH).get(ClientResponse.class); + assertEquals(HttpServletResponse.SC_OK, clientResponse.getStatus()); + } + + @Test + public void testRejectsMalformedMechanism() { + replayAndStart(); + + ClientResponse clientResponse = getRequestBuilder(PATH) + .header(HttpHeaders.AUTHORIZATION, "Basic asdf") + .get(ClientResponse.class); + assertEquals( + HttpServletResponse.SC_BAD_REQUEST, + clientResponse.getStatus()); + } + + @Test + public void testLoginFailure401() { + subject.login(isA(AuthenticationToken.class)); + expectLastCall().andThrow(new AuthenticationException()); + + replayAndStart(); + + ClientResponse clientResponse = getRequestBuilder(PATH) + .header(HttpHeaders.AUTHORIZATION, ShiroKerberosAuthenticationFilter.NEGOTIATE + " asdf") + .get(ClientResponse.class); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, clientResponse.getStatus()); + assertEquals( + ShiroKerberosAuthenticationFilter.NEGOTIATE, + clientResponse.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)); + } + + @Test + public void testLoginSuccess200() throws ServletException, IOException { + subject.login(isA(AuthenticationToken.class)); + mockServlet.service(anyObject(HttpServletRequest.class), anyObject(HttpServletResponse.class)); + + replayAndStart(); + + ClientResponse clientResponse = getRequestBuilder(PATH) + .header(HttpHeaders.AUTHORIZATION, ShiroKerberosAuthenticationFilter.NEGOTIATE + " asdf") + .get(ClientResponse.class); + + assertEquals(HttpServletResponse.SC_OK, clientResponse.getStatus()); + } + + @Test + public void testInterceptsUnauthenticatedException() throws ServletException, IOException { + mockServlet.service(anyObject(HttpServletRequest.class), anyObject(HttpServletResponse.class)); + expectLastCall().andThrow(new UnauthenticatedException()); + + replayAndStart(); + + ClientResponse clientResponse = getRequestBuilder(PATH).get(ClientResponse.class); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, clientResponse.getStatus()); + assertEquals( + ShiroKerberosAuthenticationFilter.NEGOTIATE, + clientResponse.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh ---------------------------------------------------------------------- diff --git a/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh b/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh index 74b22f6..f3c9d82 100755 --- a/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh +++ b/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh @@ -314,4 +314,6 @@ sudo docker build -t http_example ${TEST_ROOT} test_http_example "${TEST_DOCKER_ARGS[@]}" test_admin "${TEST_ADMIN_ARGS[@]}" + +/vagrant/src/test/sh/org/apache/aurora/e2e/test_kerberos_end_to_end.sh RETCODE=0 http://git-wip-us.apache.org/repos/asf/aurora/blob/7e3e7c9a/src/test/sh/org/apache/aurora/e2e/test_kerberos_end_to_end.sh ---------------------------------------------------------------------- diff --git a/src/test/sh/org/apache/aurora/e2e/test_kerberos_end_to_end.sh b/src/test/sh/org/apache/aurora/e2e/test_kerberos_end_to_end.sh new file mode 100755 index 0000000..47d22ee --- /dev/null +++ b/src/test/sh/org/apache/aurora/e2e/test_kerberos_end_to_end.sh @@ -0,0 +1,108 @@ +#!/bin/bash +set -eux + +readonly KRB5_MAJOR_MINOR=1.13 +readonly KRB5_VERSION=1.13.1 +readonly KRB5_URL_BASE=http://web.mit.edu/kerberos/dist/krb5/ +readonly KRB5_SIGNED_TARBALL=krb5-$KRB5_VERSION-signed.tar +readonly KRB5_TARBALL=krb5-$KRB5_VERSION.tar.gz +readonly KRB5_KEY_ID=0x749D7889 + +function enter_vagrant { + exec vagrant ssh -- /vagrant/src/test/sh/org/apache/aurora/e2e/test_kerberos_end_to_end.sh "$@" +} + +function enter_testrealm { + cd $HOME + [[ -f $KRB5_SIGNED_TARBALL ]] || wget "$KRB5_URL_BASE/$KRB5_MAJOR_MINOR/$KRB5_SIGNED_TARBALL" + [[ -f $KRB5_TARBALL.asc ]] || tar xvf $KRB5_SIGNED_TARBALL + gpg --list-keys $KRB5_KEY_ID &>/dev/null || gpg --keyserver pgp.mit.edu --recv-keys $KRB5_KEY_ID + gpg --verify $KRB5_TARBALL.asc + [[ -d `basename $KRB5_TARBALL .tar.gz` ]] || tar zxvf $KRB5_TARBALL + cd `basename $KRB5_TARBALL .tar.gz` + mkdir -p build + cd build + [[ -f Makefile ]] || ../src/configure + make + # Reinvokes this script with a full kerberos test realm configured. + SHELL=$0 exec make testrealm +} + +function await_scheduler_ready { + while ! curl -s localhost:8081/vars | grep framework_registered; do + sleep 3 + done +} + +readonly SNAPSHOT_RPC_DATA="[1,\"snapshot\",1,0,{}]" +readonly SNAPSHOT_RESPONSE_OUTFILE="snapshot-response.%s.json" +function snapshot_as { + local principal=$1 + kinit -k -t "testdir/${principal}.keytab" $principal + curl -u : --negotiate -w '%{http_code}\n' \ + -o $(printf $SNAPSHOT_RESPONSE_OUTFILE $principal) \ + -s 'http://localhost:8081/api' \ + --data-binary "$SNAPSHOT_RPC_DATA" + kdestroy +} + +function test_snapshot { + cat >> $KRB5_CONFIG <<EOF +[domain_realm] + .local = KRBTEST.COM +EOF + kadmin.local -q "addprinc -randkey HTTP/localhost" + rm -f testdir/HTTP-localhost.keytab + kadmin.local -q "ktadd -keytab testdir/HTTP-localhost.keytab HTTP/localhost" + + kadmin.local -q "addprinc -randkey vagrant" + rm -f testdir/vagrant.keytab + kadmin.local -q "ktadd -keytab testdir/vagrant.keytab vagrant" + + kadmin.local -q "addprinc -randkey unpriv" + rm -f testdir/unpriv.keytab + kadmin.local -q "ktadd -keytab testdir/unpriv.keytab unpriv" + + kadmin.local -q "addprinc -randkey root" + rm -f testdir/root.keytab + kadmin.local -q "ktadd -keytab testdir/root.keytab root" + + sudo cp /vagrant/examples/vagrant/upstart/aurora-scheduler-kerberos.conf \ + /etc/init/aurora-scheduler-kerberos.conf + aurorabuild scheduler + sudo stop aurora-scheduler || true + sudo start aurora-scheduler-kerberos + await_scheduler_ready + snapshot_as vagrant + cat snapshot-response.vagrant.json + grep -q 'lacks permission' snapshot-response.vagrant.json + snapshot_as unpriv + cat snapshot-response.unpriv.json + grep -q 'lacks permission' snapshot-response.unpriv.json + snapshot_as root + cat snapshot-response.root.json + grep -qv 'lacks permission' snapshot-response.root.json +} + +function tear_down { + sudo stop aurora-scheduler-kerberos || true + sudo rm -f /etc/init/aurora-scheduler-kerberos.conf + sudo start aurora-scheduler || true +} + +function main { + if [[ "$USER" != "vagrant" ]]; then + enter_vagrant "$@" + elif [[ -z "${KRB5_CONFIG:-}" ]]; then + enter_testrealm "$@" + else + trap tear_down EXIT + test_snapshot + set +x + echo + echo '*** OK (All tests passed) ***' + echo + fi +} + +main "$@"
