com.amazon.redshift.core.IamHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of redshift-jdbc42 Show documentation
Show all versions of redshift-jdbc42 Show documentation
Java JDBC 4.2 (JRE 8+) driver for Redshift database
The newest version!
package com.amazon.redshift.core;
import com.amazon.redshift.util.RedshiftProperties;
import com.amazonaws.AmazonClientException;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.BasicSessionCredentials;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.auth.profile.ProfilesConfigFile;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration;
import com.amazonaws.services.redshift.AmazonRedshift;
import com.amazonaws.services.redshift.AmazonRedshiftClientBuilder;
import com.amazonaws.services.redshift.model.Cluster;
import com.amazonaws.services.redshift.model.DescribeClustersRequest;
import com.amazonaws.services.redshift.model.DescribeClustersResult;
import com.amazonaws.services.redshift.model.Endpoint;
import com.amazonaws.services.redshift.model.GetClusterCredentialsRequest;
import com.amazonaws.services.redshift.model.GetClusterCredentialsResult;
import com.amazonaws.services.redshift.model.GetClusterCredentialsWithIAMRequest;
import com.amazonaws.services.redshift.model.GetClusterCredentialsWithIAMResult;
import com.amazonaws.services.redshift.model.DescribeCustomDomainAssociationsRequest;
import com.amazonaws.services.redshift.model.DescribeCustomDomainAssociationsResult;
import com.amazonaws.services.redshift.model.Association;
import com.amazonaws.services.redshift.AmazonRedshiftClient;
import com.amazonaws.services.redshift.AmazonRedshiftClientBuilder;
import com.amazonaws.util.StringUtils;
import com.amazon.redshift.CredentialsHolder;
import com.amazon.redshift.IPlugin;
import com.amazon.redshift.RedshiftProperty;
import com.amazon.redshift.jdbc.RedshiftConnectionImpl;
import com.amazon.redshift.logger.LogLevel;
import com.amazon.redshift.logger.RedshiftLogger;
import com.amazon.redshift.plugin.utils.RequestUtils;
import com.amazon.redshift.util.GT;
import com.amazon.redshift.util.RedshiftException;
import com.amazon.redshift.util.RedshiftState;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class IamHelper extends IdpAuthHelper {
static final int MAX_AMAZONCLIENT_RETRY = 5;
static final int MAX_AMAZONCLIENT_RETRY_DELAY_MS = 1000;
private static final String KEY_PREFERRED_ROLE = "preferred_role";
private static final String KEY_ROLE_SESSION_NAME = "roleSessionName";
private static final String KEY_ROLE_ARN = "roleArn";
// Type of GetClusterCredential API
public static final int GET_CLUSTER_CREDENTIALS_V1_API = 1;
public static final int GET_CLUSTER_CREDENTIALS_IAM_V2_API = 2;
public static final int GET_CLUSTER_CREDENTIALS_SAML_V2_API = 3;
public static final int GET_CLUSTER_CREDENTIALS_JWT_V2_API = 4;
public static final int GET_SERVERLESS_CREDENTIALS_V1_API = 5;
private static final Pattern HOST_PATTERN =
Pattern.compile("(.+)\\.(.+)\\.(.+).redshift(-dev)?\\.amazonaws\\.com(.)*");
private static final Pattern SERVERLESS_WORKGROUP_HOST_PATTERN =
Pattern.compile("(.+)\\.(.+)\\.(.+).redshift-serverless(-dev)?\\.amazonaws\\.com(.)*");
enum CredentialProviderType
{
NONE, PROFILE, IAM_KEYS_WITH_SESSION, IAM_KEYS, PLUGIN
}
private static Map credentialsCache = new HashMap();
private static Map credentialsV2Cache = new HashMap();
private IamHelper() {
}
/**
* Helper function to handle IAM connection properties. If any IAM related
* connection property is specified, all other required IAM properties
* must be specified too or else it throws an error.
*
* @param info
* Redshift client settings used to authenticate if connection should
* be granted.
* @param settings
* Redshift IAM settings
* @param log
* Redshift logger
*
* @return New property object with properties from auth profile and given
* input info properties, if auth profile found. Otherwise same
* property object as info return.
*
* @throws RedshiftException
* If an error occurs.
*/
public static RedshiftProperties setIAMProperties(RedshiftProperties info, RedshiftJDBCSettings settings, RedshiftLogger log)
throws RedshiftException {
try {
// Common code for IAM and Native Auth
info = setAuthProperties(info, settings, log);
// IAM keys
String iamAccessKey = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.IAM_ACCESS_KEY_ID.getName(),
info);
String iamSecretKey = RedshiftConnectionImpl
.getOptionalConnSetting(RedshiftProperty.IAM_SECRET_ACCESS_KEY.getName(), info);
String iamSessionToken = RedshiftConnectionImpl
.getOptionalConnSetting(RedshiftProperty.IAM_SESSION_TOKEN.getName(), info);
String authProfile = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.AUTH_PROFILE.getName(), info);
String host = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.HOST.getName(), info);
String userSetServerless = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.IS_SERVERLESS.getName(), info);
Boolean hasUserSetServerless = false;
if(null != userSetServerless) {
hasUserSetServerless = "true".equalsIgnoreCase(userSetServerless);
}
String acctId = null;
String workGroup = null;
Matcher mProvisioned = null;
Matcher mServerless = null;
if(null != host)
{
mProvisioned = HOST_PATTERN.matcher(host);
mServerless = SERVERLESS_WORKGROUP_HOST_PATTERN.matcher(host);
}
String clusterId = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.CLUSTER_IDENTIFIER.getName(), info);;
if ((null != mProvisioned && mProvisioned.matches()) || (null != clusterId && clusterId.startsWith("redshift-serverless-")))
{
// provisioned vanilla OR serverless backdoor which allows calling getClusterCredentials which is a provisioned API
if (RedshiftLogger.isEnable())
log.logInfo("Code flow for regular provisioned cluster");
clusterId = RedshiftConnectionImpl.getRequiredConnSetting(RedshiftProperty.CLUSTER_IDENTIFIER.getName(), info);
}
else if (null != mServerless && mServerless.matches())
{
// serverless vanilla
// do nothing, regular serverless logic flow
if (RedshiftLogger.isEnable())
log.logInfo("Code flow for regular serverless cluster");
// String isServerless = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.IS_SERVERLESS.getName(), info);
// settings.m_isServerless = isServerless == null ? false : Boolean.valueOf(isServerless);
settings.m_isServerless = true;
acctId = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.SERVERLESS_ACCT_ID.getName(), info);
workGroup = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.SERVERLESS_WORK_GROUP.getName(), info);
}
else if (hasUserSetServerless)
{
// hostname doesn't match serverless regex but serverless set to true explicitly by user
// when ready for implementation, remove setting of the isServerless property automatically in parseUrl(),
// set it here instead
// currently do nothing as server does not support cname for serverless
settings.m_isServerless = true;
workGroup = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.SERVERLESS_WORK_GROUP.getName(), info);
acctId = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.SERVERLESS_ACCT_ID.getName(), info);
if(workGroup != null)
{
// workgroup specified by user - serverless nlb call
// check for serverlessAcctId to enter serverless NLB logic flow, for when we implement this for serverless after server side is ready
// currently do nothing as regular code flow is sufficient
if (RedshiftLogger.isEnable())
log.logInfo("Code flow for nlb serverless cluster");
}
else
{
// attempt serverless cname call - currently not supported by server
// currently sets isCname to true which will be asserted on later, as cname for serverless is not supported yet
if (RedshiftLogger.isEnable())
log.logInfo("Code flow for cname serverless cluster");
settings.m_isCname = true;
}
}
else
{
if (RedshiftLogger.isEnable())
log.logInfo("Code flow for nlb/cname in provisioned clusters");
// attempt provisioned cname call
// cluster id will be fetched upon describing custom domain name
settings.m_isCname = true;
}
String awsRegion = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.AWS_REGION.getName(), info);
String endpointUrl = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.ENDPOINT_URL.getName(), info);
String stsEndpointUrl = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.STS_ENDPOINT_URL.getName(),
info);
String profile = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.AWS_PROFILE.getName(), info);
if (profile == null)
profile = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.AWS_PROFILE.getName().toLowerCase(),
info);
String iamDuration = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.IAM_DURATION.getName(), info);
String iamAutoCreate = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.USER_AUTOCREATE.getName(),
info);
String iamDbUser = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.DB_USER.getName(), info);
String iamDbGroups = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.DB_GROUPS.getName(), info);
String iamForceLowercase = RedshiftConnectionImpl
.getOptionalConnSetting(RedshiftProperty.FORCE_LOWERCASE.getName(), info);
String iamGroupFederation = RedshiftConnectionImpl
.getOptionalConnSetting(RedshiftProperty.GROUP_FEDERATION.getName(), info);
String dbName = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.DBNAME.getName(), info);
String hosts = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.HOST.getName(), info);
String ports = RedshiftConnectionImpl.getOptionalConnSetting(RedshiftProperty.PORT.getName(), info);
settings.m_clusterIdentifier = clusterId;
if (!settings.m_isServerless && !settings.m_isCname
&& (null == settings.m_clusterIdentifier || settings.m_clusterIdentifier.isEmpty()))
{
RedshiftException err = new RedshiftException(
GT.tr("Missing connection property {0}", RedshiftProperty.CLUSTER_IDENTIFIER.getName()),
RedshiftState.UNEXPECTED_ERROR);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
}
if (settings.m_isServerless) {
settings.m_acctId = acctId;
settings.m_workGroup = workGroup;
}
// Regions.fromName(string) requires the string to be lower case and in
// this format:
// E.g. "us-west-2"
if (null != awsRegion) {
settings.m_awsRegion = awsRegion.trim().toLowerCase();
}
if (null != endpointUrl) {
settings.m_endpoint = endpointUrl;
} else {
settings.m_endpoint = System.getProperty("redshift.endpoint-url");
}
if (null != stsEndpointUrl) {
settings.m_stsEndpoint = stsEndpointUrl;
} else {
settings.m_stsEndpoint = System.getProperty("sts.endpoint-url");
}
if (null != profile) {
settings.m_profile = profile;
}
if (null != iamDuration) {
try {
settings.m_iamDuration = Integer.parseInt(iamDuration);
if (settings.m_iamDuration < 900 || settings.m_iamDuration > 3600) {
RedshiftException err = new RedshiftException(
GT.tr("Invalid connection property value or type range(900-3600) {0}",
RedshiftProperty.IAM_DURATION.getName()),
RedshiftState.UNEXPECTED_ERROR);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
}
} catch (NumberFormatException e) {
RedshiftException err = new RedshiftException(GT.tr("Invalid connection property value {0} : {1}",
RedshiftProperty.IAM_DURATION.getName(), iamDuration), RedshiftState.UNEXPECTED_ERROR, e);
if (RedshiftLogger.isEnable())
log.log(LogLevel.DEBUG, err.toString());
throw err;
}
}
if (null != iamAccessKey) {
settings.m_iamAccessKeyID = iamAccessKey;
}
// Because the secret access key should be hidden, and most applications
// (for example:
// SQL Workbench) only hide passwords, Amazon has requested that we allow
// the
// secret access key to be passed as either the IAMSecretAccessKey
// property or
// as a password value.
if (null != iamSecretKey) {
if (StringUtils.isNullOrEmpty(settings.m_iamAccessKeyID)) {
RedshiftException err = new RedshiftException(
GT.tr("Missing connection property {0}", RedshiftProperty.IAM_ACCESS_KEY_ID.getName()),
RedshiftState.UNEXPECTED_ERROR);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
}
settings.m_iamSecretKey = iamSecretKey;
if (settings.m_iamSecretKey.isEmpty()) {
settings.m_iamSecretKey = settings.m_password;
}
} else {
settings.m_iamSecretKey = settings.m_password;
}
if (null != iamSessionToken) {
if (StringUtils.isNullOrEmpty(settings.m_iamAccessKeyID)) {
RedshiftException err = new RedshiftException(
GT.tr("Missing connection property {0}", RedshiftProperty.IAM_ACCESS_KEY_ID.getName()),
RedshiftState.UNEXPECTED_ERROR);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
}
settings.m_iamSessionToken = iamSessionToken;
}
settings.m_autocreate = iamAutoCreate == null ? null : Boolean.valueOf(iamAutoCreate);
settings.m_forceLowercase = iamForceLowercase == null ? null : Boolean.valueOf(iamForceLowercase);
settings.m_groupFederation = iamGroupFederation == null ? false : Boolean.valueOf(iamGroupFederation);
if (null != iamDbUser) {
settings.m_dbUser = iamDbUser;
}
settings.m_dbGroups = (iamDbGroups != null)
? Arrays.asList((settings.m_forceLowercase != null && settings.m_forceLowercase
? iamDbGroups.toLowerCase(Locale.getDefault()) : iamDbGroups).split(","))
: Collections.emptyList();
settings.m_Schema = dbName;
if (hosts != null) {
settings.m_host = hosts;
}
if (ports != null) {
settings.m_port = Integer.parseInt(ports);
}
setIAMCredentials(settings, log, authProfile);
return info;
} catch (RedshiftException re) {
if (RedshiftLogger.isEnable())
log.logError(re);
throw re;
}
}
/**
* Helper function to create the appropriate credential providers.
*
* @throws RedshiftException
* If an unspecified error occurs.
*/
private static void setIAMCredentials(RedshiftJDBCSettings settings, RedshiftLogger log, String authProfile) throws RedshiftException {
AWSCredentialsProvider provider;
CredentialProviderType providerType = CredentialProviderType.NONE;
boolean idpCredentialsRefresh = false;
String idpToken = null;
if (!StringUtils.isNullOrEmpty(settings.m_credentialsProvider)) {
if (!StringUtils.isNullOrEmpty(settings.m_profile)) {
RedshiftException err = new RedshiftException(
GT.tr("Conflict in connection property setting {0} and {1}",
RedshiftProperty.CREDENTIALS_PROVIDER.getName(), RedshiftProperty.AWS_PROFILE.getName()),
RedshiftState.UNEXPECTED_ERROR);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
}
if (StringUtils.isNullOrEmpty(authProfile)
&& !StringUtils.isNullOrEmpty(settings.m_iamAccessKeyID)) {
RedshiftException err = new RedshiftException(
GT.tr("Conflict in connection property setting {0} and {1}",
RedshiftProperty.CREDENTIALS_PROVIDER.getName(), RedshiftProperty.IAM_ACCESS_KEY_ID.getName()),
RedshiftState.UNEXPECTED_ERROR);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
}
try {
Class extends AWSCredentialsProvider> clazz = (Class.forName(settings.m_credentialsProvider)
.asSubclass(AWSCredentialsProvider.class));
provider = clazz.newInstance();
if (provider instanceof IPlugin) {
IPlugin plugin = ((IPlugin) provider);
providerType = CredentialProviderType.PLUGIN;
plugin.setLogger(log);
plugin.setGroupFederation(settings.m_groupFederation);
for (Map.Entry entry : settings.m_pluginArgs.entrySet()) {
String pluginArgKey = entry.getKey();
plugin.addParameter(pluginArgKey, entry.getValue());
if (KEY_PREFERRED_ROLE.equalsIgnoreCase(pluginArgKey))
settings.m_preferredRole = entry.getValue();
else if (KEY_ROLE_ARN.equalsIgnoreCase(pluginArgKey))
settings.m_roleArn = entry.getValue();
else if (KEY_ROLE_SESSION_NAME.equalsIgnoreCase(pluginArgKey))
settings.m_roleSessionName = entry.getValue();
else if (RedshiftProperty.DB_GROUPS_FILTER.getName().equalsIgnoreCase(pluginArgKey))
settings.m_dbGroupsFilter = entry.getValue();
}
}
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
RedshiftException err = new RedshiftException(
GT.tr("Invalid credentials provider class {0}", settings.m_credentialsProvider),
RedshiftState.UNEXPECTED_ERROR, e);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
} catch (NumberFormatException e) {
RedshiftException err = new RedshiftException(
GT.tr("{0} : {1}", e.getMessage(), settings.m_credentialsProvider), RedshiftState.UNEXPECTED_ERROR, e);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
}
} else if (!StringUtils.isNullOrEmpty(settings.m_profile)) {
if (StringUtils.isNullOrEmpty(authProfile)
&& !StringUtils.isNullOrEmpty(settings.m_iamAccessKeyID)) {
RedshiftException err = new RedshiftException(GT.tr("Conflict in connection property setting {0} and {1}",
RedshiftProperty.AWS_PROFILE.getName(), RedshiftProperty.IAM_ACCESS_KEY_ID.getName()),
RedshiftState.UNEXPECTED_ERROR);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
}
ProfilesConfigFile pcf = new PluginProfilesConfigFile(settings, log);
provider = new ProfileCredentialsProvider(pcf, settings.m_profile);
providerType = CredentialProviderType.PROFILE;
} else if (!StringUtils.isNullOrEmpty(settings.m_iamAccessKeyID)) {
AWSCredentials credentials;
if (!StringUtils.isNullOrEmpty(settings.m_iamSessionToken)) {
credentials = new BasicSessionCredentials(settings.m_iamAccessKeyID, settings.m_iamSecretKey,
settings.m_iamSessionToken);
providerType = CredentialProviderType.IAM_KEYS_WITH_SESSION;
} else {
credentials = new BasicAWSCredentials(settings.m_iamAccessKeyID, settings.m_iamSecretKey);
providerType = CredentialProviderType.IAM_KEYS;
}
provider = new AWSStaticCredentialsProvider(credentials);
} else {
provider = new DefaultAWSCredentialsProviderChain();
}
if (RedshiftLogger.isEnable())
log.log(LogLevel.DEBUG, "IDP Credential Provider {0}:{1}", provider, settings.m_credentialsProvider);
int getClusterCredentialApiType = findTypeOfGetClusterCredentialsAPI(settings, providerType, provider);
if (getClusterCredentialApiType == GET_CLUSTER_CREDENTIALS_V1_API
|| getClusterCredentialApiType == GET_CLUSTER_CREDENTIALS_IAM_V2_API
|| getClusterCredentialApiType == GET_SERVERLESS_CREDENTIALS_V1_API)
{
if (RedshiftLogger.isEnable())
log.log(LogLevel.DEBUG, "Calling provider.getCredentials()");
// Provider will cache the credentials, it's OK to call getCredentials()
// here.
AWSCredentials credentials = provider.getCredentials();
if (credentials instanceof CredentialsHolder) {
idpCredentialsRefresh = ((CredentialsHolder) credentials).isRefresh();
// autoCreate, user and password from URL take priority.
CredentialsHolder.IamMetadata im = ((CredentialsHolder) credentials).getMetadata();
if (null != im) {
Boolean autoCreate = im.getAutoCreate();
String dbUser = im.getDbUser();
String samlDbUser = im.getSamlDbUser();
String profileDbUser = im.getProfileDbUser();
String dbGroups = im.getDbGroups();
boolean forceLowercase = im.getForceLowercase();
boolean allowDbUserOverride = im.getAllowDbUserOverride();
if (null == settings.m_autocreate) {
settings.m_autocreate = autoCreate;
}
if (null == settings.m_forceLowercase) {
settings.m_forceLowercase = forceLowercase;
}
/*
* Order of precedence when configuring settings.m_dbUser:
*
* If allowDbUserOverride = true: 1. Value from SAML assertion. 2.
* Value from connection string setting. 3. Value from credentials
* profile setting.
*
* If allowDbUserOverride = false (default): 1. Value from connection
* string setting. 2. Value from credentials profile setting. 3. Value
* from SAML assertion.
*/
if (allowDbUserOverride) {
if (null != samlDbUser) {
settings.m_dbUser = samlDbUser;
} else if (null != dbUser) {
settings.m_dbUser = dbUser;
} else if (null != profileDbUser) {
settings.m_dbUser = profileDbUser;
}
} else {
if (null != dbUser) {
settings.m_dbUser = dbUser;
} else if (null != profileDbUser) {
settings.m_dbUser = profileDbUser;
} else if (null != samlDbUser) {
settings.m_dbUser = samlDbUser;
}
}
if (settings.m_dbGroups.isEmpty() && null != dbGroups) {
settings.m_dbGroups = Arrays
.asList((settings.m_forceLowercase ? dbGroups.toLowerCase(Locale.getDefault()) : dbGroups).split(","));
}
}
}
if ("*".equals(settings.m_username) && null == settings.m_dbUser) {
RedshiftException err = new RedshiftException(
GT.tr("Missing connection property {0}", RedshiftProperty.DB_USER.getName()),
RedshiftState.UNEXPECTED_ERROR);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
}
} // V1 Or IAM_V2 for provisional cluster or serverless
else {
// TODO not yet decided
if (RedshiftLogger.isEnable())
log.log(LogLevel.DEBUG, "groupFederation=" + settings.m_groupFederation);
// Check for GetClusterCredentialsV2 cache
// Combine key of IDP and V2 API
String key = null;
GetClusterCredentialsWithIAMResult credentials = null;
if (!settings.m_iamDisableCache) {
key = getCredentialsV2CacheKey(settings, providerType, provider, getClusterCredentialApiType, false);
credentials = credentialsV2Cache.get(key);
}
if (credentials == null
|| RequestUtils.isCredentialExpired(credentials.getExpiration())) {
// If not found or expired
// Get IDP token
if (providerType == CredentialProviderType.PLUGIN) {
IPlugin plugin = (IPlugin) provider;
if (RedshiftLogger.isEnable())
log.log(LogLevel.DEBUG, "Calling plugin.getIdpToken()");
idpToken = plugin.getIdpToken();
}
settings.m_idpToken = idpToken;
}
} // Group federation API for plugin
setClusterCredentials(provider, settings, log, providerType, idpCredentialsRefresh, getClusterCredentialApiType);
}
/**
* Calls the AWS SDK methods to return temporary credentials. The expiration
* date is returned as the local time set by the client machines OS.
*
* @throws RedshiftException
* If getting the cluster credentials fails.
*/
private static void setClusterCredentials(AWSCredentialsProvider credProvider, RedshiftJDBCSettings settings,
RedshiftLogger log, CredentialProviderType providerType, boolean idpCredentialsRefresh,
int getClusterCredentialApiType) throws RedshiftException {
try {
AmazonRedshiftClientBuilder builder = AmazonRedshiftClientBuilder.standard();
builder = (AmazonRedshiftClientBuilder) setBuilderConfiguration(settings, log, builder);
switch (getClusterCredentialApiType) {
case GET_CLUSTER_CREDENTIALS_V1_API:
// Call Provision cluster V1 API
AmazonRedshift client = builder.withCredentials(credProvider).build();
callDescribeCustomDomainNameAssociationsAPIForV1(settings, client, log);
callDescribeClustersAPIForV1(settings, client, log);
if (RedshiftLogger.isEnable())
log.log(LogLevel.DEBUG, "Call V1 API of GetClusterCredentials");
GetClusterCredentialsResult result = getClusterCredentialsResult(settings, client, log, providerType,
idpCredentialsRefresh);
settings.m_username = result.getDbUser();
settings.m_password = result.getDbPassword();
if (RedshiftLogger.isEnable())
{
Date now = new Date();
log.logInfo(now + ": Using GetClusterCredentialsResult with expiration " + result.getExpiration());
}
break;
case GET_SERVERLESS_CREDENTIALS_V1_API:
// Serverless V1 API
ServerlessIamHelper serverlessIamHelper = new ServerlessIamHelper(settings, log, credProvider);
if (null == settings.m_host || settings.m_port == 0) {
serverlessIamHelper.describeConfiguration(settings);
}
if (RedshiftLogger.isEnable())
log.log(LogLevel.DEBUG, "Call Serverless V1 API of GetCredentials");
serverlessIamHelper.getCredentialsResult(settings, providerType, idpCredentialsRefresh);
break;
case GET_CLUSTER_CREDENTIALS_IAM_V2_API:
// Call V2 IAM API Provision
AmazonRedshiftClient iamClient = (AmazonRedshiftClient) builder.withCredentials(credProvider).build();
callDescribeCustomDomainNameAssociationsAPIForV2(settings, iamClient, log);
callDescribeClustersAPIForV2(settings, iamClient, log);
if (RedshiftLogger.isEnable())
log.log(LogLevel.DEBUG, "Call V2 API of GetClusterCredentials");
GetClusterCredentialsWithIAMResult iamResult = getClusterCredentialsResultV2(settings, iamClient, log, providerType,
idpCredentialsRefresh, credProvider, getClusterCredentialApiType);
settings.m_username = iamResult.getDbUser();
settings.m_password = iamResult.getDbPassword();
// result will contain TimeToRefresh
if (RedshiftLogger.isEnable()) {
Date now = new Date();
log.logInfo(now + ": Using GetClusterCredentialsResultV2 with expiration " + iamResult.getExpiration());
log.logInfo(now + ": Using GetClusterCredentialsResultV2 with TimeToRefresh " + iamResult.getNextRefreshTime());
}
break;
}
}
catch (AmazonClientException e)
{
RedshiftException err = new RedshiftException(GT.tr("IAM error retrieving temp credentials: {0}", e.getMessage()),
RedshiftState.UNEXPECTED_ERROR, e);
if (RedshiftLogger.isEnable())
log.log(LogLevel.ERROR, err.toString());
throw err;
}
}
/**
* Helper function to call the DescribeClustersAPIForV2 for IAM clients for provisioned clusters
*/
static void callDescribeClustersAPIForV2(RedshiftJDBCSettings settings, AmazonRedshiftClient iamClient, RedshiftLogger log)
{
if (null == settings.m_host || settings.m_port == 0)
{
if (RedshiftLogger.isEnable())
log.logInfo("calling describe clusters API with clusterID : " + settings.m_clusterIdentifier);
DescribeClustersRequest req = new DescribeClustersRequest();
req.setClusterIdentifier(settings.m_clusterIdentifier);
DescribeClustersResult resp = iamClient.describeClusters(req);
List clusters = resp.getClusters();
if (clusters.isEmpty()) {
throw new AmazonClientException("Failed to describeClusters.");
}
Cluster cluster = clusters.get(0);
Endpoint endpoint = cluster.getEndpoint();
if (null == endpoint) {
throw new AmazonClientException("Cluster is not fully created yet.");
}
settings.m_host = endpoint.getAddress();
settings.m_port = endpoint.getPort();
}
}
/**
* Helper function to call the DescribeClustersAPIForV1 for provisioned clusters
*/
static void callDescribeClustersAPIForV1(RedshiftJDBCSettings settings, AmazonRedshift client, RedshiftLogger log)
{
if (null == settings.m_host || settings.m_port == 0)
{
if (RedshiftLogger.isEnable())
log.logInfo("calling describe clusters API with clusterID : " + settings.m_clusterIdentifier);
DescribeClustersRequest req = new DescribeClustersRequest();
req.setClusterIdentifier(settings.m_clusterIdentifier);
DescribeClustersResult resp = client.describeClusters(req);
List clusters = resp.getClusters();
if (clusters.isEmpty()) {
throw new AmazonClientException("Failed to describeClusters.");
}
Cluster cluster = clusters.get(0);
Endpoint endpoint = cluster.getEndpoint();
if (null == endpoint) {
throw new AmazonClientException("Cluster is not fully created yet.");
}
settings.m_host = endpoint.getAddress();
settings.m_port = endpoint.getPort();
}
}
/**
* Helper function to call the DescribeCustomDomainNameAssociationsAPI for IAM clients for provisioned clusters
*/
static void callDescribeCustomDomainNameAssociationsAPIForV2(RedshiftJDBCSettings settings, AmazonRedshiftClient iamClient, RedshiftLogger log) throws RedshiftException
{
if(settings.m_isCname)
{
DescribeCustomDomainAssociationsRequest describeRequest = new DescribeCustomDomainAssociationsRequest();
if(null != settings.m_host)
{
// this is traditional case where we pass in the host, aka custom domain name to the API
if (RedshiftLogger.isEnable())
log.logInfo("calling describe cname associations API with hostname : " + settings.m_host);
describeRequest.setCustomDomainName(settings.m_host);
}
else
{
if (RedshiftLogger.isEnable())
log.logInfo("No CNAME provided. No-op.");
return;
}
try
{
DescribeCustomDomainAssociationsResult describeResponse = iamClient.describeCustomDomainAssociations(describeRequest);
List associations = describeResponse.getAssociations();
// API itself will throw if result list's count is 0, so we enter catch case
if(associations.stream().count() > 1)
{
if (RedshiftLogger.isEnable())
log.logInfo("Multiple associations received for provided custom domain name : " + describeRequest.getCustomDomainName() + ". Only one expected.");
return;
}
String clusterID = describeResponse.getAssociations().get(0).getCertificateAssociations().get(0).getClusterIdentifier();
if(null != clusterID && !clusterID.isEmpty()) {
settings.m_clusterIdentifier = clusterID;
if (RedshiftLogger.isEnable())
log.logDebug("setting cluster ID to : " + settings.m_clusterIdentifier);
}
}
catch (Exception ex)
{
if (RedshiftLogger.isEnable())
log.logInfo("No cluster identifier received from Redshift CNAME lookup. Setting CNAME to false.");
settings.m_isCname = false; }
}
}
/**
* Helper function to call the DescribeCustomDomainNameAssociationsAPI for provisioned clusters
*/
static void callDescribeCustomDomainNameAssociationsAPIForV1(RedshiftJDBCSettings settings, AmazonRedshift client, RedshiftLogger log) throws RedshiftException
{
if(settings.m_isCname)
{
DescribeCustomDomainAssociationsRequest describeRequest = new DescribeCustomDomainAssociationsRequest();
if(null != settings.m_host)
{
// this is traditional case where we pass in the host, aka custom domain name to the API
if (RedshiftLogger.isEnable())
log.logInfo("calling describe cname associations API with hostname : " + settings.m_host);
describeRequest.setCustomDomainName(settings.m_host);
}
else
{
if (RedshiftLogger.isEnable())
log.logInfo("No CNAME provided. No-op.");
return;
}
try
{
DescribeCustomDomainAssociationsResult describeResponse = client.describeCustomDomainAssociations(describeRequest);
List associations = describeResponse.getAssociations();
// API itself will throw if result list's count is 0, so we enter catch case
if(associations.stream().count() > 1)
{
if (RedshiftLogger.isEnable())
log.logInfo("Multiple associations received for provided custom domain name : " + describeRequest.getCustomDomainName() + ". Only one expected.");
return;
}
String clusterID = describeResponse.getAssociations().get(0).getCertificateAssociations().get(0).getClusterIdentifier();
if(null != clusterID && !clusterID.isEmpty()) {
settings.m_clusterIdentifier = clusterID;
if (RedshiftLogger.isEnable())
log.logDebug("setting cluster ID to : " + settings.m_clusterIdentifier);
}
}
catch (Exception ex)
{
if (RedshiftLogger.isEnable())
log.logInfo("No cluster identifier received from Redshift CNAME lookup. Setting CNAME to false.");
settings.m_isCname = false;
}
}
}
private static synchronized GetClusterCredentialsResult getClusterCredentialsResult(RedshiftJDBCSettings settings,
AmazonRedshift client, RedshiftLogger log, CredentialProviderType providerType, boolean idpCredentialsRefresh)
throws AmazonClientException {
String key = null;
GetClusterCredentialsResult credentials = null;
if (!settings.m_iamDisableCache) {
key = getCredentialsCacheKey(settings, providerType, false);
credentials = credentialsCache.get(key);
}
if (credentials == null || (providerType == CredentialProviderType.PLUGIN && idpCredentialsRefresh)
|| RequestUtils.isCredentialExpired(credentials.getExpiration())) {
if (RedshiftLogger.isEnable())
log.logInfo("GetClusterCredentials NOT from cache");
if (!settings.m_iamDisableCache)
credentialsCache.remove(key);
if(settings.m_isCname)
{
// construct request packet with cname
GetClusterCredentialsRequest request = constructRequestForGetClusterCredentials(settings, true, log);
try
{
// make api call with cname
credentials = makeGetClusterCredentialsAPICall(request, credentials, client, log);
}
catch(AmazonClientException ace)
{
// if api call with cname fails, recreate request packet with clusterid and re-make api call
if(RedshiftLogger.isEnable())
{
log.logInfo("GetClusterCredentials API call failed with CNAME request. Retrying with ClusterID.");
}
request = constructRequestForGetClusterCredentials(settings, false, log);
credentials = makeGetClusterCredentialsAPICall(request, credentials, client, log);
}
}
else
{
// construct request packet with clusterid and make api call
GetClusterCredentialsRequest request = constructRequestForGetClusterCredentials(settings, false, log);
credentials = makeGetClusterCredentialsAPICall(request, credentials, client, log);
}
if (!settings.m_iamDisableCache)
credentialsCache.put(key, credentials);
}
else
{
if (RedshiftLogger.isEnable())
log.logInfo("GetClusterCredentials from cache");
}
return credentials;
}
/**
* Helper function to construct the request object for GetClusterCredentials API
*/
static GetClusterCredentialsRequest constructRequestForGetClusterCredentials(RedshiftJDBCSettings settings, boolean constructWithCname, RedshiftLogger log)
{
GetClusterCredentialsRequest request = new GetClusterCredentialsRequest();
if (settings.m_iamDuration > 0)
{
request.setDurationSeconds(settings.m_iamDuration);
}
request.setDbName(settings.m_Schema);
request.setDbUser(settings.m_dbUser == null ? settings.m_username : settings.m_dbUser);
request.setAutoCreate(settings.m_autocreate);
request.setDbGroups(settings.m_dbGroups);
if(constructWithCname)
{
request.setCustomDomainName(settings.m_host);
}
else
{
request.setClusterIdentifier(settings.m_clusterIdentifier);
}
if (RedshiftLogger.isEnable())
{
log.logInfo(request.toString());
}
return request;
}
/**
* Helper function to make the API call to GetClusterCredentials
*/
static GetClusterCredentialsResult makeGetClusterCredentialsAPICall(GetClusterCredentialsRequest request, GetClusterCredentialsResult credentials, AmazonRedshift client, RedshiftLogger log)
{
for (int i = 0; i < MAX_AMAZONCLIENT_RETRY; ++i)
{
try
{
credentials = client.getClusterCredentials(request);
break;
}
catch (AmazonClientException ce)
{
if(RedshiftLogger.isEnable())
log.logDebug("Call to getClusterCredentials failed with error: " + ce.getMessage());
checkForApiCallRateExceedError(ce, i, "getClusterCredentialsResult", log);
}
}
return credentials;
}
static void checkForApiCallRateExceedError(AmazonClientException ace, int i, String callerMethod, RedshiftLogger log)
throws AmazonClientException {
if (ace.getMessage().contains("Rate exceeded") && i < MAX_AMAZONCLIENT_RETRY - 1) {
if (RedshiftLogger.isEnable())
log.logInfo(callerMethod + " caught 'Rate exceeded' error...");
try {
Thread.sleep(MAX_AMAZONCLIENT_RETRY_DELAY_MS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
} else {
throw ace;
}
}
private static synchronized GetClusterCredentialsWithIAMResult getClusterCredentialsResultV2(
RedshiftJDBCSettings settings, AmazonRedshiftClient client, RedshiftLogger log,
CredentialProviderType providerType, boolean idpCredentialsRefresh, AWSCredentialsProvider provider,
int getClusterCredentialApiType) throws AmazonClientException
{
String key = null;
GetClusterCredentialsWithIAMResult credentials = null;
if (!settings.m_iamDisableCache)
{
key = getCredentialsV2CacheKey(settings, providerType, provider, getClusterCredentialApiType, false);
credentials = credentialsV2Cache.get(key);
}
if (credentials == null || (providerType == CredentialProviderType.PLUGIN && settings.m_idpToken != null)
|| RequestUtils.isCredentialExpired(credentials.getExpiration()))
{
if (RedshiftLogger.isEnable())
log.logInfo("GetClusterCredentialsV2 NOT from cache");
if (!settings.m_iamDisableCache)
credentialsV2Cache.remove(key);
if(settings.m_isCname)
{
// construct request packet with cname
GetClusterCredentialsWithIAMRequest request = constructRequestForGetClusterCredentialsWithIAM(settings, true, log);
try
{
// make api call with cname
credentials = makeGetClusterCredentialsWithIAMAPICall(request, credentials, client, log);
}
catch (AmazonClientException ce)
{
// if api call with cname fails, recreate request packet with clusterid and re-make api call
if(RedshiftLogger.isEnable())
{
log.logInfo("GetClusterCredentials API call failed with CNAME request. Retrying with ClusterID.");
}
request = constructRequestForGetClusterCredentialsWithIAM(settings, false, log);
credentials = makeGetClusterCredentialsWithIAMAPICall(request, credentials, client, log);
}
}
else
{
// construct request packet with clusterid and make api call
GetClusterCredentialsWithIAMRequest request = constructRequestForGetClusterCredentialsWithIAM(settings, false, log);
credentials = makeGetClusterCredentialsWithIAMAPICall(request, credentials, client, log);
}
if (!settings.m_iamDisableCache)
credentialsV2Cache.put(key, credentials);
}
else
{
if (RedshiftLogger.isEnable())
log.logInfo("GetClusterCredentialsV2 from cache");
}
return credentials;
}
/**
* Helper function to construct the request object for GetClusterCredentialsWithIAM API
*/
static GetClusterCredentialsWithIAMRequest constructRequestForGetClusterCredentialsWithIAM(RedshiftJDBCSettings settings, boolean constructWithCname, RedshiftLogger log)
{
GetClusterCredentialsWithIAMRequest request = new GetClusterCredentialsWithIAMRequest();
if (settings.m_iamDuration > 0) {
request.setDurationSeconds(settings.m_iamDuration);
}
request.setDbName(settings.m_Schema);
if (constructWithCname)
{
request.setCustomDomainName(settings.m_host);
}
else
{
request.setClusterIdentifier(settings.m_clusterIdentifier);
}
if (RedshiftLogger.isEnable())
log.logInfo(request.toString());
return request;
}
/**
* Helper function to make the API call to GetClusterCredentialsWithIAM
*/
static GetClusterCredentialsWithIAMResult makeGetClusterCredentialsWithIAMAPICall(GetClusterCredentialsWithIAMRequest request, GetClusterCredentialsWithIAMResult credentials, AmazonRedshiftClient client, RedshiftLogger log)
{
for (int i = 0; i < MAX_AMAZONCLIENT_RETRY; ++i)
{
try
{
credentials = client.getClusterCredentialsWithIAM(request);
break;
}
catch (AmazonClientException ace)
{
checkForApiCallRateExceedError(ace, i, "getClusterCredentialsResultV2", log);
}
}
return credentials;
}
static String getCredentialsCacheKey(RedshiftJDBCSettings settings, CredentialProviderType providerType,
boolean serverless) {
String key;
String dbGroups = "";
if (settings.m_dbGroups != null && !settings.m_dbGroups.isEmpty()) {
Collections.sort(settings.m_dbGroups);
dbGroups = String.join(",", settings.m_dbGroups);
}
key = ((!serverless) ? settings.m_clusterIdentifier : settings.m_acctId) + ";"
+ ((serverless && settings.m_workGroup != null) ? settings.m_workGroup : "") + ";"
+ (settings.m_dbUser == null ? settings.m_username : settings.m_dbUser) + ";"
+ (settings.m_Schema == null ? "" : settings.m_Schema) + ";" + dbGroups + ";" + settings.m_autocreate + ";"
+ settings.m_iamDuration;
switch (providerType) {
case PROFILE: {
key += ";" + settings.m_profile;
break;
}
case IAM_KEYS_WITH_SESSION: {
key += ";" + settings.m_iamAccessKeyID + ";" + settings.m_iamSecretKey + ";" + settings.m_iamSessionToken;
break;
}
case IAM_KEYS: {
key += ";" + settings.m_iamAccessKeyID + ";" + settings.m_iamSecretKey;
break;
}
default: {
break;
}
} // Switch
return key;
}
static String getCredentialsV2CacheKey(RedshiftJDBCSettings settings, CredentialProviderType providerType,
AWSCredentialsProvider provider, int getClusterCredentialApiType, boolean serverless) {
String key = "";
if (providerType == CredentialProviderType.PLUGIN) {
// Get IDP key
IPlugin plugin = (IPlugin) provider;
key = plugin.getCacheKey();
}
// Combine IDP key with V2 API parameters
key += (((!serverless) ? settings.m_clusterIdentifier : settings.m_acctId) + ";"
+ ((serverless && settings.m_workGroup != null) ? settings.m_workGroup : "") + ";"
+ (settings.m_Schema == null ? "" : settings.m_Schema) + ";" + settings.m_iamDuration);
if (getClusterCredentialApiType == GET_CLUSTER_CREDENTIALS_SAML_V2_API) {
if (settings.m_preferredRole != null) {
key += (settings.m_preferredRole + ";");
}
if (settings.m_dbGroupsFilter != null) {
key += (settings.m_dbGroupsFilter + ";");
}
} else if (getClusterCredentialApiType == GET_CLUSTER_CREDENTIALS_JWT_V2_API) {
if (settings.m_idpToken != null) {
key += (settings.m_idpToken + ";");
}
if (settings.m_roleArn != null) {
key += (settings.m_roleArn + ";");
}
if (settings.m_roleSessionName != null) {
key += (settings.m_roleSessionName + ";");
}
}
switch (providerType) {
case PROFILE: {
key += ";" + settings.m_profile;
break;
}
case IAM_KEYS_WITH_SESSION: {
key += ";" + settings.m_iamAccessKeyID + ";" + settings.m_iamSecretKey + ";" + settings.m_iamSessionToken;
break;
}
case IAM_KEYS: {
key += ";" + settings.m_iamAccessKeyID + ";" + settings.m_iamSecretKey;
break;
}
default: {
break;
}
} // Switch
return key;
}
private static int findTypeOfGetClusterCredentialsAPI(RedshiftJDBCSettings settings,
CredentialProviderType providerType, AWSCredentialsProvider provider) {
if (!settings.m_isServerless)
{
if (!settings.m_groupFederation)
return GET_CLUSTER_CREDENTIALS_V1_API;
else
{
return GET_CLUSTER_CREDENTIALS_IAM_V2_API;
}
} else {
// Serverless
return GET_SERVERLESS_CREDENTIALS_V1_API;
}
}
static AwsClientBuilder setBuilderConfiguration(RedshiftJDBCSettings settings, RedshiftLogger log,
AwsClientBuilder builder) {
ClientConfiguration clientConfig = RequestUtils.getProxyClientConfig(log);
if (clientConfig != null) {
builder.setClientConfiguration(clientConfig);
}
if (RedshiftLogger.isEnable()) {
log.logInfo("setBuilderConfiguration: settings.m_endpoint= " + settings.m_endpoint + " settings.m_awsRegion = "
+ settings.m_awsRegion);
}
if (settings.m_endpoint != null) {
EndpointConfiguration cfg = new EndpointConfiguration(settings.m_endpoint, settings.m_awsRegion);
builder.setEndpointConfiguration(cfg);
} else if (settings.m_awsRegion != null && !settings.m_awsRegion.isEmpty()) {
builder.setRegion(settings.m_awsRegion);
}
return builder;
}
}