Skip to content

Commit

Permalink
Add gimme-aws-creds assumer (#810)
Browse files Browse the repository at this point in the history
* Add gimme-aws-creds assumer

* Add to assumers

* parse gimme config file for profile matches

* allow credential_process refresh for OIE flow

* Add version-gating to improve reliability

* path join

* check auto-login status for credential_process

* update error

* fix go.mod/sum

* remove force open browser flag

* add comment around env cleaning
  • Loading branch information
jpts authored Jan 30, 2025
1 parent b53429f commit 2add2a1
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 1 deletion.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
github.com/common-fate/sdk v1.71.0
github.com/common-fate/xid v1.0.0
github.com/fatih/color v1.16.0
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/yamux v0.1.2
github.com/lithammer/fuzzysearch v1.1.5
github.com/mattn/go-runewidth v0.0.16
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
Expand Down
283 changes: 283 additions & 0 deletions pkg/cfaws/assumer_aws_gimme_aws_creds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
package cfaws

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/common-fate/clio"
"github.com/common-fate/granted/pkg/securestorage"
"github.com/hashicorp/go-version"
"gopkg.in/ini.v1"
)

type AwsGimmeAwsCredsAssumer struct {
config *ini.File
forceOpenBrowser bool
}

type CredentialCapture struct {
result *AwsGimmeResult
}

type AwsGimmeResult struct {
Credentials AwsGimmeCredentials `json:"credentials"`
}

type AwsGimmeCredentials struct {
AccessKeyID string `json:"aws_access_key_id"`
SecretAccessKey string `json:"aws_secret_access_key"`
SessionToken string `json:"aws_session_token"`
Expiration string `json:"expiration"`
}

func (cc *CredentialCapture) Write(p []byte) (n int, err error) {
var dest AwsGimmeResult
err = json.Unmarshal(p, &dest)
if err != nil {
return 0, fmt.Errorf("Error unmarshalling gimme-aws-creds output")
}
cc.result = &dest
return len(p), nil
}

func (cc *CredentialCapture) Creds() (aws.Credentials, error) {
if cc.result == nil {
return aws.Credentials{}, fmt.Errorf("no credential output from gimme-aws-creds")
}
c := aws.Credentials{
AccessKeyID: cc.result.Credentials.AccessKeyID,
SecretAccessKey: cc.result.Credentials.SecretAccessKey,
SessionToken: cc.result.Credentials.SessionToken,
Source: "gimme-aws-creds",
}
if cc.result.Credentials.Expiration != "" {
c.CanExpire = true
t, err := time.Parse(time.RFC3339, cc.result.Credentials.Expiration)
if err != nil {
return aws.Credentials{}, fmt.Errorf("could not parse credentials expiry: %s", cc.result.Credentials.Expiration)
}
c.Expires = t
}
return c, nil
}

func (gimme *AwsGimmeAwsCredsAssumer) AssumeTerminal(ctx context.Context, c *Profile, configOpts ConfigOpts) (aws.Credentials, error) {
// try cache
sessionCredStorage := securestorage.NewSecureSessionCredentialStorage()
creds, err := sessionCredStorage.GetCredentials(c.AWSConfig.Profile)

if err != nil {
clio.Debugw("error loading cached credentials", "error", err)
} else if creds != nil && !creds.Expired() {
clio.Debugw("credentials found in cache", "expires", creds.Expires.String(), "canExpire", creds.CanExpire, "timeNow", time.Now().String())
return *creds, nil
}

// if cred process, check we can do a non-interactive refresh
if configOpts.UsingCredentialProcess {
if !configOpts.CredentialProcessAutoLogin {
return aws.Credentials{}, fmt.Errorf("Failed to auto-refresh gimme-aws-creds, since auto-login is disabled")
}
err := gimme.LoadGimmeConfig()
if err != nil {
return aws.Credentials{}, fmt.Errorf("Failed to load gimme config file: %w", err)
}
if !gimme.CanRefreshHeadless(c.Name) {
return aws.Credentials{}, fmt.Errorf("Cannot use gimme-aws-creds in credential_process with force_classic or <2.6.0")
}
gimme.forceOpenBrowser = true
}

clio.Debugw("refreshing credentials", "reason", "none cached")

// request for the creds if they are invalid
args := []string{
fmt.Sprintf("--profile=%s", c.Name),
"--output-format=json",
}

if gimme.forceOpenBrowser {
args = append(args, "--open-browser")
}

// add passthrough args
args = append(args, configOpts.Args...)

cmd := exec.Command("gimme-aws-creds", args...)

capture := &CredentialCapture{}
cmd.Stdout = capture
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr

// We always want to request fresh credentials from gimme-aws-creds,
// since caching will take place in granted. Also prevents gimme-aws-creds
// from trying to read/parse existing AWS profiles.
cleanEnv := []string{}
var disallowedVar bool
for _, env := range os.Environ() {
disallowedVar = false
for _, disallowed := range []string{
"AWS_PROFILE",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"AWS_REGION",
"AWS_DEFAULT_REGION",
} {
if strings.HasPrefix(env, disallowed) {
clio.Debugw("removing from exec env", "var", env)
disallowedVar = true
break
}
}
if !disallowedVar {
cleanEnv = append(cleanEnv, env)
}
}
cmd.Env = cleanEnv

err = cmd.Run()
if err != nil {
return aws.Credentials{}, err
}

awscreds, err := capture.Creds()
if err != nil {
return aws.Credentials{}, err
}

// store cached creds
if err := sessionCredStorage.StoreCredentials(c.AWSConfig.Profile, awscreds); err != nil {
clio.Warnf("Error caching credentials, MFA token will be requested")
}

return awscreds, nil
}

