Skip to content

Add support for SSO credentials provider #2118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSingleSignon-9e785be.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"category": "AWS Single Sign-on",
"contributor": "",
"type": "feature",
"description": "Added support for retrieving SSO credentials: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html."
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import java.lang.annotation.Target;

/**
* Marker interface for 'internal' APIs that should not be used outside the core module. Breaking
* Marker interface for 'internal' APIs that should not be used outside the same module. Breaking
* changes can and will be introduced to elements marked as {@link SdkInternalApi}. Users of the SDK
* and the generated clients themselves should not depend on any packages, types, fields,
* constructors, or methods with this annotation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.auth.credentials;

import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.profiles.Profile;

/**
* A factory for {@link AwsCredentialsProvider}s, which can be used to create different credentials providers with
* different Profile properties.
*/
@FunctionalInterface
@SdkProtectedApi
public interface ProfileCredentialsProviderFactory {
AwsCredentialsProvider create(Profile profile);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
import software.amazon.awssdk.auth.credentials.ProcessCredentialsProvider;
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProviderFactory;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
import software.amazon.awssdk.core.internal.util.ClassLoaderHelper;
Expand All @@ -50,6 +51,8 @@
public final class ProfileCredentialsUtils {
private static final String STS_PROFILE_CREDENTIALS_PROVIDER_FACTORY =
"software.amazon.awssdk.services.sts.internal.StsProfileCredentialsProviderFactory";
private static final String SSO_PROFILE_CREDENTIALS_PROVIDER_FACTORY =
"software.amazon.awssdk.services.sso.auth.SsoProfileCredentialsProviderFactory";

private final Profile profile;

Expand Down Expand Up @@ -95,19 +98,22 @@ public Optional<AwsCredentialsProvider> credentialsProvider() {
* @param children The child profiles that source credentials from this profile.
*/
private Optional<AwsCredentialsProvider> credentialsProvider(Set<String> children) {
if (properties.containsKey(ProfileProperty.ROLE_ARN) && properties.containsKey(ProfileProperty.WEB_IDENTITY_TOKEN_FILE)) {
return Optional.ofNullable(roleAndWebIdentityTokenProfileCredentialsProvider());
}

if (properties.containsKey(ProfileProperty.SSO_ROLE_NAME) || properties.containsKey(ProfileProperty.SSO_ACCOUNT_ID)
|| properties.containsKey(ProfileProperty.SSO_REGION) || properties.containsKey(ProfileProperty.SSO_START_URL)) {
return Optional.ofNullable(ssoProfileCredentialsProvider());
}

if (properties.containsKey(ProfileProperty.ROLE_ARN)) {
boolean hasSourceProfile = properties.containsKey(ProfileProperty.SOURCE_PROFILE);
boolean hasCredentialSource = properties.containsKey(ProfileProperty.CREDENTIAL_SOURCE);
boolean hasWebIdentityTokenFile = properties.containsKey(ProfileProperty.WEB_IDENTITY_TOKEN_FILE);
boolean hasRoleArn = properties.containsKey(ProfileProperty.ROLE_ARN);
Validate.validState(!(hasSourceProfile && hasCredentialSource),
"Invalid profile file: profile has both %s and %s.",
ProfileProperty.SOURCE_PROFILE, ProfileProperty.CREDENTIAL_SOURCE);

if (hasWebIdentityTokenFile && hasRoleArn) {
return Optional.ofNullable(roleAndWebIdentityTokenProfileCredentialsProvider());
}

if (hasSourceProfile) {
return Optional.ofNullable(roleAndSourceProfileBasedProfileCredentialsProvider(children));
}
Expand Down Expand Up @@ -164,6 +170,17 @@ private AwsCredentialsProvider credentialProcessCredentialsProvider() {
.build();
}

/**
* Create the SSO credentials provider based on the related profile properties.
*/
private AwsCredentialsProvider ssoProfileCredentialsProvider() {
requireProperties(ProfileProperty.SSO_ACCOUNT_ID,
ProfileProperty.SSO_REGION,
ProfileProperty.SSO_ROLE_NAME,
ProfileProperty.SSO_START_URL);
return ssoCredentialsProviderFactory().create(profile);
}

private AwsCredentialsProvider roleAndWebIdentityTokenProfileCredentialsProvider() {
requireProperties(ProfileProperty.ROLE_ARN, ProfileProperty.WEB_IDENTITY_TOKEN_FILE);

Expand Down Expand Up @@ -263,4 +280,20 @@ private ChildProfileCredentialsProviderFactory stsCredentialsProviderFactory() {
throw new IllegalStateException("Failed to create the '" + name + "' profile credentials provider.", e);
}
}

/**
* Load the factory that can be used to create the SSO credentials provider, assuming it is on the classpath.
*/
private ProfileCredentialsProviderFactory ssoCredentialsProviderFactory() {
try {
Class<?> ssoProfileCredentialsProviderFactory = ClassLoaderHelper.loadClass(SSO_PROFILE_CREDENTIALS_PROVIDER_FACTORY,
getClass());
return (ProfileCredentialsProviderFactory) ssoProfileCredentialsProviderFactory.getConstructor().newInstance();
} catch (ClassNotFoundException e) {
throw new IllegalStateException("To use Sso related properties in the '" + name + "' profile, the 'sso' service "
+ "module must be on the class path.", e);
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new IllegalStateException("Failed to create the '" + name + "' profile credentials provider.", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@

package software.amazon.awssdk.profiles;

import static software.amazon.awssdk.utils.UserHomeDirectoryUtils.userHomeDirectory;

import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.regex.Pattern;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.utils.JavaSystemSetting;
import software.amazon.awssdk.utils.StringUtils;

/**
* A collection of static methods for loading the location for configuration and credentials files.
Expand All @@ -46,7 +45,7 @@ private ProfileFileLocation() {
public static Path configurationFilePath() {
return resolveProfileFilePath(
ProfileFileSystemSetting.AWS_CONFIG_FILE.getStringValue()
.orElse(Paths.get(ProfileFileLocation.userHomeDirectory(),
.orElse(Paths.get(userHomeDirectory(),
".aws", "config").toString()));
}

Expand Down Expand Up @@ -78,43 +77,6 @@ public static Optional<Path> credentialsFileLocation() {
return resolveIfExists(credentialsFilePath());
}

/**
* Load the home directory that should be used for the profile file. This will check the same environment variables as the CLI
* to identify the location of home, before falling back to java-specific resolution.
*/
@SdkInternalApi
static String userHomeDirectory() {
boolean isWindows = JavaSystemSetting.OS_NAME.getStringValue()
.map(s -> StringUtils.lowerCase(s).startsWith("windows"))
.orElse(false);

// To match the logic of the CLI we have to consult environment variables directly.
// CHECKSTYLE:OFF
String home = System.getenv("HOME");

if (home != null) {
return home;
}

if (isWindows) {
String userProfile = System.getenv("USERPROFILE");

if (userProfile != null) {
return userProfile;
}

String homeDrive = System.getenv("HOMEDRIVE");
String homePath = System.getenv("HOMEPATH");

if (homeDrive != null && homePath != null) {
return homeDrive + homePath;
}
}

return JavaSystemSetting.USER_HOME.getStringValueOrThrow();
// CHECKSTYLE:ON
}

private static Path resolveProfileFilePath(String path) {
// Resolve ~ using the CLI's logic, not whatever Java decides to do with it.
if (HOME_DIRECTORY_PATTERN.matcher(path).matches()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,28 @@ public final class ProfileProperty {
*/
public static final String RETRY_MODE = "retry_mode";

/**
* Aws region where the SSO directory for the given 'sso_start_url' is hosted. This is independent of the general 'region'.
*/
public static final String SSO_REGION = "sso_region";

/**
* The corresponding IAM role in the AWS account that temporary AWS credentials will be resolved for.
*/
public static final String SSO_ROLE_NAME = "sso_role_name";

/**
* AWS account ID that temporary AWS credentials will be resolved for.
*/
public static final String SSO_ACCOUNT_ID = "sso_account_id";

/**
* Start url provided by the SSO service via the console. It's the main URL used for login to the SSO directory.
* This is also referred to as the "User Portal URL" and can also be used to login to the SSO web interface for AWS
* console access.
*/
public static final String SSO_START_URL = "sso_start_url";

private ProfileProperty() {
}
}
18 changes: 18 additions & 0 deletions services/sso/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,23 @@
<artifactId>aws-json-protocol</artifactId>
<version>${awsjavasdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>profiles</artifactId>
<version>${awsjavasdk.version}</version>
<scope>compile</scope>
</dependency>

<!-- Test Dependencies -->
<dependency>
<groupId>com.google.jimfs</groupId>
<artifactId>jimfs</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.services.sso.auth;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.core.SdkField;
import software.amazon.awssdk.core.SdkPojo;
import software.amazon.awssdk.core.exception.SdkClientException;

/**
* <p>
* The session token that was passed is expired or is not valid.
* </p>
*/
@SdkPublicApi
public final class ExpiredTokenException extends SdkClientException {

private static final List<SdkField<?>> SDK_FIELDS = Collections.unmodifiableList(Arrays.asList());

private ExpiredTokenException(Builder b) {
super(b);
}

@Override
public Builder toBuilder() {
return new BuilderImpl(this);
}

public static Builder builder() {
return new BuilderImpl();
}

public interface Builder extends SdkPojo, SdkClientException.Builder {
@Override
Builder message(String message);

@Override
Builder cause(Throwable cause);

@Override
ExpiredTokenException build();
}

static final class BuilderImpl extends SdkClientException.BuilderImpl implements Builder {
private BuilderImpl() {
}

private BuilderImpl(ExpiredTokenException model) {
super(model);
}

@Override
public BuilderImpl message(String message) {
this.message = message;
return this;
}

@Override
public BuilderImpl cause(Throwable cause) {
this.cause = cause;
return this;
}

@Override
public ExpiredTokenException build() {
return new ExpiredTokenException(this);
}

@Override
public List<SdkField<?>> sdkFields() {
return SDK_FIELDS;
}
}
}
Loading