Skip to content

Commit

Permalink
Fix federated user ID parsing
Browse files Browse the repository at this point in the history
* refactor the parsing logic to make it easier
  • Loading branch information
nckturner committed Nov 17, 2023
1 parent d5453b9 commit 22022ae
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 37 deletions.
48 changes: 32 additions & 16 deletions pkg/arn/arn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, "/")
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/mapper/crd/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/mapper/dynamicfile/dynamicfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
7 changes: 4 additions & 3 deletions pkg/mapper/file/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
}
Expand Down
42 changes: 27 additions & 15 deletions pkg/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 22022ae

Please sign in to comment.