Skip to content

Commit

Permalink
Create credentialprovider package for getting AWS credentials
Browse files Browse the repository at this point in the history
Signed-off-by: Burak Varlı <[email protected]>
  • Loading branch information
unexge committed Dec 27, 2024
1 parent 7518590 commit bcc72d4
Show file tree
Hide file tree
Showing 10 changed files with 910 additions and 0 deletions.
115 changes: 115 additions & 0 deletions pkg/driver/node/credentialprovider/awsprofile/aws_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Package awsprofile provides utilities for creating and deleting AWS Profile (i.e., credentials & config files).
package awsprofile

import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"unicode"

"github.com/google/renameio"
)

const (
awsProfileName = "s3-csi"
awsProfileConfigFilename = "s3-csi-config"
awsProfileCredentialsFilename = "s3-csi-credentials"
)

// ErrInvalidCredentials is returned when given AWS Credentials contains invalid characters.
var ErrInvalidCredentials = errors.New("aws-profile: Invalid AWS Credentials")

// An AWSProfile represents an AWS profile with it's credentials and config files.
type AWSProfile struct {
Name string
ConfigFilename string
CredentialsFilename string
}

// CreateAWSProfile creates an AWS Profile with credentials and config files from given credentials.
// Created credentials and config files can be clean up with `CleanupAWSProfile`.
func CreateAWSProfile(basepath string, accessKeyID string, secretAccessKey string, sessionToken string, filePerm fs.FileMode) (AWSProfile, error) {
if !isValidCredential(accessKeyID) || !isValidCredential(secretAccessKey) || !isValidCredential(sessionToken) {
return AWSProfile{}, ErrInvalidCredentials
}

name := awsProfileName

configPath := filepath.Join(basepath, awsProfileConfigFilename)
err := writeAWSProfileFile(configPath, configFileContents(name), filePerm)
if err != nil {
return AWSProfile{}, fmt.Errorf("aws-profile: Failed to create config file %s: %v", configPath, err)
}

credentialsPath := filepath.Join(basepath, awsProfileCredentialsFilename)
err = writeAWSProfileFile(credentialsPath, credentialsFileContents(name, accessKeyID, secretAccessKey, sessionToken), filePerm)
if err != nil {
return AWSProfile{}, fmt.Errorf("aws-profile: Failed to create credentials file %s: %v", credentialsPath, err)
}

return AWSProfile{
Name: name,
ConfigFilename: awsProfileConfigFilename,
CredentialsFilename: awsProfileCredentialsFilename,
}, nil
}

// CleanupAWSProfile cleans up credentials and config files created in given `basepath` via `CreateAWSProfile`.
func CleanupAWSProfile(basepath string) error {
configPath := filepath.Join(basepath, awsProfileConfigFilename)
if err := os.Remove(configPath); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("aws-profile: Failed to remove config file %s: %v", configPath, err)
}
}

credentialsPath := filepath.Join(basepath, awsProfileCredentialsFilename)
if err := os.Remove(credentialsPath); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("aws-profile: Failed to remove credentials file %s: %v", credentialsPath, err)
}
}

return nil
}

func writeAWSProfileFile(path string, content string, filePerm os.FileMode) error {
return renameio.WriteFile(path, []byte(content), filePerm)
}

func credentialsFileContents(profile string, accessKeyID string, secretAccessKey string, sessionToken string) string {
var b strings.Builder
b.Grow(128)
b.WriteRune('[')
b.WriteString(profile)
b.WriteRune(']')
b.WriteRune('\n')

b.WriteString("aws_access_key_id=")
b.WriteString(accessKeyID)
b.WriteRune('\n')

b.WriteString("aws_secret_access_key=")
b.WriteString(secretAccessKey)
b.WriteRune('\n')

if sessionToken != "" {
b.WriteString("aws_session_token=")
b.WriteString(sessionToken)
b.WriteRune('\n')
}

return b.String()
}

func configFileContents(profile string) string {
return fmt.Sprintf("[profile %s]\n", profile)
}

// isValidCredential checks whether given credential file contains any non-printable characters.
func isValidCredential(s string) bool {
return !strings.ContainsFunc(s, func(r rune) bool { return !unicode.IsPrint(r) })
}
100 changes: 100 additions & 0 deletions pkg/driver/node/credentialprovider/awsprofile/aws_profile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package awsprofile_test

import (
"errors"
"io/fs"
"os"
"path/filepath"
"testing"

"github.com/awslabs/aws-s3-csi-driver/pkg/driver/node/credentialprovider/awsprofile"
"github.com/awslabs/aws-s3-csi-driver/pkg/driver/node/credentialprovider/awsprofile/awsprofiletest"
"github.com/awslabs/aws-s3-csi-driver/pkg/util/testutil/assert"
)

