mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-20 11:48:24 -06:00
Fixes: #11461 This will allow the user to pass a policy to further restrict the use of AssumeRole. It is important to note that it will NOT allow an expansion of access rights
218 lines
7.3 KiB
Go
218 lines
7.3 KiB
Go
package aws
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
awsCredentials "github.com/aws/aws-sdk-go/aws/credentials"
|
|
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
|
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
|
|
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/aws/aws-sdk-go/service/sts"
|
|
"github.com/hashicorp/errwrap"
|
|
"github.com/hashicorp/go-cleanhttp"
|
|
)
|
|
|
|
func GetAccountInfo(iamconn *iam.IAM, stsconn *sts.STS, authProviderName string) (string, string, error) {
|
|
// If we have creds from instance profile, we can use metadata API
|
|
if authProviderName == ec2rolecreds.ProviderName {
|
|
log.Println("[DEBUG] Trying to get account ID via AWS Metadata API")
|
|
|
|
cfg := &aws.Config{}
|
|
setOptionalEndpoint(cfg)
|
|
sess, err := session.NewSession(cfg)
|
|
if err != nil {
|
|
return "", "", errwrap.Wrapf("Error creating AWS session: {{err}}", err)
|
|
}
|
|
|
|
metadataClient := ec2metadata.New(sess)
|
|
info, err := metadataClient.IAMInfo()
|
|
if err != nil {
|
|
// This can be triggered when no IAM Role is assigned
|
|
// or AWS just happens to return invalid response
|
|
return "", "", fmt.Errorf("Failed getting EC2 IAM info: %s", err)
|
|
}
|
|
|
|
return parseAccountInfoFromArn(info.InstanceProfileArn)
|
|
}
|
|
|
|
// Then try IAM GetUser
|
|
log.Println("[DEBUG] Trying to get account ID via iam:GetUser")
|
|
outUser, err := iamconn.GetUser(nil)
|
|
if err == nil {
|
|
return parseAccountInfoFromArn(*outUser.User.Arn)
|
|
}
|
|
|
|
awsErr, ok := err.(awserr.Error)
|
|
// AccessDenied and ValidationError can be raised
|
|
// if credentials belong to federated profile, so we ignore these
|
|
if !ok || (awsErr.Code() != "AccessDenied" && awsErr.Code() != "ValidationError") {
|
|
return "", "", fmt.Errorf("Failed getting account ID via 'iam:GetUser': %s", err)
|
|
}
|
|
log.Printf("[DEBUG] Getting account ID via iam:GetUser failed: %s", err)
|
|
|
|
// Then try STS GetCallerIdentity
|
|
log.Println("[DEBUG] Trying to get account ID via sts:GetCallerIdentity")
|
|
outCallerIdentity, err := stsconn.GetCallerIdentity(&sts.GetCallerIdentityInput{})
|
|
if err == nil {
|
|
return parseAccountInfoFromArn(*outCallerIdentity.Arn)
|
|
}
|
|
log.Printf("[DEBUG] Getting account ID via sts:GetCallerIdentity failed: %s", err)
|
|
|
|
// Then try IAM ListRoles
|
|
log.Println("[DEBUG] Trying to get account ID via iam:ListRoles")
|
|
outRoles, err := iamconn.ListRoles(&iam.ListRolesInput{
|
|
MaxItems: aws.Int64(int64(1)),
|
|
})
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("Failed getting account ID via 'iam:ListRoles': %s", err)
|
|
}
|
|
|
|
if len(outRoles.Roles) < 1 {
|
|
return "", "", errors.New("Failed getting account ID via 'iam:ListRoles': No roles available")
|
|
}
|
|
|
|
return parseAccountInfoFromArn(*outRoles.Roles[0].Arn)
|
|
}
|
|
|
|
func parseAccountInfoFromArn(arn string) (string, string, error) {
|
|
parts := strings.Split(arn, ":")
|
|
if len(parts) < 5 {
|
|
return "", "", fmt.Errorf("Unable to parse ID from invalid ARN: %q", arn)
|
|
}
|
|
return parts[1], parts[4], nil
|
|
}
|
|
|
|
// This function is responsible for reading credentials from the
|
|
// environment in the case that they're not explicitly specified
|
|
// in the Terraform configuration.
|
|
func GetCredentials(c *Config) (*awsCredentials.Credentials, error) {
|
|
// build a chain provider, lazy-evaulated by aws-sdk
|
|
providers := []awsCredentials.Provider{
|
|
&awsCredentials.StaticProvider{Value: awsCredentials.Value{
|
|
AccessKeyID: c.AccessKey,
|
|
SecretAccessKey: c.SecretKey,
|
|
SessionToken: c.Token,
|
|
}},
|
|
&awsCredentials.EnvProvider{},
|
|
&awsCredentials.SharedCredentialsProvider{
|
|
Filename: c.CredsFilename,
|
|
Profile: c.Profile,
|
|
},
|
|
}
|
|
|
|
// Build isolated HTTP client to avoid issues with globally-shared settings
|
|
client := cleanhttp.DefaultClient()
|
|
|
|
// Keep the timeout low as we don't want to wait in non-EC2 environments
|
|
client.Timeout = 100 * time.Millisecond
|
|
cfg := &aws.Config{
|
|
HTTPClient: client,
|
|
}
|
|
usedEndpoint := setOptionalEndpoint(cfg)
|
|
|
|
if !c.SkipMetadataApiCheck {
|
|
// Real AWS should reply to a simple metadata request.
|
|
// We check it actually does to ensure something else didn't just
|
|
// happen to be listening on the same IP:Port
|
|
metadataClient := ec2metadata.New(session.New(cfg))
|
|
if metadataClient.Available() {
|
|
providers = append(providers, &ec2rolecreds.EC2RoleProvider{
|
|
Client: metadataClient,
|
|
})
|
|
log.Print("[INFO] AWS EC2 instance detected via default metadata" +
|
|
" API endpoint, EC2RoleProvider added to the auth chain")
|
|
} else {
|
|
if usedEndpoint == "" {
|
|
usedEndpoint = "default location"
|
|
}
|
|
log.Printf("[WARN] Ignoring AWS metadata API endpoint at %s "+
|
|
"as it doesn't return any instance-id", usedEndpoint)
|
|
}
|
|
}
|
|
|
|
// This is the "normal" flow (i.e. not assuming a role)
|
|
if c.AssumeRoleARN == "" {
|
|
return awsCredentials.NewChainCredentials(providers), nil
|
|
}
|
|
|
|
// Otherwise we need to construct and STS client with the main credentials, and verify
|
|
// that we can assume the defined role.
|
|
log.Printf("[INFO] Attempting to AssumeRole %s (SessionName: %q, ExternalId: %q, Policy: %q)",
|
|
c.AssumeRoleARN, c.AssumeRoleSessionName, c.AssumeRoleExternalID, c.AssumeRolePolicy)
|
|
|
|
creds := awsCredentials.NewChainCredentials(providers)
|
|
cp, err := creds.Get()
|
|
if err != nil {
|
|
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" {
|
|
return nil, errors.New(`No valid credential sources found for AWS Provider.
|
|
Please see https://terraform.io/docs/providers/aws/index.html for more information on
|
|
providing credentials for the AWS Provider`)
|
|
}
|
|
|
|
return nil, fmt.Errorf("Error loading credentials for AWS Provider: %s", err)
|
|
}
|
|
|
|
log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName)
|
|
|
|
awsConfig := &aws.Config{
|
|
Credentials: creds,
|
|
Region: aws.String(c.Region),
|
|
MaxRetries: aws.Int(c.MaxRetries),
|
|
HTTPClient: cleanhttp.DefaultClient(),
|
|
S3ForcePathStyle: aws.Bool(c.S3ForcePathStyle),
|
|
}
|
|
|
|
stsclient := sts.New(session.New(awsConfig))
|
|
assumeRoleProvider := &stscreds.AssumeRoleProvider{
|
|
Client: stsclient,
|
|
RoleARN: c.AssumeRoleARN,
|
|
}
|
|
if c.AssumeRoleSessionName != "" {
|
|
assumeRoleProvider.RoleSessionName = c.AssumeRoleSessionName
|
|
}
|
|
if c.AssumeRoleExternalID != "" {
|
|
assumeRoleProvider.ExternalID = aws.String(c.AssumeRoleExternalID)
|
|
}
|
|
if c.AssumeRolePolicy != "" {
|
|
assumeRoleProvider.Policy = aws.String(c.AssumeRolePolicy)
|
|
}
|
|
|
|
providers = []awsCredentials.Provider{assumeRoleProvider}
|
|
|
|
assumeRoleCreds := awsCredentials.NewChainCredentials(providers)
|
|
_, err = assumeRoleCreds.Get()
|
|
if err != nil {
|
|
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" {
|
|
return nil, fmt.Errorf("The role %q cannot be assumed.\n\n"+
|
|
" There are a number of possible causes of this - the most common are:\n"+
|
|
" * The credentials used in order to assume the role are invalid\n"+
|
|
" * The credentials do not have appropriate permission to assume the role\n"+
|
|
" * The role ARN is not valid",
|
|
c.AssumeRoleARN)
|
|
}
|
|
|
|
return nil, fmt.Errorf("Error loading credentials for AWS Provider: %s", err)
|
|
}
|
|
|
|
return assumeRoleCreds, nil
|
|
}
|
|
|
|
func setOptionalEndpoint(cfg *aws.Config) string {
|
|
endpoint := os.Getenv("AWS_METADATA_URL")
|
|
if endpoint != "" {
|
|
log.Printf("[INFO] Setting custom metadata endpoint: %q", endpoint)
|
|
cfg.Endpoint = aws.String(endpoint)
|
|
return endpoint
|
|
}
|
|
return ""
|
|
}
|