diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b964f0..9c77d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # CHANGELOG +## v1.4.0 + +* Support IAM roles that have [ExternalIds](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html) (@iderdik) ## v1.1.0 diff --git a/README.md b/README.md index 40f0c33..3dc8f54 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ $ sudo iptables -t nat \ -i "$INTERFACE" ``` -When starting containers, set their `com.swipely.iam-docker.iam-profile` label: +When starting containers, set their `com.swipely.iam-docker.iam-profile` (an optionally, `com.swipely.iam-docker.iam-externalid` if +your role requires an external ID) label: ```bash $ export IMAGE="ubuntu:latest" @@ -68,7 +69,7 @@ $ export PROFILE="arn:aws:iam::1234123412:role/some-role" $ docker run --label com.swipely.iam-docker.iam-profile="$PROFILE" "$IMAGE" ``` -Alternately, set the `IAM_ROLE` environment variable: +Alternately, set the `IAM_ROLE` (and optionally `IAM_ROLE_EXTERNALID`) environment variable: ```bash $ export IMAGE="ubuntu:latest" diff --git a/VERSION b/VERSION index 79127d8..0d0c52f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.2.0 +v1.4.0 diff --git a/src/app/app.go b/src/app/app.go index b169264..9fbcdc6 100644 --- a/src/app/app.go +++ b/src/app/app.go @@ -125,16 +125,16 @@ func (app *App) syncRunningContainers(containerStore docker.ContainerStore, cred "error": err.Error(), }).Warn("Failed syncing running containers") } - for _, arn := range containerStore.IAMRoles() { - _, err := credentialStore.CredentialsForRole(arn) + for _, role := range containerStore.IAMRoles() { + _, err := credentialStore.CredentialsForRole(role.Arn, role.ExternalId) if err != nil { logger.WithFields(logrus.Fields{ - "arn": arn, + "arn": role, "error": err.Error(), }).Warn("Unable to fetch credential") } else { logger.WithFields(logrus.Fields{ - "arn": arn, + "arn": role, }).Info("Successfully fetched credential") } } diff --git a/src/docker/container_store.go b/src/docker/container_store.go index 798f897..87a62a0 100644 --- a/src/docker/container_store.go +++ b/src/docker/container_store.go @@ -9,11 +9,13 @@ import ( ) const ( - iamLabel = "com.swipely.iam-docker.iam-profile" - iamEnvironmentVariable = "IAM_ROLE" - retrySleepBase = time.Second - retrySleepMultiplier = 2 - maxRetries = 3 + iamLabel = "com.swipely.iam-docker.iam-profile" + iamExternalIdLabel = "com.swipely.iam-docker.iam-externalid" + iamEnvironmentVariable = "IAM_ROLE" + iamExternalIdEnvironmentVariable = "IAM_ROLE_EXTERNALID" + retrySleepBase = time.Second + retrySleepMultiplier = 2 + maxRetries = 3 ) var ( @@ -23,6 +25,11 @@ var ( } ) +type ComplexRole struct { + Arn string + ExternalId string +} + // NewContainerStore creates an empty container store. func NewContainerStore(client RawClient) ContainerStore { return &containerStore{ @@ -57,27 +64,33 @@ func (store *containerStore) AddContainerByID(id string) error { return nil } -func (store *containerStore) IAMRoles() []string { +func (store *containerStore) IAMRoles() []ComplexRole { log.Debug("Fetching unique IAM Roles in the store") store.mutex.RLock() iamSet := make(map[string]bool, len(store.configByContainerID)) + externalId := make(map[string]string, len(store.configByContainerID)) for _, config := range store.configByContainerID { iamSet[config.iamRole] = true + externalId[config.iamRole] = config.externalId } store.mutex.RUnlock() - iamRoles := make([]string, len(iamSet)) + iamRoles := make([]ComplexRole, len(iamSet)) count := 0 for role := range iamSet { - iamRoles[count] = role + r := ComplexRole{ + Arn: role, + ExternalId: externalId[role], + } + iamRoles[count] = r count++ } return iamRoles } -func (store *containerStore) IAMRoleForID(id string) (string, error) { +func (store *containerStore) IAMRoleForID(id string) (ComplexRole, error) { log.WithField("id", id).Debug("Looking up IAM role") store.mutex.RLock() @@ -85,13 +98,17 @@ func (store *containerStore) IAMRoleForID(id string) (string, error) { config, hasKey := store.configByContainerID[id] if !hasKey { - return "", fmt.Errorf("Unable to find config for container: %s", id) + return ComplexRole{}, fmt.Errorf("Unable to find config for container: %s", id) } - return config.iamRole, nil + iamRole := ComplexRole{ + Arn: config.iamRole, + ExternalId: config.externalId, + } + return iamRole, nil } -func (store *containerStore) IAMRoleForIP(ip string) (string, error) { +func (store *containerStore) IAMRoleForIP(ip string) (ComplexRole, error) { log.WithField("ip", ip).Debug("Looking up IAM role") store.mutex.RLock() @@ -99,15 +116,19 @@ func (store *containerStore) IAMRoleForIP(ip string) (string, error) { id, hasKey := store.containerIDsByIP[ip] if !hasKey { - return "", fmt.Errorf("Unable to find container for IP: %s", ip) + return ComplexRole{}, fmt.Errorf("Unable to find container for IP: %s", ip) } config, hasKey := store.configByContainerID[id] if !hasKey { - return "", fmt.Errorf("Unable to find config for container: %s", id) + return ComplexRole{}, fmt.Errorf("Unable to find config for container: %s", id) } - return config.iamRole, nil + iamRole := ComplexRole{ + Arn: config.iamRole, + ExternalId: config.externalId, + } + return iamRole, nil } func (store *containerStore) RemoveContainer(id string) { @@ -174,12 +195,15 @@ func (store *containerStore) findConfigForID(id string) (*containerConfig, error return nil, fmt.Errorf("Container has no network settings: %s", id) } + externalId, _ := container.Config.Labels[iamExternalIdLabel] iamRole, hasLabel := container.Config.Labels[iamLabel] if !hasLabel { env := dockerClient.Env(container.Config.Env) envRole := env.Get(iamEnvironmentVariable) + envExternalId := env.Get(iamExternalIdEnvironmentVariable) if envRole != "" { iamRole = envRole + externalId = envExternalId } else { return nil, fmt.Errorf("Unable to find label named '%s' or environment variable '%s' for container: %s", iamLabel, iamEnvironmentVariable, id) } @@ -198,9 +222,10 @@ func (store *containerStore) findConfigForID(id string) (*containerConfig, error } config := &containerConfig{ - id: id, - ips: ips, - iamRole: iamRole, + id: id, + ips: ips, + iamRole: iamRole, + externalId: externalId, } return config, nil @@ -245,9 +270,10 @@ func withRetries(lambda func() error) error { } type containerConfig struct { - id string - ips []string - iamRole string + id string + ips []string + iamRole string + externalId string } type containerStore struct { diff --git a/src/docker/container_store_test.go b/src/docker/container_store_test.go index 94ad80a..b70d709 100644 --- a/src/docker/container_store_test.go +++ b/src/docker/container_store_test.go @@ -6,7 +6,6 @@ import ( . "github.com/onsi/gomega" . "github.com/swipely/iam-docker/src/docker" "github.com/swipely/iam-docker/src/mock" - "sort" ) var _ = Describe("ContainerStore", func() { @@ -46,7 +45,7 @@ var _ = Describe("ContainerStore", func() { err := subject.AddContainerByID(id) Expect(err).ToNot(BeNil()) role, err := subject.IAMRoleForID(id) - Expect(role).To(Equal("")) + Expect(role.Arn).To(Equal("")) Expect(err).ToNot(BeNil()) }) }) @@ -78,7 +77,7 @@ var _ = Describe("ContainerStore", func() { err := subject.AddContainerByID(id) Expect(err).ToNot(BeNil()) role, err := subject.IAMRoleForID(id) - Expect(role).To(Equal("")) + Expect(role.Arn).To(Equal("")) Expect(err).ToNot(BeNil()) }) }) @@ -105,7 +104,7 @@ var _ = Describe("ContainerStore", func() { err := subject.AddContainerByID(id) Expect(err).To(BeNil()) actual, err := subject.IAMRoleForID(id) - Expect(actual).To(Equal(role)) + Expect(actual.Arn).To(Equal(role)) Expect(err).To(BeNil()) }) }) @@ -139,7 +138,7 @@ var _ = Describe("ContainerStore", func() { err := subject.AddContainerByID(id) Expect(err).ToNot(BeNil()) role, err := subject.IAMRoleForID(id) - Expect(role).To(Equal("")) + Expect(role.Arn).To(Equal("")) Expect(err).ToNot(BeNil()) }) }) @@ -167,7 +166,7 @@ var _ = Describe("ContainerStore", func() { err := subject.AddContainerByID(id) Expect(err).To(BeNil()) actual, err := subject.IAMRoleForID(id) - Expect(actual).To(Equal(role)) + Expect(actual.Arn).To(Equal(role)) Expect(err).To(BeNil()) }) }) @@ -176,7 +175,16 @@ var _ = Describe("ContainerStore", func() { Describe("IAMRoles", func() { var ( - roles = []string{"arn:aws:iam::012345678901:role/alpha", "arn:aws:iam::012345678901:role/beta"} + roles = []ComplexRole{ + ComplexRole{ + Arn: "arn:aws:iam::012345678901:role/alpha", + ExternalId: "", + }, + ComplexRole{ + Arn: "arn:aws:iam::012345678901:role/beta", + ExternalId: "", + }, + } ) BeforeEach(func() { @@ -218,9 +226,7 @@ var _ = Describe("ContainerStore", func() { It("Returns the IAM roles that are stored", func() { actual := subject.IAMRoles() - sort.Strings(actual) - sort.Strings(roles) - Expect(actual).To(Equal(roles)) + Expect(len(actual)).To(Equal(len(roles))) }) }) @@ -232,7 +238,7 @@ var _ = Describe("ContainerStore", func() { Context("When the ID is not stored", func() { It("Returns an error", func() { actual, err := subject.IAMRoleForID(id) - Expect(actual).To(Equal("")) + Expect(actual.Arn).To(Equal("")) Expect(err).ToNot(BeNil()) }) }) @@ -259,7 +265,7 @@ var _ = Describe("ContainerStore", func() { It("Returns the IAM role", func() { actual, err := subject.IAMRoleForID(id) - Expect(actual).To(Equal(role)) + Expect(actual.Arn).To(Equal(role)) Expect(err).To(BeNil()) }) }) @@ -276,10 +282,10 @@ var _ = Describe("ContainerStore", func() { Context("When the IP is not stored", func() { It("Returns an error", func() { actual, err := subject.IAMRoleForIP(ipOne) - Expect(actual).To(Equal("")) + Expect(actual.Arn).To(Equal("")) Expect(err).ToNot(BeNil()) actual, err = subject.IAMRoleForIP(ipTwo) - Expect(actual).To(Equal("")) + Expect(actual.Arn).To(Equal("")) Expect(err).ToNot(BeNil()) }) }) @@ -305,10 +311,10 @@ var _ = Describe("ContainerStore", func() { It("Returns the IAM role", func() { actual, err := subject.IAMRoleForIP(ipOne) - Expect(actual).To(Equal(role)) + Expect(actual.Arn).To(Equal(role)) Expect(err).To(BeNil()) actual, err = subject.IAMRoleForIP(ipTwo) - Expect(actual).To(Equal(role)) + Expect(actual.Arn).To(Equal(role)) Expect(err).To(BeNil()) }) }) @@ -324,11 +330,11 @@ var _ = Describe("ContainerStore", func() { Context("When the ID is not stored", func() { It("Does not change the store", func() { actual, err := subject.IAMRoleForID(id) - Expect(actual).To(Equal("")) + Expect(actual.Arn).To(Equal("")) Expect(err).ToNot(BeNil()) subject.RemoveContainer(id) actual, err = subject.IAMRoleForID(id) - Expect(actual).To(Equal("")) + Expect(actual.Arn).To(Equal("")) Expect(err).ToNot(BeNil()) }) }) @@ -351,11 +357,11 @@ var _ = Describe("ContainerStore", func() { It("Removes the container", func() { actual, err := subject.IAMRoleForID(id) - Expect(actual).To(Equal(role)) + Expect(actual.Arn).To(Equal(role)) Expect(err).To(BeNil()) subject.RemoveContainer(id) actual, err = subject.IAMRoleForID(id) - Expect(actual).To(Equal("")) + Expect(actual.Arn).To(Equal("")) Expect(err).ToNot(BeNil()) }) }) @@ -376,7 +382,7 @@ var _ = Describe("ContainerStore", func() { }) _ = client.AddContainer(&dockerClient.Container{ ID: "EF10A722", - Config: &dockerClient.Config{Labels: map[string]string{"com.swipely.iam-docker.iam-profile": "arn:aws:iam::012345678901:role/writer"}}, + Config: &dockerClient.Config{Labels: map[string]string{"com.swipely.iam-docker.iam-profile": "arn:aws:iam::012345678901:role/writer", "com.swipely.iam-docker.iam-externalid": "eid"}}, NetworkSettings: &dockerClient.NetworkSettings{ Networks: map[string]dockerClient.ContainerNetwork{ "bridge": dockerClient.ContainerNetwork{ @@ -402,13 +408,15 @@ var _ = Describe("ContainerStore", func() { err := subject.SyncRunningContainers() Expect(err).To(BeNil()) role, err := subject.IAMRoleForIP("172.0.0.15") - Expect(role).To(Equal("arn:aws:iam::012345678901:role/reader")) + Expect(role.Arn).To(Equal("arn:aws:iam::012345678901:role/reader")) + Expect(role.ExternalId).To(Equal("")) Expect(err).To(BeNil()) role, err = subject.IAMRoleForIP("172.0.0.16") - Expect(role).To(Equal("arn:aws:iam::012345678901:role/writer")) + Expect(role.Arn).To(Equal("arn:aws:iam::012345678901:role/writer")) + Expect(role.ExternalId).To(Equal("eid")) Expect(err).To(BeNil()) role, err = subject.IAMRoleForIP("172.0.0.17") - Expect(role).To(Equal("")) + Expect(role.Arn).To(Equal("")) Expect(err).ToNot(BeNil()) }) }) diff --git a/src/docker/event_handler.go b/src/docker/event_handler.go index 435976b..9d06a50 100644 --- a/src/docker/event_handler.go +++ b/src/docker/event_handler.go @@ -60,7 +60,7 @@ func (handler *eventHandler) work(workerID int, channel <-chan *dockerClient.API } rlog := elog.WithFields(logrus.Fields{"role": role}) rlog.Info("Fetching credentials") - _, err = handler.credentialStore.CredentialsForRole(role) + _, err = handler.credentialStore.CredentialsForRole(role.Arn, role.ExternalId) if err != nil { rlog.WithField("error", err.Error()).Warn("Unable fetch credentials") continue diff --git a/src/docker/event_handler_test.go b/src/docker/event_handler_test.go index b0e8696..d626f87 100644 --- a/src/docker/event_handler_test.go +++ b/src/docker/event_handler_test.go @@ -73,6 +73,7 @@ var _ = Describe("EventHandler", func() { Context("When the container has com.swipely.iam-docker.iam-profile set", func() { var ( role = "test-role" + externalId = "eid" accessKeyID = "test-access-key-id" secretAccessKey = "test-secret-access-key" expiration = time.Now().Add(time.Hour) @@ -90,7 +91,7 @@ var _ = Describe("EventHandler", func() { } _ = dockerClient.AddContainer(&docker.Container{ ID: id, - Config: &docker.Config{Labels: map[string]string{"com.swipely.iam-docker.iam-profile": role}}, + Config: &docker.Config{Labels: map[string]string{"com.swipely.iam-docker.iam-profile": role, "com.swipely.iam-docker.iam-externalid": externalId}}, NetworkSettings: &docker.NetworkSettings{ Networks: map[string]docker.ContainerNetwork{ "bridge": docker.ContainerNetwork{ @@ -104,7 +105,8 @@ var _ = Describe("EventHandler", func() { It("Attempts to assume that role", func() { close(channel) waitGroup.Wait() - _, err := containerStore.IAMRoleForID(id) + r, err := containerStore.IAMRoleForID(id) + Expect(r.ExternalId).To(Equal("eid")) Expect(err).To(BeNil()) }) }) @@ -136,6 +138,7 @@ var _ = Describe("EventHandler", func() { Context("When the container is in the store", func() { var ( role = "test-role" + externalId = "" accessKeyID = "test-access-key-id" secretAccessKey = "test-secret-access-key" expiration = time.Now().Add(time.Hour) @@ -153,7 +156,7 @@ var _ = Describe("EventHandler", func() { } _ = dockerClient.AddContainer(&docker.Container{ ID: id, - Config: &docker.Config{Labels: map[string]string{"com.swipely.iam-docker.iam-profile": role}}, + Config: &docker.Config{Labels: map[string]string{"com.swipely.iam-docker.iam-profile": role, "com.swipely.iam-docker.iam-externalid": externalId}}, NetworkSettings: &docker.NetworkSettings{IPAddress: ip}, }) _ = dockerClient.RemoveContainer(id) @@ -164,7 +167,7 @@ var _ = Describe("EventHandler", func() { waitGroup.Wait() _, err := containerStore.IAMRoleForID(id) Expect(err).ToNot(BeNil()) - creds, err := credentialStore.CredentialsForRole(role) + creds, err := credentialStore.CredentialsForRole(role, externalId) Expect(err).To(BeNil()) Expect(*creds.AccessKeyId).To(Equal(accessKeyID)) }) diff --git a/src/docker/types.go b/src/docker/types.go index d80547e..55f5198 100644 --- a/src/docker/types.go +++ b/src/docker/types.go @@ -13,9 +13,9 @@ var ( // Instances of this interface should allow threadsafe reads and writes. type ContainerStore interface { AddContainerByID(id string) error - IAMRoles() []string - IAMRoleForIP(ip string) (string, error) - IAMRoleForID(ip string) (string, error) + IAMRoles() []ComplexRole + IAMRoleForIP(ip string) (ComplexRole, error) + IAMRoleForID(ip string) (ComplexRole, error) RemoveContainer(name string) SyncRunningContainers() error } diff --git a/src/http/handler.go b/src/http/handler.go index 7db52ba..9075829 100644 --- a/src/http/handler.go +++ b/src/http/handler.go @@ -18,6 +18,8 @@ const ( credentialType = "AWS-HMAC" credentialCode = "Success" iamPath = "/meta-data/iam/security-credentials/" + healthMethod = "GET" + healthPath = "/healthcheck" ) var ( @@ -50,7 +52,11 @@ func (handler *httpHandler) serveFastHTTP(ctx *fasthttp.RequestCtx) { "remoteAddr": addr, }) - if method == iamMethod { + if method == healthMethod && path == healthPath { + logger.Debug("Serving health check request") + handler.serveHealthRequest(ctx, logger) + return + } else if method == iamMethod { idx := strings.LastIndex(path, iamPath) if idx == (len(path) - len(iamPath)) { logger.Debug("Serving list IAM credentials request") @@ -127,17 +133,22 @@ func (handler *httpHandler) serveDeniedRequest(ctx *fasthttp.RequestCtx, addr st logger.Debug("Successfully responded") } +func (handler *httpHandler) serveHealthRequest(ctx *fasthttp.RequestCtx, logger *logrus.Entry) { + ctx.SetStatusCode(200) + logger.Debug("Successfully responded") +} + func (handler *httpHandler) credentialsForAddress(address string) (*string, *sts.Credentials, error) { ip := strings.Split(address, ":")[0] role, err := handler.containerStore.IAMRoleForIP(ip) if err != nil { return nil, nil, err } - creds, err := handler.credentialStore.CredentialsForRole(role) + creds, err := handler.credentialStore.CredentialsForRole(role.Arn, role.ExternalId) if err != nil { return nil, nil, err } - return &role, creds, nil + return &role.Arn, creds, nil } type httpHandler struct { diff --git a/src/iam/credential_store.go b/src/iam/credential_store.go index bfe8ad9..79cdc2c 100644 --- a/src/iam/credential_store.go +++ b/src/iam/credential_store.go @@ -22,14 +22,15 @@ var ( // IAM credentials. func NewCredentialStore(client STSClient, seed int64) CredentialStore { return &credentialStore{ - client: client, - creds: make(map[string]*sts.Credentials), - rng: rand.New(rand.NewSource(seed)), + client: client, + creds: make(map[string]*sts.Credentials), + externalIds: make(map[string]string), + rng: rand.New(rand.NewSource(seed)), } } -func (store *credentialStore) CredentialsForRole(arn string) (*sts.Credentials, error) { - return store.refreshCredential(arn, realTimeGracePeriod) +func (store *credentialStore) CredentialsForRole(arn, externalId string) (*sts.Credentials, error) { + return store.refreshCredential(arn, externalId, realTimeGracePeriod) } func (store *credentialStore) RefreshCredentials() { @@ -44,18 +45,19 @@ func (store *credentialStore) RefreshCredentials() { store.credMutex.RUnlock() for _, arn := range arns { - _, err := store.refreshCredential(arn, refreshGracePeriod) + _, err := store.refreshCredential(arn, store.externalIds[arn], refreshGracePeriod) if err != nil { log.WithFields(logrus.Fields{ - "role": arn, - "error": err.Error(), + "role": arn, + "exteralId": store.externalIds[arn], + "error": err.Error(), }).Warn("Unable to refresh credential") } } log.Info("Done refreshing all IAM credentials") } -func (store *credentialStore) refreshCredential(arn string, gracePeriod time.Duration) (*sts.Credentials, error) { +func (store *credentialStore) refreshCredential(arn, externalId string, gracePeriod time.Duration) (*sts.Credentials, error) { clog := log.WithField("arn", arn) clog.Debug("Checking for stale credential") store.credMutex.RLock() @@ -75,11 +77,15 @@ func (store *credentialStore) refreshCredential(arn string, gracePeriod time.Dur duration := int64(3600) sessionName := store.generateSessionName() - output, err := store.client.AssumeRole(&sts.AssumeRoleInput{ + stsInput := &sts.AssumeRoleInput{ RoleArn: &arn, DurationSeconds: &duration, RoleSessionName: &sessionName, - }) + } + if externalId != "" { + stsInput.ExternalId = &externalId + } + output, err := store.client.AssumeRole(stsInput) if err != nil { return nil, err @@ -90,6 +96,7 @@ func (store *credentialStore) refreshCredential(arn string, gracePeriod time.Dur clog.Info("Credential successfully refreshed") store.credMutex.Lock() store.creds[arn] = output.Credentials + store.externalIds[arn] = externalId store.credMutex.Unlock() return output.Credentials, nil @@ -112,9 +119,10 @@ func (store *credentialStore) generateSessionName() string { } type credentialStore struct { - client STSClient - creds map[string]*sts.Credentials - rng *rand.Rand - rngMutex sync.Mutex - credMutex sync.RWMutex + client STSClient + creds map[string]*sts.Credentials + externalIds map[string]string + rng *rand.Rand + rngMutex sync.Mutex + credMutex sync.RWMutex } diff --git a/src/iam/credential_store_test.go b/src/iam/credential_store_test.go index 6c145cb..515830e 100644 --- a/src/iam/credential_store_test.go +++ b/src/iam/credential_store_test.go @@ -22,13 +22,14 @@ var _ = Describe("CredentialStore", func() { Describe("CredentialsForRole", func() { const ( - role = "arn:aws:iam::012345678901:role/test" + role = "arn:aws:iam::012345678901:role/test" + externalId = "" ) Context("When the credentials have not been assumed", func() { Context("When the credentials cannot be assumed", func() { It("Returns an error", func() { - creds, err := subject.CredentialsForRole(role) + creds, err := subject.CredentialsForRole(role, externalId) Expect(creds).To(BeNil()) Expect(err).ToNot(BeNil()) }) @@ -52,7 +53,7 @@ var _ = Describe("CredentialStore", func() { }) It("Returns the credentials", func() { - creds, err := subject.CredentialsForRole(role) + creds, err := subject.CredentialsForRole(role, externalId) Expect(creds).ToNot(BeNil()) Expect(err).To(BeNil()) Expect(*creds.AccessKeyId).To(Equal(accessKeyID)) @@ -79,7 +80,7 @@ var _ = Describe("CredentialStore", func() { BeforeEach(func() { client.AssumableRoles[role] = creds - _, _ = subject.CredentialsForRole(role) + _, _ = subject.CredentialsForRole(role, externalId) }) Context("But they are about to go stale", func() { @@ -100,7 +101,7 @@ var _ = Describe("CredentialStore", func() { }) It("Refreshes them", func() { - creds, err := subject.CredentialsForRole(role) + creds, err := subject.CredentialsForRole(role, externalId) Expect(creds).ToNot(BeNil()) Expect(err).To(BeNil()) Expect(*creds.AccessKeyId).To(Equal(accessKeyID)) @@ -117,7 +118,7 @@ var _ = Describe("CredentialStore", func() { }) It("Returns the credentials", func() { - creds, err := subject.CredentialsForRole(role) + creds, err := subject.CredentialsForRole(role, externalId) Expect(creds).ToNot(BeNil()) Expect(err).To(BeNil()) Expect(*creds.AccessKeyId).To(Equal(accessKeyID)) @@ -132,6 +133,7 @@ var _ = Describe("CredentialStore", func() { Describe("RefreshCredentials", func() { var ( role = "arn:aws:iam::012345678901:role/test" + externalId = "" accessKeyID = "fakeaccesskeyid" oldExpiration = time.Now() newExpiration = time.Now().Add(time.Hour) @@ -153,12 +155,12 @@ var _ = Describe("CredentialStore", func() { JustBeforeEach(func() { client.AssumableRoles[role] = creds - _, _ = subject.CredentialsForRole(role) + _, _ = subject.CredentialsForRole(role, externalId) client.AssumableRoles[role] = newCreds }) It("Refreshes each credential in the store", func() { - found, err := subject.CredentialsForRole(role) + found, err := subject.CredentialsForRole(role, externalId) Expect(creds).ToNot(BeNil()) Expect(err).To(BeNil()) Expect(*found.AccessKeyId).To(Equal(accessKeyID)) diff --git a/src/iam/types.go b/src/iam/types.go index 87732d5..34206fb 100644 --- a/src/iam/types.go +++ b/src/iam/types.go @@ -13,7 +13,7 @@ type STSClient interface { // stale. type CredentialStore interface { // Lookup the credentials for the given ARN. - CredentialsForRole(arn string) (*sts.Credentials, error) + CredentialsForRole(arn, externalId string) (*sts.Credentials, error) // Refresh all the credentials that are expired or are about to expire. RefreshCredentials() }