func (gimme *AwsGimmeAwsCredsAssumer) AssumeConsole(ctx context.Context, c *Profile, configOpts ConfigOpts) (aws.Credentials, error) {
return gimme.AssumeTerminal(ctx, c, configOpts)
}

func (gimme *AwsGimmeAwsCredsAssumer) Type() string {
return "AWS_GIMME_AWS_CREDS"
}

// parse the gimme config file to check if we have a matching profile
func (gimme *AwsGimmeAwsCredsAssumer) ProfileMatchesType(rawProfile *ini.Section, parsedProfile config.SharedConfig) bool {
err := gimme.LoadGimmeConfig()
if errors.Is(err, os.ErrNotExist) {
return false
}
if err != nil {
clio.Error("Failed to load gimme config file: ", err)
return false
}

for _, section := range gimme.config.SectionStrings() {
if section == parsedProfile.Profile {
clio.Debug("matched gimme profile ", section)
return true
}
}

return false
}

func (gimme *AwsGimmeAwsCredsAssumer) Version() (*version.Version, error) {
cmd, err := exec.Command("gimme-aws-creds", "--version").Output()
if err != nil {
return nil, fmt.Errorf("Failed to get gimme-aws-creds version %w", err)
}

ver, err := version.NewVersion(strings.TrimSpace(strings.TrimPrefix(string(cmd), "gimme-aws-creds")))
if err != nil {
return nil, err
}

return ver, nil
}

func (gimme *AwsGimmeAwsCredsAssumer) LoadGimmeConfig() error {
okta_config := os.Getenv("OKTA_CONFIG")
if okta_config == "" {
home, err := os.UserHomeDir()
if err != nil {
clio.Error(err)
}
okta_config = path.Join(home, ".okta_aws_login_config")
}

_, err := os.Stat(okta_config)
if err != nil {
return err
}

gimme.config, err = ini.Load(okta_config)
if err != nil {
return err
}

return nil
}

func (gimme *AwsGimmeAwsCredsAssumer) CanRefreshHeadless(profile string) bool {
ver, err := gimme.Version()
if err != nil {
clio.Warn(err)
return false
}

// Device flow only supported in 2.6.0+
gimme_260, _ := version.NewVersion("v2.6.0")
if ver.LessThan(gimme_260) {
return false
}

section, err := gimme.config.GetSection(profile)
if err != nil {
clio.Warn(err)
return false
}

if section.HasKey("force_classic") {
// force_classic is default True after v2.8.1
force_classic_default := true
gimme_281, _ := version.NewVersion("v2.8.1")
if ver.LessThan(gimme_281) {
force_classic_default = false
}

key, err := section.GetKey("force_classic")
if err != nil {
clio.Warn(err)
return false
}
if key.MustBool(force_classic_default) == true {

Check failure on line 265 in pkg/cfaws/assumer_aws_gimme_aws_creds.go

View workflow job for this annotation

GitHub Actions / Go Lint

S1002: should omit comparison to bool constant, can be simplified to `key.MustBool(force_classic_default)` (gosimple)
return false
}
}

if section.HasKey("inherits") {
key, err := section.GetKey("inherits")
if err != nil {
clio.Warn(err)
return false
}
parent := key.MustString("")
if parent != "" {
return gimme.CanRefreshHeadless(parent)
}
}

return true
}
2 changes: 1 addition & 1 deletion pkg/cfaws/assumers.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type Assumer interface {
// List of assumers should be ordered by how they match type
// specific types should be first, generic types like IAM should be last / the (default)
// for sso profiles, the internal implementation takes precedence over credential processes
var assumers []Assumer = []Assumer{&AwsGoogleAuthAssumer{}, &AwsAzureLoginAssumer{}, &AwsSsoAssumer{}, &CredentialProcessAssumer{}, &AwsIamAssumer{}}
var assumers []Assumer = []Assumer{&AwsGimmeAwsCredsAssumer{}, &AwsGoogleAuthAssumer{}, &AwsAzureLoginAssumer{}, &AwsSsoAssumer{}, &CredentialProcessAssumer{}, &AwsIamAssumer{}}

// RegisterAssumer allows assumers to be registered when using this library as a package in other projects
// position = -1 will append the assumer
Expand Down

0 comments on commit 2add2a1

Please sign in to comment.