/*
 * Copyright DataStax, Inc.
 *
 * 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 com.datastax.dse.driver.api.core.auth;

import com.datastax.oss.driver.api.core.auth.AuthProvider;
import com.datastax.oss.driver.api.core.auth.AuthenticationException;
import com.datastax.oss.driver.api.core.auth.Authenticator;
import com.datastax.oss.driver.api.core.metadata.EndPoint;
import com.datastax.oss.driver.api.core.session.Session;
import com.datastax.oss.driver.shaded.guava.common.base.Charsets;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
import com.datastax.oss.protocol.internal.util.Bytes;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
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 javax.security.sasl.Sasl;
import javax.security.sasl.SaslClient;
import javax.security.sasl.SaslException;
import net.jcip.annotations.Immutable;
import net.jcip.annotations.NotThreadSafe;
import net.jcip.annotations.ThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ThreadSafe
public abstract class DseGssApiAuthProviderBase implements AuthProvider {

  
The default SASL service name used by this auth provider.
/** The default SASL service name used by this auth provider. */
public static final String DEFAULT_SASL_SERVICE_NAME = "dse";
The name of the system property to use to specify the SASL service name.
/** The name of the system property to use to specify the SASL service name. */
public static final String SASL_SERVICE_NAME_PROPERTY = "dse.sasl.service";
Legacy system property for SASL protocol name. Clients should migrate to SASL_SERVICE_NAME_PROPERTY above.
/** * Legacy system property for SASL protocol name. Clients should migrate to * SASL_SERVICE_NAME_PROPERTY above. */
private static final String LEGACY_SASL_PROTOCOL_PROPERTY = "dse.sasl.protocol"; private static final Logger LOG = LoggerFactory.getLogger(DseGssApiAuthProviderBase.class); private final String logPrefix;
Params:
  • logPrefix – a string that will get prepended to the logs (this is used for discrimination when you have multiple driver instances executing in the same JVM). Config-based implementations fill this with Session.getName().
