/**
* 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 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.io.Files;
import com.google.inject.AbstractModule;
import com.google.inject.PrivateModule;
import com.sun.security.auth.login.ConfigFile;
import com.sun.security.auth.module.Krb5LoginModule;
import org.apache.aurora.common.args.Arg;
import org.apache.aurora.common.args.CmdLine;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.Oid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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 = LoggerFactory.getLogger(Kerberos5ShiroRealmModule.class);
/**
* 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/aurora.example.com@EXAMPLE.COM")
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.debug("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;
}
GSSCredential serverCredential;
try {
LoginContext loginContext = new LoginContext(
getClass().getName(),
null /* subject (read from jaas config file passed below) */,
null /* callbackHandler */,
new ConfigFile(jaasConfFile.toURI()));
loginContext.login();
serverCredential = Subject.doAs(
loginContext.getSubject(),
(PrivilegedAction<GSSCredential>) () -> {
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 new RuntimeException(e);
}
});
} catch (LoginException e) {
addError(e);
return;
}
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);
}
}