Skip to content

Commit

Permalink
Introduce SubjectProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
Praveen2112 committed Oct 20, 2021
1 parent 82bfa66 commit 4f63e10
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

import com.google.common.base.CharMatcher;
import com.google.common.net.HostAndPort;
import io.trino.client.auth.kerberos.LoginBasedSubjectProvider;
import io.trino.client.auth.kerberos.SpnegoHandler;
import io.trino.client.auth.kerberos.SubjectProvider;
import okhttp3.Credentials;
import okhttp3.Interceptor;
import okhttp3.JavaNetCookieJar;
Expand Down Expand Up @@ -289,7 +291,8 @@ public static void setupKerberos(
Optional<File> keytab,
Optional<File> credentialCache)
{
SpnegoHandler handler = new SpnegoHandler(servicePrincipalPattern, remoteServiceName, useCanonicalHostname, principal, kerberosConfig, keytab, credentialCache);
SubjectProvider subjectProvider = new LoginBasedSubjectProvider(principal, kerberosConfig, keytab, credentialCache);
SpnegoHandler handler = new SpnegoHandler(servicePrincipalPattern, remoteServiceName, useCanonicalHostname, subjectProvider);
clientBuilder.addInterceptor(handler);
clientBuilder.authenticator(handler);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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 io.trino.client.auth.kerberos;

import com.google.common.collect.ImmutableMap;
import com.sun.security.auth.module.Krb5LoginModule;
import org.ietf.jgss.GSSException;

import javax.annotation.concurrent.GuardedBy;
import javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import java.io.File;
import java.util.Objects;
import java.util.Optional;

import static com.google.common.base.Preconditions.checkState;
import static java.lang.Boolean.getBoolean;
import static java.util.Objects.requireNonNull;
import static javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.REQUIRED;

public class LoginBasedSubjectProvider
implements SubjectProvider
{
private final Optional<String> principal;
private final Optional<File> keytab;
private final Optional<File> credentialCache;

@GuardedBy("this")
private LoginContext loginContext;

public LoginBasedSubjectProvider(
Optional<String> principal,
Optional<File> kerberosConfig,
Optional<File> keytab,
Optional<File> credentialCache)
{
this.principal = requireNonNull(principal, "principal is null");
this.keytab = requireNonNull(keytab, "keytab is null");
this.credentialCache = requireNonNull(credentialCache, "credentialCache is null");

kerberosConfig.ifPresent(file -> {
String newValue = file.getAbsolutePath();
String currentValue = System.getProperty("java.security.krb5.conf");
checkState(
currentValue == null || Objects.equals(currentValue, newValue),
"Refusing to set system property 'java.security.krb5.conf' to '%s', it is already set to '%s'",
newValue,
currentValue);
System.setProperty("java.security.krb5.conf", newValue);
});
}

@Override
public Subject getSubject()
{
return loginContext.getSubject();
}

@Override
public void refresh()
throws LoginException, GSSException
{
// TODO: do we need to call logout() on the LoginContext?

loginContext = new LoginContext("", null, null, new Configuration()
{
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name)
{
ImmutableMap.Builder<String, String> options = ImmutableMap.builder();
options.put("refreshKrb5Config", "true");
options.put("doNotPrompt", "true");
options.put("useKeyTab", "true");

if (getBoolean("trino.client.debugKerberos")) {
options.put("debug", "true");
}

keytab.ifPresent(file -> options.put("keyTab", file.getAbsolutePath()));

credentialCache.ifPresent(file -> {
options.put("ticketCache", file.getAbsolutePath());
options.put("useTicketCache", "true");
options.put("renewTGT", "true");
});

principal.ifPresent(value -> options.put("principal", value));

return new AppConfigurationEntry[] {
new AppConfigurationEntry(Krb5LoginModule.class.getName(), REQUIRED, options.build())
};
}
});

loginContext.login();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
package io.trino.client.auth.kerberos;

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.sun.security.auth.module.Krb5LoginModule;
import io.airlift.units.Duration;
import io.trino.client.ClientException;
import okhttp3.Authenticator;
Expand All @@ -31,12 +29,8 @@

import javax.annotation.concurrent.GuardedBy;
import javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
Expand All @@ -45,20 +39,15 @@
import java.security.PrivilegedExceptionAction;
import java.util.Base64;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;

import static com.google.common.base.CharMatcher.whitespace;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Throwables.throwIfInstanceOf;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.net.HttpHeaders.WWW_AUTHENTICATE;
import static java.lang.Boolean.getBoolean;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.SECONDS;
import static javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.REQUIRED;
import static org.ietf.jgss.GSSContext.INDEFINITE_LIFETIME;
import static org.ietf.jgss.GSSCredential.DEFAULT_LIFETIME;
import static org.ietf.jgss.GSSCredential.INITIATE_ONLY;
Expand All @@ -80,39 +69,21 @@ public class SpnegoHandler
private final String servicePrincipalPattern;
private final String remoteServiceName;
private final boolean useCanonicalHostname;
private final Optional<String> principal;
private final Optional<File> keytab;
private final Optional<File> credentialCache;
private final SubjectProvider subjectProvider;

@GuardedBy("this")
private Session clientSession;
private GSSCredential clientCredential;

public SpnegoHandler(
String servicePrincipalPattern,
String remoteServiceName,
boolean useCanonicalHostname,
Optional<String> principal,
Optional<File> kerberosConfig,
Optional<File> keytab,
Optional<File> credentialCache)
SubjectProvider subjectProvider)
{
this.servicePrincipalPattern = requireNonNull(servicePrincipalPattern, "servicePrincipalPattern is null");
this.remoteServiceName = requireNonNull(remoteServiceName, "remoteServiceName is null");
this.useCanonicalHostname = useCanonicalHostname;
this.principal = requireNonNull(principal, "principal is null");
this.keytab = requireNonNull(keytab, "keytab is null");
this.credentialCache = requireNonNull(credentialCache, "credentialCache is null");

kerberosConfig.ifPresent(file -> {
String newValue = file.getAbsolutePath();
String currentValue = System.getProperty("java.security.krb5.conf");
checkState(
currentValue == null || Objects.equals(currentValue, newValue),
"Refusing to set system property 'java.security.krb5.conf' to '%s', it is already set to '%s'",
newValue,
currentValue);
System.setProperty("java.security.krb5.conf", newValue);
});
this.subjectProvider = requireNonNull(subjectProvider, "subjectProvider is null");
}

@Override
Expand Down Expand Up @@ -161,12 +132,12 @@ private byte[] generateToken(String servicePrincipal)
{
GSSContext context = null;
try {
Session session = getSession();
context = doAs(session.getLoginContext().getSubject(), () -> {
GSSCredential clientCredential = getGssCredential();
context = doAs(subjectProvider.getSubject(), () -> {
GSSContext result = GSS_MANAGER.createContext(
GSS_MANAGER.createName(servicePrincipal, NT_HOSTBASED_SERVICE),
SPNEGO_OID,
session.getClientCredential(),
clientCredential,
INDEFINITE_LIFETIME);

result.requestMutualAuth(true);
Expand Down Expand Up @@ -196,60 +167,26 @@ private byte[] generateToken(String servicePrincipal)
}
}

private synchronized Session getSession()
private synchronized GSSCredential getGssCredential()
throws LoginException, GSSException
{
if ((clientSession == null) || clientSession.needsRefresh()) {
clientSession = createSession();
if ((clientCredential == null) || clientCredential.getRemainingLifetime() < MIN_CREDENTIAL_LIFETIME.getValue(SECONDS)) {
clientCredential = createGssCredential();
}
return clientSession;
return clientCredential;
}

private Session createSession()
private GSSCredential createGssCredential()
throws LoginException, GSSException
{
// TODO: do we need to call logout() on the LoginContext?

LoginContext loginContext = new LoginContext("", null, null, new Configuration()
{
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name)
{
ImmutableMap.Builder<String, String> options = ImmutableMap.builder();
options.put("refreshKrb5Config", "true");
options.put("doNotPrompt", "true");
options.put("useKeyTab", "true");

if (getBoolean("trino.client.debugKerberos")) {
options.put("debug", "true");
}

keytab.ifPresent(file -> options.put("keyTab", file.getAbsolutePath()));

credentialCache.ifPresent(file -> {
options.put("ticketCache", file.getAbsolutePath());
options.put("useTicketCache", "true");
options.put("renewTGT", "true");
});

principal.ifPresent(value -> options.put("principal", value));

return new AppConfigurationEntry[] {
new AppConfigurationEntry(Krb5LoginModule.class.getName(), REQUIRED, options.build())
};
}
});

loginContext.login();
Subject subject = loginContext.getSubject();
subjectProvider.refresh();
Subject subject = subjectProvider.getSubject();
Principal clientPrincipal = subject.getPrincipals().iterator().next();
GSSCredential clientCredential = doAs(subject, () -> GSS_MANAGER.createCredential(
return doAs(subject, () -> GSS_MANAGER.createCredential(
GSS_MANAGER.createName(clientPrincipal.getName(), NT_USER_NAME),
DEFAULT_LIFETIME,
KERBEROS_OID,
INITIATE_ONLY));

return new Session(loginContext, clientCredential);
}

private static String makeServicePrincipal(String servicePrincipalPattern, String serviceName, String hostName, boolean useCanonicalHostname)
Expand Down Expand Up @@ -311,35 +248,4 @@ private static Oid createOid(String value)
throw new AssertionError(e);
}
}

private static class Session
{
private final LoginContext loginContext;
private final GSSCredential clientCredential;

public Session(LoginContext loginContext, GSSCredential clientCredential)
{
requireNonNull(loginContext, "loginContext is null");
requireNonNull(clientCredential, "clientCredential is null");

this.loginContext = loginContext;
this.clientCredential = clientCredential;
}

public LoginContext getLoginContext()
{
return loginContext;
}

public GSSCredential getClientCredential()
{
return clientCredential;
}

public boolean needsRefresh()
throws GSSException
{
return clientCredential.getRemainingLifetime() < MIN_CREDENTIAL_LIFETIME.getValue(SECONDS);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 io.trino.client.auth.kerberos;

import org.ietf.jgss.GSSException;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;

public interface SubjectProvider
{
Subject getSubject();

void refresh()
throws LoginException, GSSException;
}

0 comments on commit 4f63e10

Please sign in to comment.