-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Pass long-term AWS credentials via file (#252)
* Add methods for creating and cleaning up AWS Profiles Signed-off-by: Burak Varlı <burakvar@amazon.co.uk> * Use credentials and config file to pass long-term credentials Signed-off-by: Burak Varlı <burakvar@amazon.co.uk> * Fail if AWS Credentials contains non-printable characters Signed-off-by: Burak Varlı <burakvar@amazon.co.uk> --------- Signed-off-by: Burak Varlı <burakvar@amazon.co.uk> (cherry picked from commit d4c189b)
- Loading branch information
Showing
6 changed files
with
328 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package driver | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"unicode" | ||
) | ||
|
||
const ( | ||
awsProfileName = "s3-csi" | ||
awsProfileConfigFilename = "s3-csi-config" | ||
awsProfileCredentialsFilename = "s3-csi-credentials" | ||
awsProfileFilePerm = fs.FileMode(0400) // only owner readable | ||
) | ||
|
||
// 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 | ||
ConfigPath string | ||
CredentialsPath 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) (AWSProfile, error) { | ||
if !isValidCredential(accessKeyID) || !isValidCredential(secretAccessKey) || !isValidCredential(sessionToken) { | ||
return AWSProfile{}, ErrInvalidCredentials | ||
} | ||
|
||
name := awsProfileName | ||
|
||
configPath := filepath.Join(basepath, awsProfileConfigFilename) | ||
err := writeAWSProfileFile(configPath, configFileContents(name)) | ||
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)) | ||
if err != nil { | ||
return AWSProfile{}, fmt.Errorf("aws-profile: Failed to create credentials file %s: %v", credentialsPath, err) | ||
} | ||
|
||
return AWSProfile{ | ||
Name: name, | ||
ConfigPath: configPath, | ||
CredentialsPath: credentialsPath, | ||
}, 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) error { | ||
err := os.WriteFile(path, []byte(content), awsProfileFilePerm) | ||
if err != nil { | ||
return err | ||
} | ||
// If the given file exists, `os.WriteFile` just truncates it without changing it's permissions, | ||
// so we need to ensure it always has the correct permissions. | ||
return os.Chmod(path, awsProfileFilePerm) | ||
} | ||
|
||
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) }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package driver_test | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"io/fs" | ||
"os" | ||
"testing" | ||
|
||
"github.com/awslabs/aws-s3-csi-driver/pkg/driver" | ||
|
||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/config" | ||
) | ||
|
||
const testAccessKeyId = "test-access-key-id" | ||
const testSecretAccessKey = "test-secret-access-key" | ||
const testSessionToken = "test-session-token" | ||
|
||
func TestCreatingAWSProfile(t *testing.T) { | ||
t.Run("create config and credentials files", func(t *testing.T) { | ||
profile, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey, testSessionToken) | ||
assertNoError(t, err) | ||
assertCredentialsFromAWSProfile(t, profile, testAccessKeyId, testSecretAccessKey, testSessionToken) | ||
}) | ||
|
||
t.Run("create config and credentials files with empty session token", func(t *testing.T) { | ||
profile, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey, "") | ||
assertNoError(t, err) | ||
assertCredentialsFromAWSProfile(t, profile, testAccessKeyId, testSecretAccessKey, "") | ||
}) | ||
|
||
t.Run("ensure config and credentials files are owner readable only", func(t *testing.T) { | ||
profile, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey, testSessionToken) | ||
assertNoError(t, err) | ||
assertCredentialsFromAWSProfile(t, profile, testAccessKeyId, testSecretAccessKey, testSessionToken) | ||
|
||
configStat, err := os.Stat(profile.ConfigPath) | ||
assertNoError(t, err) | ||
assertEquals(t, 0400, configStat.Mode()) | ||
|
||
credentialsStat, err := os.Stat(profile.CredentialsPath) | ||
assertNoError(t, err) | ||
assertEquals(t, 0400, 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 := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId+"\n\t\r credential_process=exit", testSecretAccessKey, testSessionToken) | ||
assertEquals(t, true, errors.Is(err, driver.ErrInvalidCredentials)) | ||
}) | ||
t.Run("secret access key", func(t *testing.T) { | ||
_, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey+"\n", testSessionToken) | ||
assertEquals(t, true, errors.Is(err, driver.ErrInvalidCredentials)) | ||
}) | ||
t.Run("session token", func(t *testing.T) { | ||
_, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey, testSessionToken+"\n\r") | ||
assertEquals(t, true, errors.Is(err, driver.ErrInvalidCredentials)) | ||
}) | ||
}) | ||
} | ||
|
||
func TestCleaningUpAWSProfile(t *testing.T) { | ||
t.Run("clean config and credentials files", func(t *testing.T) { | ||
basepath := t.TempDir() | ||
|
||
profile, err := driver.CreateAWSProfile(basepath, testAccessKeyId, testSecretAccessKey, testSessionToken) | ||
assertNoError(t, err) | ||
assertCredentialsFromAWSProfile(t, profile, testAccessKeyId, testSecretAccessKey, testSessionToken) | ||
|
||
err = driver.CleanupAWSProfile(basepath) | ||
assertNoError(t, err) | ||
|
||
_, err = os.Stat(profile.ConfigPath) | ||
assertEquals(t, true, errors.Is(err, fs.ErrNotExist)) | ||
|
||
_, err = os.Stat(profile.CredentialsPath) | ||
assertEquals(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 := driver.CleanupAWSProfile(t.TempDir()) | ||
assertNoError(t, err) | ||
}) | ||
} | ||
|
||
func assertCredentialsFromAWSProfile(t *testing.T, profile driver.AWSProfile, accessKeyID string, secretAccessKey string, sessionToken string) { | ||
credentials := parseAWSProfile(t, profile) | ||
assertEquals(t, accessKeyID, credentials.AccessKeyID) | ||
assertEquals(t, secretAccessKey, credentials.SecretAccessKey) | ||
assertEquals(t, sessionToken, credentials.SessionToken) | ||
} | ||
|
||
func parseAWSProfile(t *testing.T, profile driver.AWSProfile) aws.Credentials { | ||
sharedConfig, err := config.LoadSharedConfigProfile(context.Background(), profile.Name, func(c *config.LoadSharedConfigOptions) { | ||
c.ConfigFiles = []string{profile.ConfigPath} | ||
c.CredentialsFiles = []string{profile.CredentialsPath} | ||
}) | ||
assertNoError(t, err) | ||
return sharedConfig.Credentials | ||
} | ||
|
||
func assertEquals[T comparable](t *testing.T, expected T, got T) { | ||
if expected != got { | ||
t.Errorf("Expected %#v, Got %#v", expected, got) | ||
} | ||
} | ||
|
||
func assertNoError(t *testing.T, err error) { | ||
if err != nil { | ||
t.Errorf("Expected no error, but got: %s", err) | ||
} | ||
} |
Oops, something went wrong.