Skip to content

Commit

Permalink
deps: Migrate from github.com/kubernetes-sigs/aws-iam-authenticator/p…
Browse files Browse the repository at this point in the history
…kg/token to internal implementation

Reference: #11697
Reference: #8453
Reference: #7438
Reference: #4904

Including the Kubernetes ecosystem dependency rather than hard copying the implementation was originally for a few concerns as noted in #4904 (comment). Since its introduction, the upstream implementation has remained stable with respects to the GetWithSTS token generator implementation we use.

However, changes to the surrounding upstream package code and its broad transitive dependencies have prevented a clear upgrade path since github.com/kubernetes-sigs/aws-iam-authenticator@v0.4.0 (now re-verified with v0.5.0), where Terraform AWS Provider builds cannot succeed on solaris/amd64:

```console
$ gox -os='linux darwin windows freebsd openbsd solaris' -arch='386 amd64 arm' -osarch='!darwin/arm !darwin/386' -ldflags '-s -w -X aws/version.ProviderVersion=99.99.99 -X aws/version.ProtocolVersion=4' -output 'results/{{.OS}}_{{.Arch}}/terraform-provider-aws_v99.99.99_x4' .
...
1 errors occurred:
--> solaris/amd64 error: exit status 2
Stderr: # github.com/gofrs/flock
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:28:22: undefined: syscall.LOCK_EX
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:39:22: undefined: syscall.LOCK_SH
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:56:12: undefined: syscall.Flock
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:66:12: undefined: syscall.Flock
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:96:12: undefined: syscall.Flock
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:96:42: undefined: syscall.LOCK_UN
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:118:21: undefined: syscall.LOCK_EX
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:130:21: undefined: syscall.LOCK_SH
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:149:9: undefined: syscall.Flock
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:149:44: undefined: syscall.LOCK_NB
../../../../go/pkg/mod/github.com/gofrs/flock@v0.7.0/flock_unix.go:149:44: too many errors
```

This issue is non-obvious to contributors and maintainers as we do not perform cross-compilation build testing in CI during pull requests since it is very time prohibitive.

Rather than leave this single data source's dependency in an unstable state, instead we opt to hard copy the relevant upstream Go package and prune that package to only the code we use, removing many unnecessary dependencies.

Updated via:

```console
$ go mod tidy
$ go mod vendor
```

Output from acceptance testing:

```
--- PASS: TestAccAWSEksClusterAuthDataSource_basic (15.00s)
```
  • Loading branch information
bflad committed Jan 31, 2020
1 parent 9ad6c87 commit 487ecf0
Show file tree
Hide file tree
Showing 146 changed files with 373 additions and 33,652 deletions.
4 changes: 2 additions & 2 deletions aws/data_source_aws_eks_cluster_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"github.com/kubernetes-sigs/aws-iam-authenticator/pkg/token"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks/token"
)

