lib/aws/warehouse.go (841 lines of code) (raw):
// Copyright 2020 Google LLC.
//
// 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 aws abstracts interacting with certain aspects of AWS,
// such as creating IAM roles and user, account keys, and access tokens.
package aws
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws" /* copybara-comment */
"github.com/aws/aws-sdk-go/aws/awserr" /* copybara-comment */
"github.com/aws/aws-sdk-go/service/iam" /* copybara-comment */
"github.com/aws/aws-sdk-go/service/sts" /* copybara-comment */
"github.com/cenkalti/backoff" /* copybara-comment */
"bitbucket.org/creachadair/stringset" /* copybara-comment */
"github.com/pborman/uuid" /* copybara-comment */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/clouds" /* copybara-comment: clouds */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/timeutil" /* copybara-comment: timeutil */
glog "github.com/golang/glog" /* copybara-comment */
pb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/dam/v1" /* copybara-comment: go_proto */
)
const (
// TemporaryCredMaxTTL is the maximum TTL for an AWS access token.
TemporaryCredMaxTTL = 12 * time.Hour
// S3ItemFormat is the canonical item format identifier for S3 buckets.
S3ItemFormat = "s3bucket"
// RedshiftItemFormat is the canonical item format identifier for Redshift clusters.
RedshiftItemFormat = "redshift"
// RedshiftConsoleItemFormat is the canonical item format identifier for the Redshift console.
RedshiftConsoleItemFormat = "redshift-console"
// HumanInterfacePrefix is the canonical prefix for interface URNs that grant console access to AWS resources.
HumanInterfacePrefix = "web:aws:"
)
type principalType int
const (
userType principalType = iota
roleType
)
type credentialType int
const (
temporaryKey credentialType = iota
permanentKey
usernamePassword
)
type resourceType int
const (
otherRType resourceType = iota
bucketType
clusterUserType
)
const (
backoffInitialInterval = 1 * time.Second
backoffRandomizationFactor = 0.5
backoffMultiplier = 1.5
backoffMaxInterval = 3 * time.Second
backoffMaxElapsedTime = 10 * time.Second
)
var (
exponentialBackoff = &backoff.ExponentialBackOff{
InitialInterval: backoffInitialInterval,
RandomizationFactor: backoffRandomizationFactor,
Multiplier: backoffMultiplier,
MaxInterval: backoffMaxInterval,
MaxElapsedTime: backoffMaxElapsedTime,
Clock: backoff.SystemClock,
}
)
// APIClient is a wrapper around the AWS SDK that can be mocked for unit testing.
type APIClient interface {
ListUsers(input *iam.ListUsersInput) (*iam.ListUsersOutput, error)
ListAccessKeys(input *iam.ListAccessKeysInput) (*iam.ListAccessKeysOutput, error)
DeleteAccessKey(input *iam.DeleteAccessKeyInput) (*iam.DeleteAccessKeyOutput, error)
GetCallerIdentity(input *sts.GetCallerIdentityInput) (*sts.GetCallerIdentityOutput, error)
AssumeRole(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error)
CreateAccessKey(input *iam.CreateAccessKeyInput) (*iam.CreateAccessKeyOutput, error)
PutRolePolicy(input *iam.PutRolePolicyInput) (*iam.PutRolePolicyOutput, error)
ListUserPolicies(input *iam.ListUserPoliciesInput) (*iam.ListUserPoliciesOutput, error)
PutUserPolicy(input *iam.PutUserPolicyInput) (*iam.PutUserPolicyOutput, error)
DeleteUserPolicy(input *iam.DeleteUserPolicyInput) (*iam.DeleteUserPolicyOutput, error)
GetUser(input *iam.GetUserInput) (*iam.GetUserOutput, error)
CreateUser(input *iam.CreateUserInput) (*iam.CreateUserOutput, error)
DeleteUser(input *iam.DeleteUserInput) (*iam.DeleteUserOutput, error)
GetRole(input *iam.GetRoleInput) (*iam.GetRoleOutput, error)
CreateRole(input *iam.CreateRoleInput) (*iam.CreateRoleOutput, error)
CreateLoginProfile(input *iam.CreateLoginProfileInput) (*iam.CreateLoginProfileOutput, error)
UpdateLoginProfile(input *iam.UpdateLoginProfileInput) (*iam.UpdateLoginProfileOutput, error)
GetLoginProfile(input *iam.GetLoginProfileInput) (*iam.GetLoginProfileOutput, error)
DeleteLoginProfile(input *iam.DeleteLoginProfileInput) (*iam.DeleteLoginProfileOutput, error)
}
// ResourceTokenResult is returned from MintTokenWithTTL for aws adapter.
type ResourceTokenResult struct {
Account string
PrincipalARN string
Format string
AccessKeyID *string
SecretAccessKey *string
SessionToken *string
UserName *string
Password *string
}
// AccountWarehouse is used to create AWS IAM Users and temporary credentials
type AccountWarehouse struct {
account string
svcUserARN string
svcUserName string
apiClient APIClient
}
// NewWarehouse creates a new AccountWarehouse using the provided client
// and options.
func NewWarehouse(_ context.Context, awsClient APIClient) (*AccountWarehouse, error) {
wh := &AccountWarehouse{
apiClient: awsClient,
}
gcio, err := awsClient.GetCallerIdentity(&sts.GetCallerIdentityInput{})
if err != nil {
return nil, err
}
wh.svcUserARN = *gcio.Arn
wh.account, err = extractAccount(wh.svcUserARN)
if err != nil {
return nil, err
}
wh.svcUserName, err = extractUserName(wh.svcUserARN)
if err != nil {
return nil, err
}
return wh, nil
}
// GetAwsAccount returns the AWS account used by this AccountWarehouse for creating IAM
// users, roles, and policies.
func (wh *AccountWarehouse) GetAwsAccount() string {
return wh.account
}
// GetServiceAccounts returns IAM users created by this warehouse in the warehouse AWS account.
func (wh *AccountWarehouse) GetServiceAccounts(ctx context.Context, _ string) (<-chan *clouds.Account, error) {
c := make(chan *clouds.Account)
go func() {
defer close(c)
f := func(acct *iam.User) error {
a := &clouds.Account{
ID: aws.StringValue(acct.UserName),
DisplayName: aws.StringValue(acct.UserName),
}
select {
case c <- a:
case <-ctx.Done():
return ctx.Err()
}
return nil
}
// TODO: get PathPrefix from config
accounts, err := wh.apiClient.ListUsers(&iam.ListUsersInput{
PathPrefix: aws.String("/ddap/"),
})
if err != nil {
glog.Errorf("getting users list: %v", err)
return
}
users := accounts.Users
for _, user := range users {
if err := f(user); err != nil {
glog.Errorf("getting user accounts list: %v", err)
return
}
}
}()
return c, nil
}
// RemoveServiceAccount removes an AWS IAM user (project parameter is ignored).
func (wh *AccountWarehouse) RemoveServiceAccount(_ context.Context, _, userName string) error {
err := wh.deleteLoginProfile(userName)
if err != nil {
return err
}
err = wh.deleteAccessKeys(userName)
if err != nil {
return err
}
err = wh.deleteInlineUserPolicies(userName)
if err != nil {
return err
}
// delete user
_, err = wh.apiClient.DeleteUser(&iam.DeleteUserInput{UserName: aws.String(userName)})
if err != nil {
return fmt.Errorf("delete operation on AWS user %s failed: %v", userName, err)
}
return nil
}
func (wh *AccountWarehouse) deleteInlineUserPolicies(userName string) error {
// gather policies
var policyNames []*string
var marker *string
for {
userPolicyOutput, err := wh.apiClient.ListUserPolicies(&iam.ListUserPoliciesInput{UserName: aws.String(userName), Marker: marker})
if err != nil {
return fmt.Errorf("unable to list policies for AWS user %s: %v", userName, err)
}
for _, policyName := range userPolicyOutput.PolicyNames {
policyNames = append(policyNames, policyName)
}
if *userPolicyOutput.IsTruncated {
marker = userPolicyOutput.Marker
continue
}
break
}
for _, policyName := range policyNames {
_, err := wh.apiClient.DeleteUserPolicy(&iam.DeleteUserPolicyInput{
PolicyName: policyName,
UserName: aws.String(userName),
})
if err != nil {
return fmt.Errorf("unable to delete AWS policy %s for AWS user %s: %v", *policyName, userName, err)
}
}
return nil
}
func (wh *AccountWarehouse) deleteAccessKeys(userName string) error {
// gather all keys before deleting
var keys []*iam.AccessKeyMetadata
var marker *string
for {
listKeysOutput, err := wh.apiClient.ListAccessKeys(&iam.ListAccessKeysInput{UserName: aws.String(userName), Marker: marker})
if err != nil {
return fmt.Errorf("unable to list keys for user %s: %v", userName, err)
}
for _, keyData := range listKeysOutput.AccessKeyMetadata {
keys = append(keys, keyData)
}
if *listKeysOutput.IsTruncated {
marker = listKeysOutput.Marker
continue
}
break
}
for _, keyData := range keys {
_, err := wh.apiClient.DeleteAccessKey(&iam.DeleteAccessKeyInput{
AccessKeyId: keyData.AccessKeyId,
UserName: keyData.UserName,
})
if err != nil {
return fmt.Errorf("unable to delete access key %s for AWS user %s: %v", *keyData.AccessKeyId, *keyData.UserName, err)
}
}
return nil
}
func (wh *AccountWarehouse) deleteLoginProfile(userName string) error {
_, err := wh.apiClient.GetLoginProfile(&iam.GetLoginProfileInput{UserName: aws.String(userName)})
if err == nil {
_, err = wh.apiClient.DeleteLoginProfile(&iam.DeleteLoginProfileInput{UserName: aws.String(userName)})
if err != nil {
return fmt.Errorf("unable to delete AWS user %s login profile: %v", userName, err)
}
} else if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != iam.ErrCodeNoSuchEntityException {
return fmt.Errorf("error looking up login profile while attempting to delete AWS user %s: %v", userName, aerr)
}
return nil
}
// ManageAccountKeys is the main method where key removal happens
func (wh *AccountWarehouse) ManageAccountKeys(_ context.Context, _, accountID string, _, maxKeyTTL time.Duration, now time.Time, keysPerAccount int64) (int, int, error) {
// A key has expired if key.CreatedDate + maxTTL < now, i.e. key.ValidAfterTime < now - maxTTL
expired := now.Add(-1 * maxKeyTTL).Format(time.RFC3339)
accessKeys, err := wh.apiClient.ListAccessKeys(&iam.ListAccessKeysInput{
UserName: aws.String(accountID),
})
if err != nil {
return 0, 0, fmt.Errorf("error getting aws key list: %v", err)
}
keys := accessKeys.AccessKeyMetadata
var actives []*iam.AccessKeyMetadata
active := len(keys)
for _, key := range keys {
t := aws.TimeValue(key.CreateDate).Format(time.RFC3339)
if t < expired {
// Access key deletion
_, err := wh.apiClient.DeleteAccessKey(&iam.DeleteAccessKeyInput{
AccessKeyId: key.AccessKeyId,
UserName: aws.String(accountID),
})
if err != nil {
return active, len(keys) - active, fmt.Errorf("error deleting aws access key: %v", err)
}
active--
continue
}
actives = append(actives, key)
}
if int64(len(actives)) < keysPerAccount {
return active, len(keys) - active, nil
}
// Remove earliest expiring keys
sort.Slice(actives, func(i, j int) bool {
return aws.TimeValue(actives[i].CreateDate).After(aws.TimeValue(actives[j].CreateDate))
})
for _, key := range actives[keysPerAccount:] {
_, err := wh.apiClient.DeleteAccessKey(&iam.DeleteAccessKeyInput{
AccessKeyId: key.AccessKeyId,
UserName: aws.String(accountID),
})
if err != nil {
return active, len(keys) - active, fmt.Errorf("deleting key: %v", err)
}
active--
}
return active, len(keys) - active, nil
}
// ResourceParams contains all the arguments necessary to call MintTokenWithTTL on an
// AWS AccountWarehouse.
type ResourceParams struct {
UserID string
TTL time.Duration
MaxKeyTTL time.Duration
ManagedKeysPerAccount int
Vars map[string]string
TargetRoles []string
TargetScopes []string
DamResourceID string
DamViewID string
DamRoleID string
DamInterfaceID string
ServiceTemplate *pb.ServiceTemplate
}
type resourceSpec struct {
rType resourceType
arn string
}
type principalSpec struct {
pType principalType
// Used for roles that must be assumed
damPrincipalARN string
damPrincipalUserName string
// path must start and end with slash
path string
account string
params *ResourceParams
}
type credentialSpec struct {
cType credentialType
principalSpec *principalSpec
params *ResourceParams
}
type policySpec struct {
credSpec *credentialSpec
rSpecs []*resourceSpec
params *ResourceParams
}
func (spec *policySpec) getID() string {
return spec.credSpec.principalSpec.getDamResourceViewRoleID()
}
func (spec *policySpec) sessionScoped() bool {
if spec.credSpec.principalSpec.pType == roleType {
for _, rSpec := range spec.rSpecs {
if rSpec.rType == clusterUserType {
return true
}
}
}
return false
}
func (spec *principalSpec) getID() string {
if spec.pType == roleType {
return spec.getDamResourceViewRoleID()
}
return convertDamUserIDtoAwsName(spec.params.UserID, spec.damPrincipalUserName)
}
func (spec *principalSpec) getDamResourceViewRoleID() string {
return fmt.Sprintf("%s,%s,%s@%s", spec.params.DamResourceID, spec.params.DamViewID, spec.params.DamRoleID, spec.damPrincipalUserName)
}
func (spec *principalSpec) getARN() string {
if spec.pType == roleType {
return fmt.Sprintf("arn:aws:iam::%s:role%s%s", spec.account, spec.path, spec.getID())
}
return fmt.Sprintf("arn:aws:iam::%s:user%s%s", spec.account, spec.path, spec.getID())
}
func calculateDBuserARN(clusterARN string, userName string) (string, error) {
parts := strings.Split(clusterARN, ":")
if len(parts) < 7 {
return "", fmt.Errorf("argument is not a proper cluster ARN: %s", clusterARN)
}
return fmt.Sprintf("%s:%s:%s:%s:%s:dbuser:%s/%s", parts[0], parts[1], parts[2], parts[3], parts[4], parts[6], userName), nil
}
func extractAccount(arn string) (string, error) {
parts := strings.Split(arn, ":")
if len(parts) < 5 {
return "", fmt.Errorf("argument is not a proper ARN: %s", arn)
}
return parts[4], nil
}
// MintTokenWithTTL returns an AccountKey or an AccessToken depending on the TTL requested.
func (wh *AccountWarehouse) MintTokenWithTTL(ctx context.Context, params *ResourceParams) (*ResourceTokenResult, error) {
if params.TTL > params.MaxKeyTTL {
return nil, fmt.Errorf("given ttl [%s] is greater than max ttl [%s]", params.TTL, params.MaxKeyTTL)
}
credSpec := wh.determineCredentialSpec(params)
rSpecs, err := wh.determineResourceSpecs(params)
if err != nil {
return nil, err
}
polSpec := &policySpec{
credSpec: credSpec,
rSpecs: rSpecs,
params: params,
}
principalARN, err := wh.ensurePrincipal(credSpec.principalSpec)
if err != nil {
return nil, err
}
return wh.ensureTokenResult(ctx, principalARN, polSpec)
}
func (wh *AccountWarehouse) determineResourceSpecs(params *ResourceParams) ([]*resourceSpec, error) {
switch params.ServiceTemplate.ServiceName {
case S3ItemFormat:
bucket, ok := params.Vars["bucket"]
if !ok {
return nil, fmt.Errorf("no bucket specified")
}
paths, ok := params.Vars["paths"]
if !ok || paths == "" || paths == "*" || paths == "/*" {
return []*resourceSpec{
{
arn: fmt.Sprintf("arn:aws:s3:::%s/*", bucket),
rType: bucketType,
},
{
arn: fmt.Sprintf("arn:aws:s3:::%s", bucket),
rType: bucketType,
},
}, nil
}
uniquePaths := stringset.New(strings.Split(paths, ";")...)
var resourceSpecs []*resourceSpec
for _, v := range uniquePaths.Elements() {
resourceSpecs = append(resourceSpecs, &resourceSpec{
arn: fmt.Sprintf("arn:aws:s3:::%s%s", bucket, v),
rType: bucketType,
})
}
return resourceSpecs, nil
case RedshiftItemFormat:
clusterARN, ok := params.Vars["cluster"]
if !ok {
return nil, fmt.Errorf("no cluster specified")
}
clusterSpec := &resourceSpec{
rType: otherRType,
arn: clusterARN,
}
dbUser := convertDamUserIDtoAwsName(params.UserID, wh.svcUserName)
dbUserARN, err := calculateDBuserARN(clusterARN, dbUser)
if err != nil {
return nil, err
}
userSpec := &resourceSpec{
rType: clusterUserType,
arn: dbUserARN,
}
group, ok := params.Vars["group"]
if ok {
return []*resourceSpec{
clusterSpec,
userSpec,
{
rType: otherRType,
arn: group,
},
}, nil
}
return []*resourceSpec{clusterSpec, userSpec}, nil
case RedshiftConsoleItemFormat:
packedResources, ok := params.Vars["resources"]
var resources []string
if ok {
resources = strings.Split(packedResources, ";")
} else {
resources = []string{"*"}
}
var specs []*resourceSpec
for _, res := range resources {
specs = append(specs, &resourceSpec{
rType: otherRType,
arn: res,
})
}
return specs, nil
default:
return nil, fmt.Errorf("unrecognized item format [%s] for AWS target adapter", params.ServiceTemplate.ServiceName)
}
}
func (wh *AccountWarehouse) determineCredentialSpec(params *ResourceParams) *credentialSpec {
credentialSpec := &credentialSpec{params: params}
if strings.HasPrefix(params.DamInterfaceID, HumanInterfacePrefix) {
credentialSpec.cType = usernamePassword
} else if params.TTL > TemporaryCredMaxTTL {
credentialSpec.cType = permanentKey
} else {
credentialSpec.cType = temporaryKey
}
credentialSpec.principalSpec = wh.determinePrincipalSpec(credentialSpec)
return credentialSpec
}
func (wh *AccountWarehouse) determinePrincipalSpec(credSpec *credentialSpec) *principalSpec {
params := credSpec.params
princSpec := &principalSpec{
damPrincipalARN: wh.svcUserARN,
damPrincipalUserName: wh.svcUserName,
account: wh.account,
params: params,
// TODO: Make prefix configurable for different dam deployments
path: "/ddap/",
}
if credSpec.cType == temporaryKey {
princSpec.pType = roleType
} else {
princSpec.pType = userType
}
return princSpec
}
func (wh *AccountWarehouse) ensureTokenResult(ctx context.Context, principalARN string, polSpec *policySpec) (*ResourceTokenResult, error) {
err := wh.ensureIdentityPolicy(polSpec)
if err != nil {
return nil, err
}
switch polSpec.credSpec.cType {
case permanentKey:
return wh.ensureAccessKeyResult(ctx, principalARN, polSpec.credSpec.principalSpec)
case temporaryKey:
return wh.createTempCredentialResult(polSpec)
case usernamePassword:
return wh.createUsernamePasswordResult(principalARN)
default:
return nil, fmt.Errorf("cannot generate token for invalid spec with [%v] credential type", polSpec.credSpec.cType)
}
}
func (wh *AccountWarehouse) createTempCredentialResult(polSpec *policySpec) (*ResourceTokenResult, error) {
aro, err := wh.assumeRole(polSpec)
if err != nil {
return nil, err
}
return &ResourceTokenResult{
Account: wh.account,
PrincipalARN: *aro.AssumedRoleUser.Arn,
AccessKeyID: aro.Credentials.AccessKeyId,
SecretAccessKey: aro.Credentials.SecretAccessKey,
SessionToken: aro.Credentials.SessionToken,
Format: "aws/session",
}, nil
}
func (wh *AccountWarehouse) ensureAccessKeyResult(ctx context.Context, principalARN string, princSpec *principalSpec) (*ResourceTokenResult, error) {
accessKey, err := wh.ensureAccessKey(ctx, princSpec, wh.svcUserARN)
if err != nil {
return nil, err
}
return &ResourceTokenResult{
Account: wh.account,
PrincipalARN: principalARN,
AccessKeyID: accessKey.AccessKeyId,
SecretAccessKey: accessKey.SecretAccessKey,
Format: "aws/key",
}, nil
}
func (wh *AccountWarehouse) createUsernamePasswordResult(principalARN string) (*ResourceTokenResult, error) {
userName, err := extractUserName(principalARN)
if err != nil {
return nil, fmt.Errorf("generated principal ARN is invalid: %v", err)
}
password, err := wh.ensureLoginProfile(userName)
if err != nil {
return nil, err
}
return &ResourceTokenResult{
Account: wh.account,
PrincipalARN: principalARN,
UserName: &userName,
Password: &password,
Format: "aws",
}, nil
}
func (wh *AccountWarehouse) ensurePrincipal(princSpec *principalSpec) (string, error) {
if princSpec.pType == roleType {
return wh.ensureRole(princSpec)
}
return wh.ensureUser(princSpec)
}
func (wh *AccountWarehouse) ensureIdentityPolicy(spec *policySpec) error {
if len(spec.rSpecs) == 0 {
return fmt.Errorf("cannot have policy without any resources")
}
switch spec.credSpec.principalSpec.pType {
case userType:
return wh.ensureUserPolicy(spec)
case roleType:
return wh.ensureRolePolicy(spec)
default:
return fmt.Errorf("cannot generate policy for invalid spec with [%v] principal type", spec.credSpec.principalSpec.pType)
}
}
func convertDamUserIDtoAwsName(endUserID, damSvcUserName string) string {
parts := strings.SplitN(endUserID, "|", 2)
sessionName := parts[0] + "@" + damSvcUserName
maxLen := 64
if len(sessionName) < 64 {
maxLen = len(sessionName)
}
return sessionName[0:maxLen]
}
func (wh *AccountWarehouse) assumeRole(polSpec *policySpec) (*sts.AssumeRoleOutput, error) {
params := polSpec.params
roleARN := polSpec.credSpec.principalSpec.getARN()
sessionName := convertDamUserIDtoAwsName(params.UserID, wh.svcUserName)
var sessPolicy *string = nil
/*
Session scope policy restricts permissions granted by identity policy on roles.
Read more here: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#policies_session
We need this for resources like redshift dbuser where we want to use a role, but each session only should get
access for a particular db user
*/
if polSpec.sessionScoped() {
policyJSON, err := convertToPolicyJSON(polSpec)
if err != nil {
return nil, err
}
sessPolicy = aws.String(string(policyJSON))
}
var aro *sts.AssumeRoleOutput
f := func() error {
var err error
aro, err = wh.apiClient.AssumeRole(&sts.AssumeRoleInput{
RoleArn: aws.String(roleARN),
RoleSessionName: aws.String(sessionName),
DurationSeconds: toSeconds(params.TTL),
Policy: sessPolicy,
})
return err
}
err := backoff.Retry(f, exponentialBackoff)
if err != nil {
return nil, fmt.Errorf("unable to assume role %s: %v", roleARN, err)
}
return aro, nil
}
func (wh *AccountWarehouse) ensureLoginProfile(userName string) (string, error) {
password := uuid.New()
var call func() error
_, err := wh.apiClient.GetLoginProfile(&iam.GetLoginProfileInput{UserName: aws.String(userName)})
if err != nil {
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == iam.ErrCodeNoSuchEntityException {
call = func() error {
_, err := wh.apiClient.CreateLoginProfile(&iam.CreateLoginProfileInput{
UserName: aws.String(userName),
Password: aws.String(password),
PasswordResetRequired: aws.Bool(false),
})
return err
}
} else {
return "", err
}
} else {
call = func() error {
_, err := wh.apiClient.UpdateLoginProfile(&iam.UpdateLoginProfileInput{
UserName: aws.String(userName),
Password: aws.String(password),
PasswordResetRequired: aws.Bool(false),
})
return err
}
}
err = backoff.Retry(call, exponentialBackoff)
if err != nil {
return "", fmt.Errorf("unable to create login profile for user %s: %v", userName, err)
}
return password, nil
}
func (wh *AccountWarehouse) ensureAccessKey(ctx context.Context, princSpec *principalSpec, svcUserARN string) (*iam.AccessKey, error) {
// garbage collection call
keysPerAccount := princSpec.params.ManagedKeysPerAccount
if keysPerAccount < 1 {
return nil, fmt.Errorf("cannot create access key: maximum number keys per account is %d", keysPerAccount)
}
makeRoom := keysPerAccount - 1
keyTTL := timeutil.KeyTTL(princSpec.params.MaxKeyTTL, keysPerAccount)
userID := princSpec.getID()
if _, _, err := wh.ManageAccountKeys(ctx, svcUserARN, userID, princSpec.params.TTL, keyTTL, time.Now(), int64(makeRoom)); err != nil {
return nil, fmt.Errorf("garbage collecting keys: %v", err)
}
kres, err := wh.apiClient.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(userID)})
if err != nil {
return nil, fmt.Errorf("unable to create access key for user %s: %v", userID, err)
}
return kres.AccessKey, nil
}
type policy struct {
Version string `json:"Version"`
Statement statement `json:"Statement"`
}
type statement struct {
Effect string `json:"Effect"`
Principal map[string]interface{} `json:"Principal,omitempty"`
Action []string `json:"Action"`
Resource []string `json:"Resource,omitempty"`
Condition map[string]interface{} `json:"Condition,omitempty"`
}
func (wh *AccountWarehouse) ensureRolePolicy(spec *policySpec) error {
spec, err := widenUserScopedResources(*spec)
if err != nil {
return fmt.Errorf("unable to generate role policy: %v", err)
}
policyJSON, err := convertToPolicyJSON(spec)
if err != nil {
return fmt.Errorf("error creating AWS policy JSON: %v", err)
}
f := func() error { return wh.putRolePolicy(spec, string(policyJSON)) }
return backoff.Retry(f, exponentialBackoff)
}
func widenUserScopedResources(spec policySpec) (*policySpec, error) {
rSpecs := make([]*resourceSpec, len(spec.rSpecs))
for i, rSpec := range spec.rSpecs {
if rSpec.rType == clusterUserType {
orig := spec.rSpecs[i]
widened := *orig
arnParts := strings.SplitN(orig.arn, ":", 7)
if len(arnParts) != 7 {
return nil, fmt.Errorf("given arn is not a cluster DB user: %s", orig.arn)
}
dbUserParts := strings.SplitN(arnParts[6], "/", 2)
if len(dbUserParts) != 2 {
return nil, fmt.Errorf("given arn is not a cluster DB user: %s", orig.arn)
}
widened.arn = fmt.Sprintf(
"%s:%s:%s:%s:%s:%s:%s/*",
arnParts[0],
arnParts[1],
arnParts[2],
arnParts[3],
arnParts[4],
arnParts[5],
dbUserParts[0],
)
rSpecs[i] = &widened
} else {
rSpecs[i] = spec.rSpecs[i]
}
}
spec.rSpecs = rSpecs
return &spec, nil
}
func convertToPolicyJSON(spec *policySpec) ([]byte, error) {
resourceARNs := resourceARNToArray(spec.rSpecs)
policy := &policy{
Version: "2012-10-17",
Statement: statement{
Effect: "Allow",
Action: spec.params.TargetRoles,
Resource: resourceARNs,
},
}
return json.Marshal(policy)
}
func (wh *AccountWarehouse) putRolePolicy(spec *policySpec, policy string) error {
_, err := wh.apiClient.PutRolePolicy(&iam.PutRolePolicyInput{
PolicyName: aws.String(spec.getID()),
RoleName: aws.String(spec.credSpec.principalSpec.getID()),
PolicyDocument: aws.String(policy),
})
if err != nil {
return fmt.Errorf("unable to create AWS role policy %s: %v", spec.credSpec.principalSpec.getID(), err)
}
return nil
}
func (wh *AccountWarehouse) ensureUserPolicy(spec *policySpec) error {
resources := resourceARNToArray(spec.rSpecs)
policy := &policy{
Version: "2012-10-17",
Statement: statement{
Effect: "Allow",
Action: spec.params.TargetRoles,
Resource: resources,
Condition: map[string]interface{}{
"DateLessThanEquals": map[string]string{
"aws:CurrentTime": (time.Now().Add(spec.params.TTL)).Format(time.RFC3339),
},
},
},
}
policyJSON, err := json.Marshal(policy)
if err != nil {
return fmt.Errorf("error creating AWS policy JSON: %v", err)
}
f := func() error { return wh.putUserPolicy(spec, string(policyJSON)) }
return backoff.Retry(f, exponentialBackoff)
}
func (wh *AccountWarehouse) putUserPolicy(spec *policySpec, policy string) error {
_, err := wh.apiClient.PutUserPolicy(&iam.PutUserPolicyInput{
PolicyName: aws.String(spec.getID()),
UserName: aws.String(spec.credSpec.principalSpec.getID()),
PolicyDocument: aws.String(policy),
})
if err != nil {
return fmt.Errorf("unable to create AWS user policy %s: %v", spec.credSpec.principalSpec.getID(), err)
}
return nil
}
// ensures user is created and returns non-empty user ARN if successful
func (wh *AccountWarehouse) ensureUser(spec *principalSpec) (string, error) {
guo, err := wh.apiClient.GetUser(&iam.GetUserInput{UserName: aws.String(spec.getID())})
if err != nil {
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == iam.ErrCodeNoSuchEntityException {
cuo, err := wh.apiClient.CreateUser(&iam.CreateUserInput{
UserName: aws.String(spec.getID()),
Path: aws.String(spec.path),
})
if err != nil {
return "", fmt.Errorf("unable to create IAM user %s: %v", spec.getID(), err)
}
return *cuo.User.Arn, nil
}
return "", fmt.Errorf("unable to send AWS IAM request for user %s: %v", spec.getID(), err)
}
return *guo.User.Arn, nil
}
func (wh *AccountWarehouse) ensureRole(spec *principalSpec) (string, error) {
gro, err := wh.apiClient.GetRole(&iam.GetRoleInput{RoleName: aws.String(spec.getID())})
if err != nil {
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == iam.ErrCodeNoSuchEntityException {
policy := &policy{
Version: "2012-10-17",
Statement: statement{
Effect: "Allow",
Principal: map[string]interface{}{
"AWS": spec.damPrincipalARN,
},
Action: []string{"sts:AssumeRole"},
},
}
policyJSON, err := json.Marshal(policy)
if err != nil {
return "", fmt.Errorf("error creating AWS policy JSON: %v", err)
}
cro, err := wh.apiClient.CreateRole(&iam.CreateRoleInput{
AssumeRolePolicyDocument: aws.String(string(policyJSON)),
RoleName: aws.String(spec.getID()),
Path: aws.String(spec.path),
MaxSessionDuration: toSeconds(TemporaryCredMaxTTL),
Tags: []*iam.Tag{
{Key: aws.String("DamResource"), Value: aws.String(spec.params.DamResourceID)},
{Key: aws.String("DamView"), Value: aws.String(spec.params.DamViewID)},
{Key: aws.String("DamRole"), Value: aws.String(spec.params.DamRoleID)},
},
})
if err != nil {
return "", fmt.Errorf("unable to create AWS role %s: %v", spec.getID(), err)
}
return *cro.Role.Arn, nil
}
return "", fmt.Errorf("unable to retrieve AWS role %s: %v", spec.getID(), err)
}
return *gro.Role.Arn, nil
}
func extractUserName(userARN string) (string, error) {
arnParts := strings.Split(userARN, ":")
if len(arnParts) < 6 {
return "", fmt.Errorf("argument is not a proper user ARN: %s", userARN)
}
pathParts := strings.Split(arnParts[5], "/")
return pathParts[len(pathParts)-1], nil
}
func toSeconds(duration time.Duration) *int64 {
seconds := duration.Nanoseconds() / time.Second.Nanoseconds()
return &seconds
}
func resourceARNToArray(rSpecs []*resourceSpec) []string {
arns := make([]string, len(rSpecs))
for i, rSpec := range rSpecs {
arns[i] = rSpec.arn
}
return arns
}