Skip to content

Commit

Permalink
Manage static API creds
Browse files Browse the repository at this point in the history
* Update golangci-lint to latest version
* Add awsconfig module
* Add basic add/delete/list commands for managing static AWS
    API creds.
* Update secure store to support static creds
* Better detect invalid AWS AccountIDs

Refs: #240
  • Loading branch information
synfinatic committed Apr 29, 2022
1 parent a19d2ab commit b474165
Show file tree
Hide file tree
Showing 24 changed files with 977 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
uses: golangci/golangci-lint-action@v2
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v1.43.0
version: v1.45.2

# Optional: working directory, useful for monorepos
# working-directory: somedir
Expand Down
137 changes: 137 additions & 0 deletions awsconfig/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package awsconfig

/*
* AWS SSO CLI
* Copyright (c) 2021-2022 Aaron Turner <synfinatic at gmail dot com>
*
* This program is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or with the authors permission any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import (
"fmt"
// "os"
"strings"

"github.com/synfinatic/aws-sso-cli/storage"
"github.com/synfinatic/aws-sso-cli/utils"
"gopkg.in/ini.v1"
)

const (
CONFIG_FILE = "~/.aws/config"
CREDENTIALS_FILE = "~/.aws/credentials" // #nosec
)

type AwsConfig struct {
ConfigFile string
Config *ini.File
CredentialsFile string
Credentials *ini.File
Profiles map[string]map[string]interface{} // profile.go
}

// NewAwsConfig creates a new *AwsConfig struct
func NewAwsConfig(config, credentials string) (*AwsConfig, error) {
var err error
a := AwsConfig{}
p := utils.GetHomePath(config)

if a.Config, err = ini.Load(p); err != nil {
return nil, fmt.Errorf("unable to open %s: %s", p, err.Error())
} else {
a.ConfigFile = config
}

p = utils.GetHomePath(credentials)
if a.Credentials, err = ini.Load(p); err == nil {
a.CredentialsFile = p
}

return &a, nil
}

// StaticProfiles returns a list of all the profiles with static API creds
// stored in ~/.aws/config and ~/.aws/credentials
func (a *AwsConfig) StaticProfiles() ([]Profile, error) {
profiles := []Profile{}
creds := a.Credentials

for _, profile := range a.Config.Sections() {
x := strings.Split(profile.Name(), " ")
if x[0] != "profile" || len(x) != 2 {
log.Errorf("invalid profile: %s", profile.Name())
continue
}

if HasStaticCreds(profile) {
log.Debugf("Found api keys for %s with in config file", x[1])
profiles = append(profiles,
Profile{
Name: x[1],
AccessKeyId: profile.Key("aws_access_key_id").String(),
SecretAccessKey: profile.Key("aws_secret_access_key").String(),
FromConfig: true,
})
} else if creds != nil {
if cp, err := creds.GetSection(x[1]); err == nil {
if cp != nil && HasStaticCreds(cp) {
log.Debugf("Found api keys for %s in credentials file", x[1])
profiles = append(profiles,
Profile{
Name: x[1],
AccessKeyId: cp.Key("aws_access_key_id").String(),
SecretAccessKey: cp.Key("aws_secret_access_key").String(),
FromConfig: false,
})
}
}
} else {
log.Errorf("skipping because no credentials file")
}
}
return profiles, nil
}

// UpdateSecureStore writes any new role ARN credentials to the provided SecureStorage
func (a *AwsConfig) UpdateSecureStore(store storage.SecureStorage) error {
profiles, err := a.StaticProfiles()
if err != nil {
return err
}

for _, p := range profiles {
arn, err := p.GetArn()
if err != nil {
return err
}
accountid, rolename, _ := utils.ParseRoleARN(arn)

creds := storage.StaticCredentials{
RoleName: rolename,
AccountId: accountid,
AccessKeyId: p.AccessKeyId,
SecretAccessKey: p.SecretAccessKey,
}
if err = store.SaveStaticCredentials(arn, creds); err != nil {
return err
}
}
return nil
}

// Write updates the AWS ~/.aws/config file to use aws-sso via a credential_process
// and removes the associated ~/.aws/credentials entries
func (a *AwsConfig) Write() error {
return nil
}
35 changes: 35 additions & 0 deletions awsconfig/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package awsconfig

/*
* AWS SSO CLI
* Copyright (c) 2021-2022 Aaron Turner <synfinatic at gmail dot com>
*
* This program is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or with the authors permission any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import (
"github.com/sirupsen/logrus"
)

var log *logrus.Logger

func SetLogger(l *logrus.Logger) {
log = l
}

/*
func GetLogger() *logrus.Logger {
return log
}
*/
114 changes: 114 additions & 0 deletions awsconfig/profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package awsconfig