func dataSourceAwsEksClusterAuth() *schema.Resource {
Expand All @@ -32,7 +32,7 @@ func dataSourceAwsEksClusterAuth() *schema.Resource {
func dataSourceAwsEksClusterAuthRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).stsconn
name := d.Get("name").(string)
generator, err := token.NewGenerator(false)
generator, err := token.NewGenerator(false, false)
if err != nil {
return fmt.Errorf("error getting token generator: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion aws/data_source_aws_eks_cluster_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/kubernetes-sigs/aws-iam-authenticator/pkg/token"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks/token"
)

func TestAccAWSEksClusterAuthDataSource_basic(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
package arn
/*
This file is a hard copy of:
https://github.com/kubernetes-sigs/aws-iam-authenticator/blob/7547c74e660f8d34d9980f2c69aa008eed1f48d0/pkg/arn/arn.go
With the following modifications:
- Rename package from arn to token for simplication
*/

package token

import (
"fmt"
Expand Down
41 changes: 41 additions & 0 deletions aws/internal/service/eks/token/arn_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
This file is a hard copy of:
https://github.com/kubernetes-sigs/aws-iam-authenticator/blob/7547c74e660f8d34d9980f2c69aa008eed1f48d0/pkg/arn/arn_test.go
With the following modifications:
- Rename package from arn to token for simplication
*/

package token

import (
"fmt"
"testing"
)

var arnTests = []struct {
arn string // input arn
expected string // canonacalized arn
err error // expected error value
}{
{"NOT AN ARN", "", fmt.Errorf("Not an arn")},
{"arn:aws:iam::123456789012:user/Alice", "arn:aws:iam::123456789012:user/Alice", nil},
{"arn:aws:iam::123456789012:role/Users", "arn:aws:iam::123456789012:role/Users", nil},
{"arn:aws:sts::123456789012:assumed-role/Admin/Session", "arn:aws:iam::123456789012:role/Admin", nil},
{"arn:aws:sts::123456789012:federated-user/Bob", "arn:aws:sts::123456789012:federated-user/Bob", nil},
{"arn:aws:iam::123456789012:root", "arn:aws:iam::123456789012:root", nil},
{"arn:aws:sts::123456789012:assumed-role/Org/Team/Admin/Session", "arn:aws:iam::123456789012:role/Org/Team/Admin", nil},
}

func TestUserARN(t *testing.T) {
for _, tc := range arnTests {
actual, err := Canonicalize(tc.arn)
if err != nil && tc.err == nil || err == nil && tc.err != nil {
t.Errorf("Canoncialize(%s) expected err: %v, actual err: %v", tc.arn, tc.err, err)
continue
}
if actual != tc.expected {
t.Errorf("Canonicalize(%s) expected: %s, actual: %s", tc.arn, tc.expected, actual)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
/*
Copyright 2017 by the contributors.
This file is a hard copy of:
https://github.com/kubernetes-sigs/aws-iam-authenticator/blob/7547c74e660f8d34d9980f2c69aa008eed1f48d0/pkg/token/token.go
With the following modifications:
- Removal of all Generator interface methods and implementations except GetWithSTS
- Removal of other unused code
- Use *sts.STS instead of stsiface.STSAPI in Generator interface and GetWithSTS implementation
- Hard copy and use local Canonicalize implementation instead of "sigs.k8s.io/aws-iam-authenticator/pkg/arn"
- Fix two staticcheck errors
*/

/*
Copyright 2017-2020 by the contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -23,18 +36,12 @@ import (
"io/ioutil"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/kubernetes-sigs/aws-iam-authenticator/pkg/arn"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1alpha1 "k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1"
)

// Identity is returned on successful Verify() results. It contains a parsed
Expand Down Expand Up @@ -62,6 +69,11 @@ type Identity struct {
// users or other roles are allowed to assume the role, they can provide
// (nearly) arbitrary strings here.
SessionName string

// The AWS Access Key ID used to authenticate the request. This can be used
// in conjuction with CloudTrail to determine the identity of the individual
// if the individual assumed an IAM role before making the request.
AccessKeyID string
}

const (
Expand All @@ -78,6 +90,7 @@ const (
// Format of the X-Amz-Date header used for expiration
// https://golang.org/pkg/time/#pkg-constants
dateHeaderFormat = "20060102T150405Z"
hostRegexp = `^sts(\.[a-z1-9\-]+)?\.amazonaws\.com(\.cn)?$`
)

// Token is generated and used by Kubernetes client-go to authenticate with a Kubernetes cluster.
Expand Down Expand Up @@ -138,97 +151,25 @@ type getCallerIdentityWrapper struct {
} `json:"GetCallerIdentityResponse"`
}

// Generator provides new tokens for the heptio authenticator.
// Generator provides new tokens for the AWS IAM Authenticator.
type Generator interface {
// Get a token using credentials in the default credentials chain.
Get(string) (Token, error)
// GetWithRole creates a token by assuming the provided role, using the credentials in the default chain.
GetWithRole(clusterID, roleARN string) (Token, error)
// GetWithRoleForSession creates a token by assuming the provided role, using the provided session.
GetWithRoleForSession(clusterID string, roleARN string, sess *session.Session) (Token, error)
// GetWithSTS returns a token valid for clusterID using the given STS client.
GetWithSTS(clusterID string, stsAPI *sts.STS) (Token, error)
// FormatJSON returns the client auth formatted json for the ExecCredential auth
FormatJSON(Token) string
}

type generator struct {
forwardSessionName bool
cache bool
}

// NewGenerator creates a Generator and returns it.
func NewGenerator(forwardSessionName bool) (Generator, error) {
func NewGenerator(forwardSessionName bool, cache bool) (Generator, error) {
return generator{
forwardSessionName: forwardSessionName,
cache: cache,
}, nil
}

// Get uses the directly available AWS credentials to return a token valid for
// clusterID. It follows the default AWS credential handling behavior.
func (g generator) Get(clusterID string) (Token, error) {
return g.GetWithRole(clusterID, "")
}

func StdinStderrTokenProvider() (string, error) {
var v string
fmt.Fprint(os.Stderr, "Assume Role MFA token code: ")
_, err := fmt.Scanln(&v)
return v, err
}

// GetWithRole assumes the given AWS IAM role and returns a token valid for
// clusterID. If roleARN is empty, behaves like Get (does not assume a role).
func (g generator) GetWithRole(clusterID string, roleARN string) (Token, error) {
// create a session with the "base" credentials available
// (from environment variable, profile files, EC2 metadata, etc)
sess, err := session.NewSessionWithOptions(session.Options{
AssumeRoleTokenProvider: StdinStderrTokenProvider,
SharedConfigState: session.SharedConfigEnable,
})
if err != nil {
return Token{}, fmt.Errorf("could not create session: %v", err)
}

return g.GetWithRoleForSession(clusterID, roleARN, sess)
}

// GetWithRole assumes the given AWS IAM role for the given session and behaves
// like GetWithRole.
func (g generator) GetWithRoleForSession(clusterID string, roleARN string, sess *session.Session) (Token, error) {
// use an STS client based on the direct credentials
stsAPI := sts.New(sess)

// if a roleARN was specified, replace the STS client with one that uses
// temporary credentials from that role.
if roleARN != "" {
sessionSetter := func(provider *stscreds.AssumeRoleProvider) {}
if g.forwardSessionName {
// If the current session is already a federated identity, carry through
// this session name onto the new session to provide better debugging
// capabilities
resp, err := stsAPI.GetCallerIdentity(&sts.GetCallerIdentityInput{})
if err != nil {
return Token{}, err
}

userIDParts := strings.Split(*resp.UserId, ":")
sessionSetter = func(provider *stscreds.AssumeRoleProvider) {
if len(userIDParts) == 2 {
provider.RoleSessionName = userIDParts[1]
}
}
}

// create STS-based credentials that will assume the given role
creds := stscreds.NewCredentials(sess, roleARN, sessionSetter)

// create an STS API interface that uses the assumed role's temporary credentials
stsAPI = sts.New(sess, &aws.Config{Credentials: creds})
}

return g.GetWithSTS(clusterID, stsAPI)
}

// GetWithSTS returns a token valid for clusterID using the given STS client.
func (g generator) GetWithSTS(clusterID string, stsAPI *sts.STS) (Token, error) {
// generate an sts:GetCallerIdentity request and add our custom cluster ID header
Expand All @@ -252,23 +193,6 @@ func (g generator) GetWithSTS(clusterID string, stsAPI *sts.STS) (Token, error)
return Token{v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLString)), tokenExpiration}, nil
}

// FormatJSON formats the json to support ExecCredential authentication
func (g generator) FormatJSON(token Token) string {
expirationTimestamp := metav1.NewTime(token.Expiration)
execInput := &clientauthv1alpha1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1alpha1",
Kind: "ExecCredential",
},
Status: &clientauthv1alpha1.ExecCredentialStatus{
ExpirationTimestamp: &expirationTimestamp,
Token: token.Token,
},
}
enc, _ := json.Marshal(execInput)
return string(enc)
}

// Verifier validates tokens by calling STS and returning the associated identity.
type Verifier interface {
Verify(token string) (*Identity, error)
Expand All @@ -287,6 +211,15 @@ func NewVerifier(clusterID string) Verifier {
}
}

// verify a sts host, doc: http://docs.amazonaws.cn/en_us/general/latest/gr/rande.html#sts_region
func (v tokenVerifier) verifyHost(host string) error {
if match, _ := regexp.MatchString(hostRegexp, host); !match {
return FormatError{fmt.Sprintf("unexpected hostname %q in pre-signed URL", host)}
}

return nil
}

// Verify a token is valid for the specified clusterID. On success, returns an
// Identity that contains information about the AWS principal that created the
// token. On failure, returns nil and a non-nil error.
Expand Down Expand Up @@ -314,8 +247,8 @@ func (v tokenVerifier) Verify(token string) (*Identity, error) {
return nil, FormatError{fmt.Sprintf("unexpected scheme %q in pre-signed URL", parsedURL.Scheme)}
}

if parsedURL.Host != "sts.amazonaws.com" {
return nil, FormatError{"unexpected hostname in pre-signed URL"}
if err = v.verifyHost(parsedURL.Host); err != nil {
return nil, err
}

if parsedURL.Path != "/" {
Expand Down Expand Up @@ -354,6 +287,9 @@ func (v tokenVerifier) Verify(token string) (*Identity, error) {
return nil, FormatError{"X-Amz-Date parameter must be present in pre-signed URL"}
}

// Obtain AWS Access Key ID from supplied credentials
accessKeyID := strings.Split(queryParamsLower.Get("x-amz-credential"), "/")[0]

dateParam, err := time.Parse(dateHeaderFormat, date)
if err != nil {
return nil, FormatError{fmt.Sprintf("error parsing X-Amz-Date parameter %s into format %s: %s", date, dateHeaderFormat, err.Error())}
Expand All @@ -365,7 +301,7 @@ func (v tokenVerifier) Verify(token string) (*Identity, error) {
return nil, FormatError{fmt.Sprintf("X-Amz-Date parameter is expired (%.f minute expiration) %s", presignedURLExpiration.Minutes(), dateParam)}
}

req, err := http.NewRequest("GET", parsedURL.String(), nil)
req, _ := http.NewRequest("GET", parsedURL.String(), nil)
req.Header.Set(clusterIDHeader, v.clusterID)
req.Header.Set("accept", "application/json")

Expand All @@ -379,15 +315,15 @@ func (v tokenVerifier) Verify(token string) (*Identity, error) {
}
defer response.Body.Close()

if response.StatusCode != 200 {
return nil, NewSTSError(fmt.Sprintf("error from AWS (expected 200, got %d)", response.StatusCode))
}

responseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, NewSTSError(fmt.Sprintf("error reading HTTP result: %v", err))
}

if response.StatusCode != 200 {
return nil, NewSTSError(fmt.Sprintf("error from AWS (expected 200, got %d). Body: %s", response.StatusCode, string(responseBody[:])))
}

var callerIdentity getCallerIdentityWrapper
err = json.Unmarshal(responseBody, &callerIdentity)
if err != nil {
Expand All @@ -396,10 +332,11 @@ func (v tokenVerifier) Verify(token string) (*Identity, error) {

// parse the response into an Identity
id := &Identity{
ARN: callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Arn,
AccountID: callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Account,
ARN: callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Arn,
AccountID: callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Account,
AccessKeyID: accessKeyID,
}
id.CanonicalARN, err = arn.Canonicalize(id.ARN)
id.CanonicalARN, err = Canonicalize(id.ARN)
if err != nil {
return nil, NewSTSError(err.Error())
}
Expand All @@ -424,7 +361,7 @@ func (v tokenVerifier) Verify(token string) (*Identity, error) {
func hasSignedClusterIDHeader(paramsLower *url.Values) bool {
signedHeaders := strings.Split(paramsLower.Get("x-amz-signedheaders"), ";")
for _, hdr := range signedHeaders {
if strings.ToLower(hdr) == strings.ToLower(clusterIDHeader) {
if strings.EqualFold(hdr, clusterIDHeader) {
return true
}
}
Expand Down
Loading

0 comments on commit 487ecf0

Please sign in to comment.