const testAccessKeyId = "test-access-key-id"
const testSecretAccessKey = "test-secret-access-key"
const testSessionToken = "test-session-token"
const testFilePerm = fs.FileMode(0600)

func TestCreatingAWSProfile(t *testing.T) {
t.Run("create config and credentials files", func(t *testing.T) {
basepath := t.TempDir()
profile, err := awsprofile.CreateAWSProfile(basepath, testAccessKeyId, testSecretAccessKey, testSessionToken, testFilePerm)
assert.NoError(t, err)
assertCredentialsFromAWSProfile(t, basepath, profile, testAccessKeyId, testSecretAccessKey, testSessionToken)
})

t.Run("create config and credentials files with empty session token", func(t *testing.T) {
basepath := t.TempDir()
profile, err := awsprofile.CreateAWSProfile(basepath, testAccessKeyId, testSecretAccessKey, "", testFilePerm)
assert.NoError(t, err)
assertCredentialsFromAWSProfile(t, basepath, profile, testAccessKeyId, testSecretAccessKey, "")
})

t.Run("ensure config and credentials files are created with correct permissions", func(t *testing.T) {
basepath := t.TempDir()
profile, err := awsprofile.CreateAWSProfile(basepath, testAccessKeyId, testSecretAccessKey, testSessionToken, testFilePerm)
assert.NoError(t, err)
assertCredentialsFromAWSProfile(t, basepath, profile, testAccessKeyId, testSecretAccessKey, testSessionToken)

configStat, err := os.Stat(filepath.Join(basepath, profile.ConfigFilename))
assert.NoError(t, err)
assert.Equals(t, testFilePerm, configStat.Mode())

credentialsStat, err := os.Stat(filepath.Join(basepath, profile.CredentialsFilename))
assert.NoError(t, err)
assert.Equals(t, testFilePerm, credentialsStat.Mode())
})

t.Run("fail if credentials contains non-ascii characters", func(t *testing.T) {
t.Run("access key ID", func(t *testing.T) {
_, err := awsprofile.CreateAWSProfile(t.TempDir(), testAccessKeyId+"\n\t\r credential_process=exit", testSecretAccessKey, testSessionToken, testFilePerm)
assert.Equals(t, true, errors.Is(err, awsprofile.ErrInvalidCredentials))
})
t.Run("secret access key", func(t *testing.T) {
_, err := awsprofile.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey+"\n", testSessionToken, testFilePerm)
assert.Equals(t, true, errors.Is(err, awsprofile.ErrInvalidCredentials))
})
t.Run("session token", func(t *testing.T) {
_, err := awsprofile.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey, testSessionToken+"\n\r", testFilePerm)
assert.Equals(t, true, errors.Is(err, awsprofile.ErrInvalidCredentials))
})
})
}

func TestCleaningUpAWSProfile(t *testing.T) {
t.Run("clean config and credentials files", func(t *testing.T) {
basepath := t.TempDir()

profile, err := awsprofile.CreateAWSProfile(basepath, testAccessKeyId, testSecretAccessKey, testSessionToken, testFilePerm)
assert.NoError(t, err)
assertCredentialsFromAWSProfile(t, basepath, profile, testAccessKeyId, testSecretAccessKey, testSessionToken)

err = awsprofile.CleanupAWSProfile(basepath)
assert.NoError(t, err)

_, err = os.Stat(filepath.Join(basepath, profile.ConfigFilename))
assert.Equals(t, true, errors.Is(err, fs.ErrNotExist))

_, err = os.Stat(filepath.Join(basepath, profile.CredentialsFilename))
assert.Equals(t, true, errors.Is(err, fs.ErrNotExist))
})

t.Run("cleaning non-existent config and credentials files should not be an error", func(t *testing.T) {
err := awsprofile.CleanupAWSProfile(t.TempDir())
assert.NoError(t, err)
})
}

