Skip to content

Write aws creds file #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 21, 2022
Merged
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ Also see the CLI's online help `$ okta-aws-cli --help`
| AWS Session Duration (optional) | AWS_SESSION_DURATION | `--session-duration [value]` | The lifetime, in seconds, of the AWS credentials. Must be between 60 and 43200. |
| Output format (optional) | FORMAT | `--format [value]` | Default is `env-var`. Options: `env-var` for output to environment variables, `aws-credentials` for output to AWS credentials file |
| Profile (optional) | PROFILE | `--profile [value]` | Default is `default` |
| Display QR Code (optional) | QR_CODE | `--qr-code` | `yes` if flag is present |
| Automatically open the activation URL with the system web browser (optional) | OPEN_BROWSER | `--open-browser` | `yes` if flag is present |
| Display QR Code (optional) | QR_CODE | `--qr-code` | `true` if flag is present |
| Automatically open the activation URL with the system web browser (optional) | OPEN_BROWSER | `--open-browser` | `true` if flag is present |
| Alternate AWS credentials file path (optional) | AWS_CREDENTIALS | `--aws-credentials` | Path to alternative credentials file other than AWS CLI default |
| Write to the AWS credentials file (optional). Default formatting is to append and not modify the file beyond adding new lines. WARNING: When enabled, writing can inadvertantly remove dangling comments and extraneous formatting from the creds file. | WRITE_AWS_CREDENTIALS | `--write-aws-credentials` | `true` if flag is present |

### Allowed Web SSO Client

Expand Down
7 changes: 7 additions & 0 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ func init() {
usage: "Automatically open the activation URL with the system web browser",
envVar: "OPEN_BROWSER",
},
{
name: "write-aws-credentials",
short: "z",
value: false,
usage: fmt.Sprintf("Write the created/updated profile to the %q file. WARNING: This can inadvertantly remove dangling comments and extraneous formatting from the creds file.", awsCredentialsFilename),
envVar: "WRITE_AWS_CREDENTIALS",
},
}
}

Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ require (
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mattn/go-isatty v0.0.16
github.com/mdp/qrterminal v1.0.1
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.13.0
github.com/stretchr/testify v1.8.0
github.com/tidwall/pretty v1.2.0
golang.org/x/net v0.0.0-20220907135653-1e95f45603a7
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
gopkg.in/ini.v1 v1.67.0
)

