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 "$@"

Reply via email to