Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion pkg/cfaws/cred_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import (

// ExportCredsToProfile will write assumed credentials to ~/.aws/credentials with a specified profile name header
func ExportCredsToProfile(profileName string, creds aws.Credentials) error {
return ExportCredsToProfileWithOptions(profileName, creds, true)
}

// ExportCredsToProfileWithOptions writes assumed credentials to a profile and
// optionally applies the configured export credential suffix.
func ExportCredsToProfileWithOptions(profileName string, creds aws.Credentials, applySuffix bool) error {
// fetch the parsed cred file
credPath := GetAWSCredentialsPath()

Expand Down Expand Up @@ -45,7 +51,7 @@ func ExportCredsToProfile(profileName string, creds aws.Credentials) error {
return err
}

if cfg.ExportCredentialSuffix != nil && *cfg.ExportCredentialSuffix!= "" {
if applySuffix && cfg.ExportCredentialSuffix != nil && *cfg.ExportCredentialSuffix != "" {
profileName = profileName + "-" + *cfg.ExportCredentialSuffix
}

Expand Down
85 changes: 85 additions & 0 deletions pkg/cfaws/cred_exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cfaws

import (
"os"
"path/filepath"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
grantedconfig "github.com/fwdcloudsec/granted/pkg/config"
"github.com/stretchr/testify/assert"
"gopkg.in/ini.v1"
)

func TestExportCredsToProfileWithOptions_AppliesSuffix(t *testing.T) {
tests := []struct {
name string
sourceProfileName string
applySuffix bool
expectedSection string
unexpectedSection string
}{
{
name: "applies configured suffix",
sourceProfileName: "source-profile",
applySuffix: true,
expectedSection: "source-profile-team",
unexpectedSection: "source-profile",
},
{
name: "skips suffix with custom export name",
sourceProfileName: "renamed-profile",
applySuffix: false,
expectedSection: "renamed-profile",
unexpectedSection: "renamed-profile-team",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpHome := t.TempDir()
tmpCredsPath := filepath.Join(tmpHome, "aws-credentials")
t.Setenv("HOME", tmpHome)
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", tmpCredsPath)

suffix := "team"
writeGrantedConfigForTest(t, &suffix)

err := ExportCredsToProfileWithOptions(tt.sourceProfileName, testCreds(), tt.applySuffix)
assert.NoError(t, err)

credentialsFile, err := ini.Load(tmpCredsPath)
assert.NoError(t, err)

section, err := credentialsFile.GetSection(tt.expectedSection)
assert.NoError(t, err)
assert.Equal(t, "AKIA_TEST", section.Key("aws_access_key_id").String())
assert.Equal(t, "secret_test", section.Key("aws_secret_access_key").String())
assert.Equal(t, "token_test", section.Key("aws_session_token").String())

_, err = credentialsFile.GetSection(tt.unexpectedSection)
assert.Error(t, err)
})
}
}

func writeGrantedConfigForTest(t *testing.T, suffix *string) {
t.Helper()

grantedFolder := filepath.Join(os.Getenv("HOME"), ".dgranted")
err := os.MkdirAll(grantedFolder, 0700)
assert.NoError(t, err)

cfg := grantedconfig.NewDefaultConfig()
cfg.ExportCredentialSuffix = suffix
err = cfg.Save()
assert.NoError(t, err)
}

func testCreds() aws.Credentials {
return aws.Credentials{
AccessKeyID: "AKIA_TEST",
SecretAccessKey: "secret_test",
SessionToken: "token_test",
}
}
12 changes: 11 additions & 1 deletion pkg/cfaws/granted_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,18 @@ func hasGrantedSSOPrefix(rawConfig *ini.Section) bool {
// also check whether the provided flag to 'granted credential-process --profile pname'
// matches the AWS config profile name. If it doesn't then return an err
// as the user will certainly run into unexpected behaviour.
//
// The credential_process value may reference the granted/dgranted binary
// either as a bare command (relying on $PATH resolution) or via a relative
// or absolute path (e.g. '/home/user/.local/bin/granted'). This allows users
// to write the full path to the binary, which is useful in environments
// where the process invoking the AWS SDK doesn't share the same $PATH as
// the interactive shell (IDEs, GUI apps, containers, cron jobs, etc.).
// Only the last path segment (the binary name, ignoring any directory
// prefix and an optional Windows '.exe' suffix) is checked against
// 'granted'/'dgranted'.
func validateCredentialProcess(arg string, awsProfileName string) error {
regex := regexp.MustCompile(`^(\s+)?(dgranted|granted)\s+credential-process.*--profile\s+(?P<PName>([^\s]+))`)
regex := regexp.MustCompile(`^(\s+)?(?:\S*[\\/])?(dgranted|granted)(\.exe)?\s+credential-process.*--profile\s+(?P<PName>([^\s]+))`)

if regex.MatchString(arg) {
matches := regex.FindStringSubmatch(arg)
Expand Down
86 changes: 84 additions & 2 deletions pkg/cfaws/granted_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,98 @@ func TestValidateCredentialProcess(t *testing.T) {
profileName: "apple",
wantErr: "unable to parse 'credential_process'. Looks like your credential_process isn't configured correctly. \n You need to add 'granted credential-process --profile <profile-name>'",
},
{
name: "valid argument using dgranted bare command",
arg: "dgranted credential-process --profile develop",
profileName: "develop",
},
{
name: "valid argument with relative path",
arg: "./granted credential-process --profile develop",
profileName: "develop",
},
{
name: "valid argument with relative path in subdirectory",
arg: "bin/dgranted credential-process --profile develop",
profileName: "develop",
},
{
name: "valid argument with absolute unix path",
arg: "/home/user/.local/bin/granted credential-process --profile develop",
profileName: "develop",
},
{
name: "valid argument with absolute unix path and profile containing a slash",
arg: "/home/user/.local/bin/granted credential-process --profile my-account/MyRole",
profileName: "my-account/MyRole",
},
{
name: "valid argument with absolute unix path to dgranted",
arg: "/home/user/.local/bin/dgranted credential-process --profile develop",
profileName: "develop",
},
{
name: "valid argument with windows path and .exe suffix",
arg: `C:\Users\foo\bin\granted.exe credential-process --profile develop`,
profileName: "develop",
},
{
name: "valid argument with windows path and .exe suffix to dgranted",
arg: `C:\Users\foo\bin\dgranted.exe credential-process --profile develop`,
profileName: "develop",
},
{
name: "valid argument with bare .exe suffix",
arg: "granted.exe credential-process --profile develop",
profileName: "develop",
},
{
name: "full path with mismatched profile name still detected",
arg: "/home/user/.local/bin/granted credential-process --profile abc",
profileName: "develop",
wantErr: "unmatched profile names. The profile name 'abc' provided to 'granted credential-process' does not match AWS profile name 'develop'",
},
{
name: "rejects an unrelated binary invoked via an absolute path",
arg: "/usr/local/bin/aws-vault credential-process --profile develop",
profileName: "develop",
wantErr: "unable to parse 'credential_process'. Looks like your credential_process isn't configured correctly. \n You need to add 'granted credential-process --profile <profile-name>'",
},
{
name: "rejects a binary that merely starts with 'granted'",
arg: "/usr/local/bin/granted-wrapper credential-process --profile develop",
profileName: "develop",
wantErr: "unable to parse 'credential_process'. Looks like your credential_process isn't configured correctly. \n You need to add 'granted credential-process --profile <profile-name>'",
},
{
name: "rejects a bare command that merely starts with 'granted'",
arg: "grantedx credential-process --profile develop",
profileName: "develop",
wantErr: "unable to parse 'credential_process'. Looks like your credential_process isn't configured correctly. \n You need to add 'granted credential-process --profile <profile-name>'",
},
{
name: "missing profile name",
arg: "granted credential-process",
profileName: "develop",
wantErr: "unable to parse 'credential_process'. Looks like your credential_process isn't configured correctly. \n You need to add 'granted credential-process --profile <profile-name>'",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

err := validateCredentialProcess(tt.arg, tt.profileName)
if err != nil {
if err.Error() != tt.wantErr {
if tt.wantErr == "" {
if err != nil {
t.Fatal(err)
}
return
}
if err == nil {
t.Fatalf("expected error %q, got nil", tt.wantErr)
}
if err.Error() != tt.wantErr {
t.Fatal(err)
}
})
}
Expand Down
69 changes: 69 additions & 0 deletions pkg/granted/sso.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"os"
"regexp"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/aws/aws-sdk-go-v2/aws"
Expand Down Expand Up @@ -56,6 +57,7 @@ var GenerateCommand = cli.Command{
&cli.StringFlag{Name: "default-region", Usage: "Set the 'region' key on generated profiles (can differ from the SSO region)"},
&cli.StringSliceFlag{Name: "source", Usage: "The sources to load AWS profiles from (valid values are: 'aws-sso')", Value: cli.NewStringSlice("aws-sso")},
&cli.BoolFlag{Name: "no-credential-process", Usage: "Generate profiles without the Granted credential-process integration"},
&cli.BoolFlag{Name: "credential-process-full-path", Usage: "Use the full path to the current granted binary in generated credential_process entries, instead of relying on $PATH"},
&cli.StringFlag{Name: "profile-template", Usage: "Specify profile name template", Value: awsconfigfile.DefaultProfileNameTemplate},
&cli.StringFlag{Name: "sso-browser-profile", Usage: "Use a pre-existing profile in your browser for SSO login", EnvVars: []string{"GRANTED_SSO_BROWSER_PROFILE"}},
&cli.BoolFlag{Name: "use-device-code", Usage: "Force device code flow even if authorization code with PKCE is enabled"},
Expand Down Expand Up @@ -102,6 +104,14 @@ var GenerateCommand = cli.Command{
return err
}

var binaryPath string
if c.Bool("credential-process-full-path") {
binaryPath, err = resolveCredentialProcessBinaryPath()
if err != nil {
return err
}
}

g := awsconfigfile.Generator{
Config: ini.Empty(),
ProfileNameTemplate: profileNameTemplate,
Expand All @@ -125,6 +135,9 @@ var GenerateCommand = cli.Command{
if err != nil {
return err
}
if binaryPath != "" {
applyCredentialProcessBinaryPath(g.Config, binaryPath)
}

_, err = g.Config.WriteTo(os.Stdout)
if err != nil {
Expand All @@ -149,6 +162,7 @@ var PopulateCommand = cli.Command{
&cli.BoolFlag{Name: "prune", Usage: "Remove any generated profiles with the 'common_fate_generated_from' key which no longer exist"},
&cli.StringFlag{Name: "profile-template", Usage: "Specify profile name template", Value: awsconfigfile.DefaultProfileNameTemplate},
&cli.BoolFlag{Name: "no-credential-process", Usage: "Generate profiles without the Granted credential-process integration"},
&cli.BoolFlag{Name: "credential-process-full-path", Usage: "Use the full path to the current granted binary in generated credential_process entries, instead of relying on $PATH"},
&cli.StringFlag{Name: "sso-browser-profile", Usage: "Use a pre-existing profile in your browser for SSO login", EnvVars: []string{"GRANTED_SSO_BROWSER_PROFILE"}},
&cli.BoolFlag{Name: "use-device-code", Usage: "Force device code flow even if authorization code with PKCE is enabled"},
},
Expand Down Expand Up @@ -224,6 +238,14 @@ var PopulateCommand = cli.Command{
pruneStartURLs = []string{startURL}
}

var binaryPath string
if c.Bool("credential-process-full-path") {
binaryPath, err = resolveCredentialProcessBinaryPath()
if err != nil {
return err
}
}

g := awsconfigfile.Generator{
Config: config,
ProfileNameTemplate: profileNameTemplate,
Expand All @@ -247,6 +269,9 @@ var PopulateCommand = cli.Command{
if err != nil {
return err
}
if binaryPath != "" {
applyCredentialProcessBinaryPath(config, binaryPath)
}

err = config.SaveTo(configFilename)
if err != nil {
Expand Down Expand Up @@ -536,3 +561,47 @@ func resolveDefaultRegion(flagValue, configValue string) (string, error) {
}
return expanded, nil
}

// resolveCredentialProcessBinaryPath resolves the full path to the currently
// running granted binary, for use in generated credential_process entries
// (see the --credential-process-full-path flag).
//
// This is useful in environments where the process invoking the AWS SDK
// doesn't have the same $PATH as the interactive shell (IDEs, GUI apps,
// containers, cron jobs, etc.), since a bare 'granted' command would
// otherwise fail to be found.
//
// We deliberately do not resolve symlinks (e.g. via filepath.EvalSymlinks)
// here, so that a stable symlink path (such as the shims created by tools
// like asdf or mise) keeps working after the underlying granted binary is
// upgraded.
func resolveCredentialProcessBinaryPath() (string, error) {
binaryPath, err := os.Executable()
if err != nil {
return "", fmt.Errorf("unable to resolve the full path to the current granted binary: %w", err)
}
return binaryPath, nil
}

func applyCredentialProcessBinaryPath(config *ini.File, binaryPath string) {
const generatedFrom = "common_fate_generated_from"
const credentialProcess = "credential_process"
const defaultCommand = "granted credential-process"

for _, section := range config.Sections() {
generatedFromKey, err := section.GetKey(generatedFrom)
if err != nil || generatedFromKey.String() != "aws-sso" {
continue
}

key, err := section.GetKey(credentialProcess)
if err != nil {
continue
}

remainder, ok := strings.CutPrefix(key.String(), defaultCommand)
if ok {
key.SetValue(binaryPath + " credential-process" + remainder)
}
}
}
Loading