require (
Expand All @@ -39,7 +41,6 @@ require (
github.com/subosito/gotenv v1.4.1 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/qr v0.2.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwb
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
6 changes: 3 additions & 3 deletions internal/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package aws

// Credential Convenience representation of an AWS credential.
type Credential struct {
AccessKeyID string
SecretAccessKey string
SessionToken string
AccessKeyID string `ini:"aws_access_key_id"`
SecretAccessKey string `ini:"aws_secret_access_key"`
SessionToken string `ini:"aws_session_token"`
}
51 changes: 28 additions & 23 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,19 @@ const (

// Config A config object for the CLI
type Config struct {
OrgDomain string
OIDCAppID string
FedAppID string
AWSIAMIdP string
AWSIAMRole string
AWSSessionDuration int64
Format string
Profile string
QRCode bool
AWSCredentials string
OpenBrowser bool
HTTPClient *http.Client
OrgDomain string
OIDCAppID string
FedAppID string
AWSIAMIdP string
AWSIAMRole string
AWSSessionDuration int64
Format string
Profile string
QRCode bool
AWSCredentials string
WriteAWSCredentials bool
OpenBrowser bool
HTTPClient *http.Client
}

// NewConfig Creates a new config gathering values in this order of precedence:
Expand All @@ -53,17 +54,18 @@ type Config struct {
// 3. .env file
func NewConfig() *Config {
cfg := Config{
OrgDomain: viper.GetString("org-domain"),
OIDCAppID: viper.GetString("oidc-client-id"),
FedAppID: viper.GetString("aws-acct-fed-app-id"),
AWSIAMIdP: viper.GetString("aws-iam-idp"),
AWSIAMRole: viper.GetString("aws-iam-role"),
AWSSessionDuration: viper.GetInt64("session-duration"),
Format: viper.GetString("format"),
Profile: viper.GetString("profile"),
QRCode: viper.GetBool("qr-code"),
OpenBrowser: viper.GetBool("open-browser"),
AWSCredentials: viper.GetString("aws-credentials"),
OrgDomain: viper.GetString("org-domain"),
OIDCAppID: viper.GetString("oidc-client-id"),
FedAppID: viper.GetString("aws-acct-fed-app-id"),
AWSIAMIdP: viper.GetString("aws-iam-idp"),
AWSIAMRole: viper.GetString("aws-iam-role"),
AWSSessionDuration: viper.GetInt64("session-duration"),
Format: viper.GetString("format"),
Profile: viper.GetString("profile"),
QRCode: viper.GetBool("qr-code"),
OpenBrowser: viper.GetBool("open-browser"),
AWSCredentials: viper.GetString("aws-credentials"),
WriteAWSCredentials: viper.GetBool("write-aws-credentials"),
}
if cfg.Format == "" {
cfg.Format = "env-var"
Expand Down Expand Up @@ -108,6 +110,9 @@ func NewConfig() *Config {
if viper.GetString(awsCrentials) != "" {
cfg.AWSCredentials = viper.GetString(awsCrentials)
}
if !cfg.WriteAWSCredentials {
cfg.WriteAWSCredentials = viper.GetBool("write_aws_credentials")
}

tr := &http.Transport{
IdleConnTimeout: 30 * time.Second,
Expand Down
84 changes: 81 additions & 3 deletions internal/output/aws_credentials_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,70 @@ package output
import (
"fmt"
"os"
"path/filepath"

"github.com/okta/okta-aws-cli/internal/aws"
"github.com/okta/okta-aws-cli/internal/config"
"github.com/pkg/errors"
"gopkg.in/ini.v1"
)

// ensureConfigExists verify that the config file exists
func ensureConfigExists(filename string, profile string) error {
if _, err := os.Stat(filename); err != nil {
if errors.Is(err, os.ErrNotExist) {
dir := filepath.Dir(filename)

// create the aws config dir
err = os.MkdirAll(dir, os.ModePerm)
if err != nil {
return errors.Wrapf(err, "unable to create AWS credentials directory %q", dir)
}

// create an base config file
err = os.WriteFile(filename, []byte("["+profile+"]"), 0o600)
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Created credentials file %q with profile %q.\n", filename, profile)
return nil
}
return err
}
return nil
}

func saveProfile(filename, profile string, awsCreds *aws.Credential) error {
config, err := updateConfig(filename, profile, awsCreds)
if err != nil {
return err
}

err = config.SaveTo(filename)
if err != nil {
return err
}

fmt.Fprintf(os.Stderr, "Updated profile %q in credentials file %q.\n", profile, filename)
return nil
}

func updateConfig(filename, profile string, awsCreds *aws.Credential) (config *ini.File, err error) {
config, err = ini.Load(filename)
if err != nil {
return
}

iniProfile, err := config.NewSection(profile)
if err != nil {
return
}

err = iniProfile.ReflectFrom(awsCreds)

return
}

// AWSCredentialsFile AWS credentials file output formatter
type AWSCredentialsFile struct{}

Expand All @@ -35,7 +94,15 @@ func NewAWSCredentialsFile() *AWSCredentialsFile {
// Output Satisfies the Outputter interface and appends AWS credentials to
// credentials file.
func (e *AWSCredentialsFile) Output(c *config.Config, ac *aws.Credential) error {
f, err := os.OpenFile(c.AWSCredentials, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if c.WriteAWSCredentials {
return e.writeConfig(c, ac)
}

return e.appendConfig(c, ac)
}

func (e *AWSCredentialsFile) appendConfig(c *config.Config, ac *aws.Credential) error {
f, err := os.OpenFile(c.AWSCredentials, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
if err != nil {
return err
}
Expand All @@ -48,7 +115,6 @@ func (e *AWSCredentialsFile) Output(c *config.Config, ac *aws.Credential) error
aws_access_key_id = %s
aws_secret_access_key = %s
aws_session_token = %s

`
creds = fmt.Sprintf(creds, c.Profile, ac.AccessKeyID, ac.SecretAccessKey, ac.SessionToken)
_, err = f.WriteString(creds)
Expand All @@ -57,7 +123,19 @@ aws_session_token = %s
}
_ = f.Sync()

fmt.Fprintf(os.Stderr, "Wrote profile %q to %s\n", c.Profile, c.AWSCredentials)
fmt.Fprintf(os.Stderr, "Appended profile %q to %s\n", c.Profile, c.AWSCredentials)

return nil
}

func (e *AWSCredentialsFile) writeConfig(c *config.Config, ac *aws.Credential) error {
filename := c.AWSCredentials
profile := c.Profile

err := ensureConfigExists(filename, profile)
if err != nil {
return err
}

return saveProfile(filename, profile, ac)
}
99 changes: 99 additions & 0 deletions internal/output/aws_credentials_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2022-Present, Okta, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package output

import (
"fmt"
"os"
"testing"

"github.com/okta/okta-aws-cli/internal/aws"
"github.com/stretchr/testify/assert"
)

// TestINIFormatCredentialsContent provides a litmus test on how well
// gopkg.in/ini.v1 package renders updates to an aws credential file
// representation. The test won't fail but output a diff as a skip if our
// expections are not met.
//
// At the time this test was written the INI package would trim out extra new
// lines and dangling comments.
func TestINIFormatCredentialsContent(t *testing.T) {
have, err := credsTemplate([]interface{}{"A", "B", "C", "D", "E", "F"})
assert.NoError(t, err)
want, err := credsTemplate([]interface{}{"A", "B", "C", "d", "e", "f"})
assert.NoError(t, err)

f, err := os.CreateTemp("", "test")
filename := f.Name()
defer func() {
_ = os.Remove(filename)
}()
assert.NoError(t, err)
_, err = f.Write([]byte(have))
assert.NoError(t, err)
err = f.Close()
assert.NoError(t, err)

awsCreds := &aws.Credential{
AccessKeyID: "d",
SecretAccessKey: "e",
SessionToken: "f",
}
config, err := updateConfig(filename, "test", awsCreds)
assert.NoError(t, err)

err = config.SaveTo(filename)
assert.NoError(t, err)
result, err := os.ReadFile(filename)
assert.NoError(t, err)

got := string(result)
if got != want {
hr := "-------------------------"
t.Skipf("INI package modified reflected creds beyond our expections.\nExpected:\n%s%s%s\n\nGot:\n%s\n%s%s", hr, want, hr, hr, got, hr)
}
}

func credsTemplate(vars []any) (string, error) {
if len(vars) != 6 {
return "", fmt.Errorf("expected 6 vars got %d", len(vars))
}

template := `
# comment 1

# comment 2
[default]
aws_access_key_id = %s
# comment 3
aws_secret_access_key = %s
aws_session_token = %s


[test]
aws_access_key_id = %s
aws_secret_access_key = %s
aws_session_token = %s


# comment 4
`
template = fmt.Sprintf(template, vars...)

return template, nil
}