Skip to content

Commit

Permalink
Add subcommand for writing credentials to file (#4)
Browse files Browse the repository at this point in the history
* add subcommand for writing credentials to file

* clean up and bump version
  • Loading branch information
patricksanders authored Oct 16, 2020
1 parent 2baf8f1 commit 1250e28
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 25 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor)
BINARY_NAME=weep
VERSION=0.1.0
VERSION=0.1.1
REGISTRY=$(REGISTRY)
BRANCH=$(shell git rev-parse --abbrev-ref HEAD)

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,23 @@ eval $(weep export fullOrPartialRoleName)

Then run `aws sts get-caller-identity` to confirm that your credentials work properly.

### Credentials file

Write retrieved credentials to an AWS credentials file (`~/.aws/credentials` by default with the profile name `consoleme`).

```bash
weep file exampleRole

# you can also specify a profile name
weep file stagingRole --profile staging
weep file prodRole --profile prod

# or you can save it to a different place
weep file exampleRole -o /tmp/credentials
```

Weep will do its best to preserve existing credentials in the file (but it will overwrite a conflicting profile name, so be careful!).

## Building

In most cases, `weep` can be built by running the `make` command in the repository root. `make release` (requires
Expand Down
72 changes: 50 additions & 22 deletions challenge/challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"github.com/golang/glog"
"github.com/netflix/weep/config"
"github.com/netflix/weep/util"
log "github.com/sirupsen/logrus"
"io/ioutil"
"net/http"
Expand All @@ -26,13 +27,21 @@ func NewHTTPClient(consolemeUrl string) (*http.Client, error) {
}
var challenge ConsolemeChallengeResponse
jar, err := cookiejar.New(&cookiejar.Options{})
if err != nil { return nil, err }
if err != nil {
return nil, err
}
credentialsPath, err := getCredentialsPath()
if err != nil { return nil, err }
if err != nil {
return nil, err
}
challengeBody, err := ioutil.ReadFile(credentialsPath)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
err = json.Unmarshal(challengeBody, &challenge)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
cookies := []*http.Cookie{{
Name: challenge.CookieName,
Value: challenge.EncodedJwt,
Expand All @@ -43,23 +52,22 @@ func NewHTTPClient(consolemeUrl string) (*http.Client, error) {
},
}
consoleMeUrlParsed, err := url.Parse(consolemeUrl)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
jar.SetCookies(consoleMeUrlParsed, cookies)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
client := &http.Client{
Jar: jar,
}

return client, err
}

func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

func isWSL() bool {
if fileExists("/proc/sys/kernel/osrelease") {
if util.FileExists("/proc/sys/kernel/osrelease") {
if osrelease, err := ioutil.ReadFile("/proc/sys/kernel/osrelease"); err == nil {
if strings.Contains(strings.ToLower(string(osrelease)), "microsoft") {
return true
Expand All @@ -75,7 +83,9 @@ func poll(pollingUrl string) (*ConsolemeChallengeResponse, error) {
timeout := time.After(2 * time.Minute)
tick := time.Tick(3 * time.Second)
req, err := http.NewRequest("GET", pollingUrl, nil)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
// Keep trying until we're timed out or got a result or got an error
Expand All @@ -85,12 +95,16 @@ func poll(pollingUrl string) (*ConsolemeChallengeResponse, error) {
return nil, errors.New("*** Unable to validate Challenge Response after 2 minutes. Quitting. ***")
case <-tick:
resp, err := client.Do(req)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
pollResponseBody, err = ioutil.ReadAll(resp.Body)
err := json.Unmarshal(pollResponseBody, &pollResponse)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
if pollResponse.Status == "success" {
return &pollResponse, nil
}
Expand All @@ -101,7 +115,9 @@ func poll(pollingUrl string) (*ConsolemeChallengeResponse, error) {

func getCredentialsPath() (string, error) {
currentUser, err := user.Current()
if err != nil { return "", err }
if err != nil {
return "", err
}
weepDir := filepath.Join(currentUser.HomeDir, ".weep")
// Setup the directories where we will be writing credentials
if _, err := os.Stat(weepDir); os.IsNotExist(err) {
Expand Down Expand Up @@ -157,10 +173,14 @@ func RefreshChallenge() error {
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil { return err }
if err != nil {
return err
}
defer resp.Body.Close()
tokenResponseBody, err := ioutil.ReadAll(resp.Body)
if err != nil { return err }
if err != nil {
return err
}

if err := json.Unmarshal(tokenResponseBody, &challenge); err != nil {
return err
Expand Down Expand Up @@ -205,14 +225,22 @@ func RefreshChallenge() error {

// Step 3: Continue polling backend to see if request has been authenticated yet. Poll every 3 seconds for 2 minutes
pollResponse, err := poll(challenge.PollingUrl)
if err != nil { return err }
if err != nil {
return err
}

jsonPollResponse, err := json.Marshal(pollResponse)
if err != nil { return err }
if err != nil {
return err
}

credentialsPath, err := getCredentialsPath()
if err != nil { return err }
if err != nil {
return err
}
err = ioutil.WriteFile(credentialsPath, jsonPollResponse, 0600)
if err != nil { return err }
if err != nil {
return err
}
return nil
}
87 changes: 87 additions & 0 deletions cmd/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package cmd

import (
"fmt"
"github.com/mitchellh/go-homedir"
"github.com/netflix/weep/consoleme"
"github.com/netflix/weep/util"
"github.com/spf13/cobra"
"gopkg.in/ini.v1"
"os"
"path"
)

var (
fileDestination string
fileNoIPRestrict bool
fileProfileName string
fileRole string
)

func init() {
fileCmd.PersistentFlags().BoolVarP(&fileNoIPRestrict, "no-ip", "n", false, "remove IP restrictions")
fileCmd.PersistentFlags().StringVarP(&fileDestination, "output", "o", getDefaultCredentialsFile(), "output file for credentials")
fileCmd.PersistentFlags().StringVarP(&fileProfileName, "profile", "p", "consoleme", "profile name")
rootCmd.AddCommand(fileCmd)
}

var fileCmd = &cobra.Command{
Use: "file [role_name]",
Short: "retrieve credentials and save them to a credentials file",
Args: cobra.ExactArgs(1),
RunE: runFile,
}

func runFile(cmd *cobra.Command, args []string) error {
fileRole = args[0]
client, err := consoleme.GetClient()
if err != nil {
return err
}
credentials, err := client.GetRoleCredentials(fileRole, fileNoIPRestrict)
if err != nil {
return err
}
err = writeCredentialsFile(credentials)
if err != nil {
return err
}
return nil
}

func getDefaultCredentialsFile() string {
home, err := homedir.Dir()
if err != nil {
fmt.Printf("couldn't get default directory!")
os.Exit(1)
}
return path.Join(home, ".aws", "credentials")
}

func writeCredentialsFile(credentials consoleme.AwsCredentials) error {
var credentialsINI *ini.File
var err error

// Disable pretty format, but still put spaces around `=`
ini.PrettyFormat = false
ini.PrettyEqual = true

if util.FileExists(fileDestination) {
credentialsINI, err = ini.Load(fileDestination)
if err != nil {
return err
}
} else {
credentialsINI = ini.Empty()
}

credentialsINI.Section(fileProfileName).Key("aws_access_key_id").SetValue(credentials.AccessKeyId)
credentialsINI.Section(fileProfileName).Key("aws_secret_access_key").SetValue(credentials.SecretAccessKey)
credentialsINI.Section(fileProfileName).Key("aws_session_token").SetValue(credentials.SessionToken)
err = credentialsINI.SaveTo(fileDestination)
if err != nil {
return err
}

return nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ require (
github.com/spf13/viper v1.7.1
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc // indirect
golang.org/x/sys v0.0.0-20200828081204-131dc92a58d5 // indirect
gopkg.in/ini.v1 v1.60.0 // indirect
gopkg.in/ini.v1 v1.62.0
gopkg.in/yaml.v2 v2.3.0
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.60.0 h1:P5ZzC7RJO04094NJYlEnBdFK2wwmnCAy/+7sAzvWs60=
gopkg.in/ini.v1 v1.60.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
7 changes: 6 additions & 1 deletion util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package util
import (
"errors"
"fmt"
"os"
"strings"

log "github.com/sirupsen/logrus"
Expand All @@ -19,7 +20,6 @@ type AwsArn struct {
ResourceDelimiter string
}


func validate(arn string, pieces []string) error {
if len(pieces) < 6 {
return errors.New("Malformed ARN")
Expand Down Expand Up @@ -68,3 +68,8 @@ func CheckError(err error) {
log.Fatal(err)
}
}

func FileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

0 comments on commit 1250e28

Please sign in to comment.