diff --git a/cmd/main.go b/cmd/main.go index 4eea625..bea3fae 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -50,9 +50,9 @@ func newRootCmd() (*cobra.Command, error) { } rootCmd.AddCommand(x509CredentialProcessCmd) - oneShotCredentialWriteCmd, err := newOneShotCredentialWrite() + oneShotCredentialWriteCmd, err := newX509CredentialFileCmd() if err != nil { - return nil, fmt.Errorf("initializing one-shot-credential-write command: %w", err) + return nil, fmt.Errorf("initializing x509-credential-file command: %w", err) } rootCmd.AddCommand(oneShotCredentialWriteCmd) @@ -89,10 +89,10 @@ func (f *sharedFlags) addFlags(cmd *cobra.Command) error { return nil } -func newOneShotCredentialWrite() (*cobra.Command, error) { +func newX509CredentialFileCmd() (*cobra.Command, error) { sf := &sharedFlags{} cmd := &cobra.Command{ - Use: "x509-one-shot-credential-write", + Use: "x509-credential-file", Short: ``, Long: ``, RunE: func(cmd *cobra.Command, args []string) error { @@ -109,6 +109,40 @@ func newOneShotCredentialWrite() (*cobra.Command, error) { if err != nil { return fmt.Errorf("fetching x509 context: %w", err) } + svid := x509Ctx.DefaultSVID() + slog.Debug( + "Fetched X509 SVID", + slog.Group("svid", + "spiffe_id", svid.ID, + "hint", svid.Hint, + ), + ) + + signer := &awsspiffe.X509SVIDSigner{ + SVID: svid, + } + signatureAlgorithm, err := signer.SignatureAlgorithm() + if err != nil { + return fmt.Errorf("getting signature algorithm: %w", err) + } + credentials, err := vendoredaws.GenerateCredentials(&vendoredaws.CredentialsOpts{ + RoleArn: sf.roleARN, + ProfileArnStr: sf.profileARN, + Region: sf.region, + RoleSessionName: sf.roleSessionName, + TrustAnchorArnStr: sf.trustAnchorARN, + SessionDuration: sf.sessionDuration, + }, signer, signatureAlgorithm) + if err != nil { + return fmt.Errorf("generating credentials: %w", err) + } + slog.Debug( + "Generated AWS credentials", + "expiration", credentials.Expiration, + ) + + // Now we write this to disk in the format that the AWS CLI/SDK + // expects for a credentials file. }, // Hidden for now as the daemon is likely more "usable" Hidden: true, diff --git a/go.mod b/go.mod index 50f3d03..c1a96f7 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,16 @@ require ( require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/zeebo/errs v1.3.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.30.0 // indirect @@ -26,4 +30,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 06adbdd..390510f 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= @@ -53,6 +55,8 @@ google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFN google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/aws_credentials_file.go b/internal/aws_credentials_file.go new file mode 100644 index 0000000..19bd903 --- /dev/null +++ b/internal/aws_credentials_file.go @@ -0,0 +1,74 @@ +package internal + +import ( + "fmt" + "log/slog" + "os" + + "gopkg.in/ini.v1" +) + +type AWSCredentialsFileConfig struct { + Path string + ProfileName string + Force bool + ReplaceFile bool +} + +type AWSCredentialsFileProfile struct { + AWSAccessKeyID string + AWSSecretAccessKey string + AWSSessionToken string +} + +func (p AWSCredentialsFileProfile) Validate() error { + // TODO: Validate + return nil +} + +// UpsertAWSCredentialsFileProfile writes the provided AWS credentials profile to the AWS credentials file. +// See https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html +func UpsertAWSCredentialsFileProfile( + log *slog.Logger, + cfg AWSCredentialsFileConfig, + p AWSCredentialsFileProfile, +) error { + if err := p.Validate(); err != nil { + return fmt.Errorf("validating aws credentials file profile: %w", err) + } + + f, err := ini.Load(cfg.Path) + if err != nil { + if !os.IsNotExist(err) { + if !cfg.Force { + log.Error( + "When loading the existing AWS credentials file, an error occurred. Use --force to ignore errors and attempt to overwrite.", + "error", err, + "path", cfg.Path, + ) + return fmt.Errorf("loading existing aws credentials file: %w", err) + } + log.Warn( + "When loading the existing AWS credentials file, an error occurred. As --force is set, the file will be overwritten.", + "error", err, + "path", cfg.Path, + ) + } + f = ini.Empty() + } + + sectionName := "default" + if cfg.ProfileName != "" { + sectionName = cfg.ProfileName + } + sec := f.Section(sectionName) + + sec.Key("aws_secret_access_key").SetValue(p.AWSSecretAccessKey) + sec.Key("aws_access_key_id").SetValue(p.AWSAccessKeyID) + sec.Key("aws_session_token").SetValue(p.AWSSessionToken) + + if err := f.SaveTo(cfg.Path); err != nil { + return fmt.Errorf("saving aws credentials file: %w", err) + } + return nil +} diff --git a/internal/aws_credentials_file_test.go b/internal/aws_credentials_file_test.go new file mode 100644 index 0000000..df40a96 --- /dev/null +++ b/internal/aws_credentials_file_test.go @@ -0,0 +1,69 @@ +package internal + +import ( + "log/slog" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAWSCredentialsFile_Write(t *testing.T) { + // TODO: Add more cases: + // - If file exists, but is a bad ini and Force moe + // - Replace mode + tmp := t.TempDir() + configPath := filepath.Join(tmp, "config") + log := slog.Default() + + err := UpsertAWSCredentialsFileProfile( + log, + AWSCredentialsFileConfig{ + Path: configPath, + }, + AWSCredentialsFileProfile{ + AWSAccessKeyID: "1234567890", + AWSSecretAccessKey: "abcdefgh", + AWSSessionToken: "ijklmnop", + }, + ) + require.NoError(t, err) + + got, err := os.ReadFile(configPath) + require.NoError(t, err) + + require.Equal(t, `[default] +aws_secret_access_key = abcdefgh +aws_access_key_id = 1234567890 +aws_session_token = ijklmnop +`, string(got)) + + t.Run("bad file", func(t *testing.T) { + tmp := t.TempDir() + configPath := filepath.Join(tmp, "config") + require.NoError(t, os.WriteFile(configPath, []byte("bad ini"), 0600)) + err := UpsertAWSCredentialsFileProfile( + log, + AWSCredentialsFileConfig{ + Path: configPath, + Force: true, + }, + AWSCredentialsFileProfile{ + AWSAccessKeyID: "1234567890", + AWSSecretAccessKey: "abcdefgh", + AWSSessionToken: "ijklmnop", + }, + ) + require.NoError(t, err) + + got, err := os.ReadFile(configPath) + require.NoError(t, err) + + require.Equal(t, `[default] +aws_secret_access_key = abcdefgh +aws_access_key_id = 1234567890 +aws_session_token = ijklmnop +`, string(got)) + }) +}