func assertCredentialsFromAWSProfile(t *testing.T, basepath string, profile awsprofile.AWSProfile, accessKeyID string, secretAccessKey string, sessionToken string) {
awsprofiletest.AssertCredentialsFromAWSProfile(
t,
profile.Name,
filepath.Join(basepath, profile.ConfigFilename),
filepath.Join(basepath, profile.CredentialsFilename),
accessKeyID,
secretAccessKey,
sessionToken,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Package awsprofiletest provides testing utilities for AWS Profiles.
package awsprofiletest

import (
"context"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"

"github.com/awslabs/aws-s3-csi-driver/pkg/util/testutil/assert"
)

func AssertCredentialsFromAWSProfile(t *testing.T, profileName, configFile, credentialsFile, accessKeyID, secretAccessKey, sessionToken string) {
t.Helper()

credentials := parseAWSProfile(t, profileName, configFile, credentialsFile)
assert.Equals(t, accessKeyID, credentials.AccessKeyID)
assert.Equals(t, secretAccessKey, credentials.SecretAccessKey)
assert.Equals(t, sessionToken, credentials.SessionToken)
}

func parseAWSProfile(t *testing.T, profileName, configFile, credentialsFile string) aws.Credentials {
sharedConfig, err := config.LoadSharedConfigProfile(context.Background(), profileName, func(c *config.LoadSharedConfigOptions) {
c.ConfigFiles = []string{configFile}
c.CredentialsFiles = []string{credentialsFile}
})
assert.NoError(t, err)
return sharedConfig.Credentials
}
28 changes: 28 additions & 0 deletions pkg/driver/node/credentialprovider/credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package credentialprovider

import (
"io/fs"

"github.com/awslabs/aws-s3-csi-driver/pkg/driver/node/envprovider"
)

// CredentialFilePerm is the default permissions to be used for credential files.
// It's only readable and writeable by the owner.
const CredentialFilePerm = fs.FileMode(0600)

// CredentialDirPerm is the default permissions to be used for credential directories.
// It's only readable, listable (execute bit), and writeable by the owner.
const CredentialDirPerm = fs.FileMode(0700)

// Credentials is the interface implemented by credential providers.
type Credentials interface {
// Source returns the source of these credentials.
Source() AuthenticationSource

// Dump dumps credentials into `writePath` and returns environment variables
// relative to `envPath` to pass to Mountpoint during mount.
//
// The environment variables will only passed to Mountpoint once during mount operation,
// in subsequent calls, this method will update previously written credentials on disk.
Dump(writePath string, envPath string) (envprovider.Environment, error)
}
38 changes: 38 additions & 0 deletions pkg/driver/node/credentialprovider/credentials_long_term.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package credentialprovider

import (
"fmt"
"path/filepath"

"github.com/awslabs/aws-s3-csi-driver/pkg/driver/node/credentialprovider/awsprofile"
"github.com/awslabs/aws-s3-csi-driver/pkg/driver/node/envprovider"
)

type longTermCredentials struct {
source AuthenticationSource

accessKeyID string
secretAccessKey string
sessionToken string
}

func (c *longTermCredentials) Source() AuthenticationSource {
return c.source
}

func (c *longTermCredentials) Dump(writePath string, envPath string) (envprovider.Environment, error) {
awsProfile, err := awsprofile.CreateAWSProfile(writePath, c.accessKeyID, c.secretAccessKey, c.sessionToken, CredentialFilePerm)
if err != nil {
return nil, fmt.Errorf("credentialprovider: long-term: failed to create aws profile: %w", err)
}

profile := awsProfile.Name
configFile := filepath.Join(envPath, awsProfile.ConfigFilename)
credentialsFile := filepath.Join(envPath, awsProfile.CredentialsFilename)

return envprovider.Environment{
envprovider.Format(envprovider.EnvProfile, profile),
envprovider.Format(envprovider.EnvConfigFile, configFile),
envprovider.Format(envprovider.EnvSharedCredentialsFile, credentialsFile),
}, nil
}
24 changes: 24 additions & 0 deletions pkg/driver/node/credentialprovider/credentials_multi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package credentialprovider

import "github.com/awslabs/aws-s3-csi-driver/pkg/driver/node/envprovider"

type multiCredentials struct {
source AuthenticationSource
credentials []Credentials
}

func (c *multiCredentials) Source() AuthenticationSource {
return c.source
}

func (c *multiCredentials) Dump(writePath string, envPath string) (envprovider.Environment, error) {
environment := envprovider.Environment{}
for _, c := range c.credentials {
env, err := c.Dump(writePath, envPath)
if err != nil {
return nil, err
}
environment = append(environment, env...)
}
return environment, nil
}
23 changes: 23 additions & 0 deletions pkg/driver/node/credentialprovider/credentials_shared_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package credentialprovider

import (
"github.com/awslabs/aws-s3-csi-driver/pkg/driver/node/envprovider"
)

type sharedProfileCredentials struct {
source AuthenticationSource

configFile string
sharedCredentialsFile string
}

func (c *sharedProfileCredentials) Source() AuthenticationSource {
return c.source
}

func (c *sharedProfileCredentials) Dump(writePath string, envPath string) (envprovider.Environment, error) {
return envprovider.Environment{
envprovider.Format(envprovider.EnvConfigFile, c.configFile),
envprovider.Format(envprovider.EnvSharedCredentialsFile, c.sharedCredentialsFile),
}, nil
}
Loading

0 comments on commit bcc72d4

Please sign in to comment.