/** * @param logPrefix a string that will get prepended to the logs (this is used for discrimination * when you have multiple driver instances executing in the same JVM). Config-based * implementations fill this with {@link Session#getName()}. */
protected DseGssApiAuthProviderBase(@NonNull String logPrefix) { this.logPrefix = Objects.requireNonNull(logPrefix); } @NonNull protected abstract GssApiOptions getOptions( @NonNull EndPoint endPoint, @NonNull String serverAuthenticator); @NonNull @Override public Authenticator newAuthenticator( @NonNull EndPoint endPoint, @NonNull String serverAuthenticator) throws AuthenticationException { return new GssApiAuthenticator( getOptions(endPoint, serverAuthenticator), endPoint, serverAuthenticator); } @Override public void onMissingChallenge(@NonNull EndPoint endPoint) { LOG.warn( "[{}] {} did not send an authentication challenge; " + "This is suspicious because the driver expects authentication", logPrefix, endPoint); } @Override public void close() { // nothing to do }
The options to initialize a new authenticator.

Use builder() to create an instance.

/** * The options to initialize a new authenticator. * * <p>Use {@link #builder()} to create an instance. */
@Immutable public static class GssApiOptions { @NonNull public static Builder builder() { return new Builder(); } private final Configuration loginConfiguration; private final Subject subject; private final String saslProtocol; private final String authorizationId; private final Map<String, String> saslProperties; private GssApiOptions( @Nullable Configuration loginConfiguration, @Nullable Subject subject, @Nullable String saslProtocol, @Nullable String authorizationId, @NonNull Map<String, String> saslProperties) { this.loginConfiguration = loginConfiguration; this.subject = subject; this.saslProtocol = saslProtocol; this.authorizationId = authorizationId; this.saslProperties = saslProperties; } @Nullable public Configuration getLoginConfiguration() { return loginConfiguration; } @Nullable public Subject getSubject() { return subject; } @Nullable public String getSaslProtocol() { return saslProtocol; } @Nullable public String getAuthorizationId() { return authorizationId; } @NonNull public Map<String, String> getSaslProperties() { return saslProperties; } @NotThreadSafe public static class Builder { private Configuration loginConfiguration; private Subject subject; private String saslProtocol; private String authorizationId; private final Map<String, String> saslProperties = new HashMap<>(); public Builder() { saslProperties.put(Sasl.SERVER_AUTH, "true"); saslProperties.put(Sasl.QOP, "auth"); }
Sets a login configuration that will be used to create a LoginContext.

You MUST call either a withLoginConfiguration method or withSubject(Subject); if both are called, the subject takes precedence, and the login configuration will be ignored.

See Also:
/** * Sets a login configuration that will be used to create a {@link LoginContext}. * * <p>You MUST call either a withLoginConfiguration method or {@link #withSubject(Subject)}; * if both are called, the subject takes precedence, and the login configuration will be * ignored. * * @see #withLoginConfiguration(Map) */
@NonNull public Builder withLoginConfiguration(@Nullable Configuration loginConfiguration) { this.loginConfiguration = loginConfiguration; return this; }
Sets a login configuration that will be used to create a LoginContext.

This is an alternative to withLoginConfiguration(Configuration), that builds the configuration from Krb5LoginModule with the given options.

You MUST call either a withLoginConfiguration method or withSubject(Subject); if both are called, the subject takes precedence, and the login configuration will be ignored.

/** * Sets a login configuration that will be used to create a {@link LoginContext}. * * <p>This is an alternative to {@link #withLoginConfiguration(Configuration)}, that builds * the configuration from {@code Krb5LoginModule} with the given options. * * <p>You MUST call either a withLoginConfiguration method or {@link #withSubject(Subject)}; * if both are called, the subject takes precedence, and the login configuration will be * ignored. */
@NonNull public Builder withLoginConfiguration(@Nullable Map<String, String> loginConfiguration) { this.loginConfiguration = fetchLoginConfiguration(loginConfiguration); return this; }
Sets a previously authenticated subject to reuse.

You MUST call either this method or withLoginConfiguration(Configuration); if both are called, the subject takes precedence, and the login configuration will be ignored.

/** * Sets a previously authenticated subject to reuse. * * <p>You MUST call either this method or {@link #withLoginConfiguration(Configuration)}; if * both are called, the subject takes precedence, and the login configuration will be ignored. */
@NonNull public Builder withSubject(@Nullable Subject subject) { this.subject = subject; return this; }
Sets the SASL protocol name to use; should match the username of the Kerberos service principal used by the DSE server.
/** * Sets the SASL protocol name to use; should match the username of the Kerberos service * principal used by the DSE server. */
@NonNull public Builder withSaslProtocol(@Nullable String saslProtocol) { this.saslProtocol = saslProtocol; return this; }
Sets the authorization ID (allows proxy authentication).
/** Sets the authorization ID (allows proxy authentication). */
@NonNull public Builder withAuthorizationId(@Nullable String authorizationId) { this.authorizationId = authorizationId; return this; }
Add a SASL property to use when creating the SASL client.

Note that this builder pre-initializes these two default properties:

javax.security.sasl.server.authentication = true
javax.security.sasl.qop = auth
/** * Add a SASL property to use when creating the SASL client. * * <p>Note that this builder pre-initializes these two default properties: * * <pre> * javax.security.sasl.server.authentication = true * javax.security.sasl.qop = auth * </pre> */
@NonNull public Builder addSaslProperty(@NonNull String name, @NonNull String value) { this.saslProperties.put(Objects.requireNonNull(name), Objects.requireNonNull(value)); return this; } @NonNull public GssApiOptions build() { return new GssApiOptions( loginConfiguration, subject, saslProtocol, authorizationId, ImmutableMap.copyOf(saslProperties)); } public static Configuration fetchLoginConfiguration(Map<String, String> options) { return new Configuration() { @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { return new AppConfigurationEntry[] { new AppConfigurationEntry( "com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) }; } }; } } } protected static class GssApiAuthenticator extends BaseDseAuthenticator { private static final ByteBuffer MECHANISM = ByteBuffer.wrap("GSSAPI".getBytes(Charsets.UTF_8)).asReadOnlyBuffer(); private static final ByteBuffer SERVER_INITIAL_CHALLENGE = ByteBuffer.wrap("GSSAPI-START".getBytes(Charsets.UTF_8)).asReadOnlyBuffer(); private static final ByteBuffer EMPTY_BYTE_ARRAY = ByteBuffer.wrap(new byte[0]).asReadOnlyBuffer(); private static final String JAAS_CONFIG_ENTRY = "DseClient"; private static final String[] SUPPORTED_MECHANISMS = new String[] {"GSSAPI"}; private Subject subject; private SaslClient saslClient; private EndPoint endPoint; protected GssApiAuthenticator( GssApiOptions options, EndPoint endPoint, String serverAuthenticator) { super(serverAuthenticator); try { if (options.getSubject() != null) { this.subject = options.getSubject(); } else { Configuration loginConfiguration = options.getLoginConfiguration(); if (loginConfiguration == null) { throw new IllegalArgumentException("Must provide one of subject or loginConfiguration"); } LoginContext login = new LoginContext(JAAS_CONFIG_ENTRY, null, null, loginConfiguration); login.login(); this.subject = login.getSubject(); } String protocol = options.getSaslProtocol(); if (protocol == null) { protocol = System.getProperty( SASL_SERVICE_NAME_PROPERTY, System.getProperty(LEGACY_SASL_PROTOCOL_PROPERTY, DEFAULT_SASL_SERVICE_NAME)); } this.saslClient = Sasl.createSaslClient( SUPPORTED_MECHANISMS, options.getAuthorizationId(), protocol, ((InetSocketAddress) endPoint.resolve()).getAddress().getCanonicalHostName(), options.getSaslProperties(), null); } catch (LoginException | SaslException e) { throw new AuthenticationException(endPoint, e.getMessage()); } this.endPoint = endPoint; } @NonNull @Override protected ByteBuffer getMechanism() { return MECHANISM; } @NonNull @Override protected ByteBuffer getInitialServerChallenge() { return SERVER_INITIAL_CHALLENGE; } @Nullable @Override public ByteBuffer evaluateChallengeSync(@Nullable ByteBuffer challenge) { byte[] challengeBytes; if (SERVER_INITIAL_CHALLENGE.equals(challenge)) { if (!saslClient.hasInitialResponse()) { return EMPTY_BYTE_ARRAY; } challengeBytes = new byte[0]; } else { // The native protocol spec says the incoming challenge can be null depending on the // implementation. But saslClient.evaluateChallenge clearly documents that the byte array // can't be null, which probably means that a SASL authenticator never sends back null. if (challenge == null) { throw new AuthenticationException(this.endPoint, "Unexpected null challenge from server"); } challengeBytes = Bytes.getArray(challenge); } try { return ByteBuffer.wrap( Subject.doAs( subject, new PrivilegedExceptionAction<byte[]>() { @Override public byte[] run() throws SaslException { return saslClient.evaluateChallenge(challengeBytes); } })); } catch (PrivilegedActionException e) { throw new AuthenticationException(this.endPoint, e.getMessage(), e.getException()); } } } }