diff --git a/pkg/arn/arn.go b/pkg/arn/arn.go index 55c936862..a7d8fd85d 100644 --- a/pkg/arn/arn.go +++ b/pkg/arn/arn.go @@ -8,23 +8,35 @@ import ( "github.com/aws/aws-sdk-go/aws/endpoints" ) +type PrincipalType int + +const ( + // Supported principals + NONE PrincipalType = iota + ROLE + USER + ROOT + FEDERATED_USER + ASSUMED_ROLE +) + // Canonicalize validates IAM resources are appropriate for the authenticator // and converts STS assumed roles into the IAM role resource. // // Supported IAM resources are: -// * AWS account: arn:aws:iam::123456789012:root -// * IAM user: arn:aws:iam::123456789012:user/Bob -// * IAM role: arn:aws:iam::123456789012:role/S3Access -// * IAM Assumed role: arn:aws:sts::123456789012:assumed-role/Accounting-Role/Mary (converted to IAM role) -// * Federated user: arn:aws:sts::123456789012:federated-user/Bob -func Canonicalize(arn string) (string, error) { +// - AWS account: arn:aws:iam::123456789012:root +// - IAM user: arn:aws:iam::123456789012:user/Bob +// - IAM role: arn:aws:iam::123456789012:role/S3Access +// - IAM Assumed role: arn:aws:sts::123456789012:assumed-role/Accounting-Role/Mary (converted to IAM role) +// - Federated user: arn:aws:sts::123456789012:federated-user/Bob +func Canonicalize(arn string) (PrincipalType, string, error) { parsed, err := awsarn.Parse(arn) if err != nil { - return "", fmt.Errorf("arn '%s' is invalid: '%v'", arn, err) + return NONE, "", fmt.Errorf("arn '%s' is invalid: '%v'", arn, err) } if err := checkPartition(parsed.Partition); err != nil { - return "", fmt.Errorf("arn '%s' does not have a recognized partition", arn) + return NONE, "", fmt.Errorf("arn '%s' does not have a recognized partition", arn) } parts := strings.Split(parsed.Resource, "/") @@ -34,27 +46,31 @@ func Canonicalize(arn string) (string, error) { case "sts": switch resource { case "federated-user": - return arn, nil + return FEDERATED_USER, arn, nil case "assumed-role": if len(parts) < 3 { - return "", fmt.Errorf("assumed-role arn '%s' does not have a role", arn) + return NONE, "", fmt.Errorf("assumed-role arn '%s' does not have a role", arn) } // IAM ARNs can contain paths, part[0] is resource, parts[len(parts)] is the SessionName. role := strings.Join(parts[1:len(parts)-1], "/") - return fmt.Sprintf("arn:%s:iam::%s:role/%s", parsed.Partition, parsed.AccountID, role), nil + return ASSUMED_ROLE, fmt.Sprintf("arn:%s:iam::%s:role/%s", parsed.Partition, parsed.AccountID, role), nil default: - return "", fmt.Errorf("unrecognized resource %s for service sts", parsed.Resource) + return NONE, "", fmt.Errorf("unrecognized resource %s for service sts", parsed.Resource) } case "iam": switch resource { - case "role", "user", "root": - return arn, nil + case "role": + return ROLE, arn, nil + case "user": + return USER, arn, nil + case "root": + return ROOT, arn, nil default: - return "", fmt.Errorf("unrecognized resource %s for service iam", parsed.Resource) + return NONE, "", fmt.Errorf("unrecognized resource %s for service iam", parsed.Resource) } } - return "", fmt.Errorf("service %s in arn %s is not a valid service for identities", parsed.Service, arn) + return NONE, "", fmt.Errorf("service %s in arn %s is not a valid service for identities", parsed.Service, arn) } func checkPartition(partition string) error { diff --git a/pkg/mapper/crd/controller/controller.go b/pkg/mapper/crd/controller/controller.go index 433d1ebd6..c35103025 100644 --- a/pkg/mapper/crd/controller/controller.go +++ b/pkg/mapper/crd/controller/controller.go @@ -207,7 +207,7 @@ func (c *Controller) syncHandler(key string) (err error) { if iamIdentityMapping.Spec.ARN != "" { iamIdentityMappingCopy := iamIdentityMapping.DeepCopy() - canonicalizedARN, err := arn.Canonicalize(strings.ToLower(iamIdentityMapping.Spec.ARN)) + _, canonicalizedARN, err := arn.Canonicalize(strings.ToLower(iamIdentityMapping.Spec.ARN)) if err != nil { return err } diff --git a/pkg/mapper/dynamicfile/dynamicfile.go b/pkg/mapper/dynamicfile/dynamicfile.go index b6081c9b5..c63dc9261 100644 --- a/pkg/mapper/dynamicfile/dynamicfile.go +++ b/pkg/mapper/dynamicfile/dynamicfile.go @@ -63,14 +63,14 @@ func (ms *DynamicFileMapStore) saveMap( ms.awsAccounts = make(map[string]interface{}) for _, user := range userMappings { - key, _ := arn.Canonicalize(strings.ToLower(user.UserARN)) + _, key, _ := arn.Canonicalize(strings.ToLower(user.UserARN)) if ms.userIDStrict { key = user.UserId } ms.users[key] = user } for _, role := range roleMappings { - key, _ := arn.Canonicalize(strings.ToLower(role.RoleARN)) + _, key, _ := arn.Canonicalize(strings.ToLower(role.RoleARN)) if ms.userIDStrict { key = role.UserId } diff --git a/pkg/mapper/file/mapper.go b/pkg/mapper/file/mapper.go index 5a181ff97..6fb5adb3c 100644 --- a/pkg/mapper/file/mapper.go +++ b/pkg/mapper/file/mapper.go @@ -2,9 +2,10 @@ package file import ( "fmt" - "sigs.k8s.io/aws-iam-authenticator/pkg/token" "strings" + "sigs.k8s.io/aws-iam-authenticator/pkg/token" + "sigs.k8s.io/aws-iam-authenticator/pkg/arn" "sigs.k8s.io/aws-iam-authenticator/pkg/config" "sigs.k8s.io/aws-iam-authenticator/pkg/mapper" @@ -32,7 +33,7 @@ func NewFileMapper(cfg config.Config) (*FileMapper, error) { return nil, err } if m.RoleARN != "" { - canonicalizedARN, err := arn.Canonicalize(m.RoleARN) + _, canonicalizedARN, err := arn.Canonicalize(m.RoleARN) if err != nil { return nil, err } @@ -47,7 +48,7 @@ func NewFileMapper(cfg config.Config) (*FileMapper, error) { } var key string if m.UserARN != "" { - canonicalizedARN, err := arn.Canonicalize(strings.ToLower(m.UserARN)) + _, canonicalizedARN, err := arn.Canonicalize(strings.ToLower(m.UserARN)) if err != nil { return nil, fmt.Errorf("error canonicalizing ARN: %v", err) } diff --git a/pkg/token/token.go b/pkg/token/token.go index 99b286074..ddd88f68e 100644 --- a/pkg/token/token.go +++ b/pkg/token/token.go @@ -600,29 +600,41 @@ func (v tokenVerifier) Verify(token string) (*Identity, error) { return nil, NewSTSError(err.Error()) } - // parse the response into an Identity id := &Identity{ - ARN: callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Arn, - AccountID: callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Account, AccessKeyID: accessKeyID, } - id.CanonicalARN, err = arn.Canonicalize(id.ARN) + return getIdentityFromSTSResponse(id, callerIdentity) +} + +func getIdentityFromSTSResponse(id *Identity, wrapper getCallerIdentityWrapper) (*Identity, error) { + var err error + result := wrapper.GetCallerIdentityResponse.GetCallerIdentityResult + + id.ARN = result.Arn + id.AccountID = result.Account + + var principalType arn.PrincipalType + principalType, id.CanonicalARN, err = arn.Canonicalize(id.ARN) if err != nil { return nil, NewSTSError(err.Error()) } - // The user ID is either UserID:SessionName (for assumed roles) or just - // UserID (for IAM User principals). - userIDParts := strings.Split(callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.UserID, ":") - if len(userIDParts) == 2 { - id.UserID = userIDParts[0] - id.SessionName = userIDParts[1] - } else if len(userIDParts) == 1 { - id.UserID = userIDParts[0] + // The user ID is one of: + // 1. UserID:SessionName (for assumed roles) + // 2. UserID (for IAM User principals). + // 3. AWSAccount:CallerSpecifiedName (for federated users) + if principalType == arn.FEDERATED_USER || principalType == arn.USER || principalType == arn.ROOT { + id.UserID = result.UserID } else { - return nil, STSError{fmt.Sprintf( - "malformed UserID %q", - callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.UserID)} + userIDParts := strings.Split(result.UserID, ":") + if len(userIDParts) == 2 { + id.UserID = userIDParts[0] + id.SessionName = userIDParts[1] + } else { + return nil, STSError{fmt.Sprintf( + "malformed UserID %q", + result.UserID)} + } } return id, nil