/*
* AWS SSO CLI
* Copyright (c) 2021-2022 Aaron Turner <synfinatic at gmail dot com>
*
* This program is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or with the authors permission any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import (
"context"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/sts"
)

type Profile struct {
// Required for import of static creds
FromConfig bool
Name string
AccessKeyId string `ini:"aws_access_key_id"`
SecretAccessKey string `ini:"aws_secret_access_key"`
MfaSerial string `ini:"mfa_serial"`
// optional
/*
Region string `ini:"region"`
Output string `ini:"output"`
CABundle string `ini:"ca_bundle"`
CliAutoPrompt string `ini:"cli_auto_prompt"`
CliBinaryFormat string `ini:"cli_binary_format"`
CliPager string `ini:"cli_pager"`
CliTimestampFormat string `ini:"cli_timestamp_format"`
CredentialProcess string `ini:"credential_process"`
CredentialSource string `ini:"credential_source"`
DurationSeconds uint32 `ini:"duration_seconds"`
ExternalId string `ini:"external_id"`
MaxAttempts uint32 `ini:"max_attempts"`
ParameterValidation bool `ini:"parameter_validation"`
RetryMode string `ini:"retry_mode"`
RoleArn string `ini:"role_arn"`
RoleSessionName string `ini:"role_session_name"`
SourceProfile string `ini:"source_profile"`
SsoAccountId int64 `ini:"sso_account_id"`
SsoRegion string `ini:"sso_region"`
SsoRoleName string `ini:"sso_role_name"`
SsoStartUrl string `ini:"sso_start_url"`
StsRegionEndpoints string `ini:"sts_region_endpoints"`
WebIdentityTokenFile string `ini:"web_identity_token_file"`
TcpKeepalive bool `ini:"tcp_keepalive"`
*/
}

func (p *Profile) config() (aws.Config, error) {
creds := credentials.NewStaticCredentialsProvider(
p.AccessKeyId,
p.SecretAccessKey,
"", // sessionToken
)
return config.LoadDefaultConfig(
context.TODO(),
config.WithRegion("us-east-1"),
config.WithCredentialsProvider(creds),
)
}

// GetArn uses the credentials to call sts:GetCallerIdentity to retrieve the ARN
func (p *Profile) GetArn() (string, error) {
cfg, err := p.config()
if err != nil {
return "", err
}

s := sts.NewFromConfig(cfg)
out, err := s.GetCallerIdentity(context.TODO(), &sts.GetCallerIdentityInput{})
if err != nil {
return "", err
}

return aws.ToString(out.Arn), nil
}

// GetAccountAlias calls iam:ListAccountAliases to retrieve the AWS Account
// Alias. Returns an empty string if no alias has been set.
func (p *Profile) GetAccountAlias() string {
cfg, err := p.config()
if err != nil {
return ""
}

i := iam.NewFromConfig(cfg)
out, err := i.ListAccountAliases(context.TODO(), &iam.ListAccountAliasesInput{})
if err != nil {
return ""
}

if len(out.AccountAliases) > 0 {
return out.AccountAliases[0]
}
return ""
}
41 changes: 41 additions & 0 deletions awsconfig/section.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package awsconfig

/*
* AWS SSO CLI
* Copyright (c) 2021-2022 Aaron Turner <synfinatic at gmail dot com>
*
* This program is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or with the authors permission any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import (
"gopkg.in/ini.v1"
)

func HasStaticCreds(s *ini.Section) bool {
aws_access_key_id := false
aws_secret_access_key := false
for _, key := range s.KeyStrings() {
switch key {
case "aws_access_key_id":
aws_access_key_id = true
case "aws_secret_access_key":
aws_secret_access_key = true
case "mfa_serial":
// we don't support prompting for MFA so don't import them
log.Infof("Skipping MFA enabled profile: %s", s.Name())
return false
}
}
return aws_access_key_id && aws_secret_access_key
}
Loading

0 comments on commit b474165

Please sign in to comment.