Skip to content

Commit

Permalink
Add File Permission Support
Browse files Browse the repository at this point in the history
  • Loading branch information
liaandy committed Feb 28, 2025
1 parent e9d3aca commit ddae436
Show file tree
Hide file tree
Showing 5 changed files with 422 additions and 9 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ The primary objects field of the SecretProviderClass can contain the following s
* objectName: This field is required. It specifies the name of the secret or parameter to be fetched. For Secrets Manager this is the [SecretId](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html#API_GetSecretValue_RequestParameters) parameter and can be either the friendly name or full ARN of the secret. For SSM Parameter Store, this is the [Name](https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParameter.html#API_GetParameter_RequestParameters) of the parameter and can be either the name or full ARN of the parameter.
* objectType: This field is optional when using a Secrets Manager ARN for objectName, otherwise it is required. This field can be either "secretsmanager" or "ssmparameter".
* objectAlias: This optional field specifies the file name under which the secret will be mounted. When not specified the file name defaults to objectName.
* filePermission: This optional field expects a 4 digit string which specifies the file permission for the secret that will be mounted. When not specified this will default to "0644" permisions. Ensure the 4 digit string is a valid octal file permission.
* objectVersion: This field is optional, and generally not recommended since updates to the secret require updating this field. For Secrets Manager this is the [VersionId](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html#API_GetSecretValue_RequestParameters). For SSM Parameter Store, this is the optional [version number](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-versions.html#reference-parameter-version).
* objectVersionLabel: This optional field specifies the alias used for the version. Most applications should not use this field since the most recent version of the secret is used by default. For Secrets Manager this is the [VersionStage](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html#API_GetSecretValue_RequestParameters). For SSM Parameter Store this is the optional [Parameter Label](https://docs.amazonaws.cn/en_us/systems-manager/latest/userguide/sysman-paramstore-labels.html).
Expand Down Expand Up @@ -231,6 +232,9 @@ The primary objects field of the SecretProviderClass can contain the following s
* path: This required field is the [JMES path](https://jmespath.org/specification.html) to use for retrieval
* objectAlias: This required field specifies the file name under which the key-value pair secret will be mounted.

You may pass an additional sub-field to specify the file permission:
* filePermission: This optional field expects a 4 digit string which specifies the file permission for the secret that will be mounted. When not specified this will default to the parent object's file permission.
## Additional Considerations
### Rotation
Expand Down
50 changes: 46 additions & 4 deletions provider/secret_descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ type SecretDescriptor struct {
// One of secretsmanager or ssmparameter (not required when using full secrets manager ARN).
ObjectType string `json:"objectType"`

// Optional file permission (default to driver file permission).
FilePermission string `json:"filePermission"`

// Optional array to specify what json key value pairs to extract from a secret and mount as individual secrets
JMESPath []JMESPathEntry `json:"jmesPath"`

Expand All @@ -53,6 +56,9 @@ type JMESPathEntry struct {

//File name in which to store the secret in.
ObjectAlias string `json:"objectAlias"`

// Optional file permission (default to driver file permission).
FilePermission string `json:"filePermission"`
}

// An individual json key value pair to mount
Expand Down Expand Up @@ -145,11 +151,17 @@ func (p *SecretDescriptor) GetSecretType() (stype SecretType) {

// Return a descriptor for a jmes object entry within the secret
func (p *SecretDescriptor) getJmesEntrySecretDescriptor(j *JMESPathEntry) (d SecretDescriptor) {
permission := j.FilePermission
if permission == "" {
permission = p.FilePermission
}

return SecretDescriptor{
ObjectAlias: j.ObjectAlias,
ObjectType: p.getObjectType(),
translate: p.translate,
mountDir: p.mountDir,
ObjectAlias: j.ObjectAlias,
ObjectType: p.getObjectType(),
translate: p.translate,
mountDir: p.mountDir,
FilePermission: permission,
}
}

Expand Down Expand Up @@ -181,6 +193,24 @@ func (p *SecretDescriptor) GetObjectVersion(useFailoverRegion bool) (secretName
return p.ObjectVersion
}

// Private helper to validate a filePermission
//
// This function validates the filePermission and ensures it is a valid 4 digit octal string
func (p *SecretDescriptor) validateFilePermission(filePermission string) error {
// No file permission
if len(filePermission) == 0 {
return nil
}

match, _ := regexp.MatchString("^[0-7]{4}$", filePermission)

if match == false {
return fmt.Errorf("File permission must be valid 4 digit octal string: %s", filePermission)
}

return nil
}

// Private helper to validate the contents of SecretDescriptor.
//
// This method is used to validate input before it is used by the rest of the
Expand All @@ -206,6 +236,12 @@ func (p *SecretDescriptor) validateSecretDescriptor(regions []string) error {
return fmt.Errorf("path can not contain ../: %s", p.ObjectName)
}

// Ensure the string file permission is valid octal
err = p.validateFilePermission(p.FilePermission)
if err != nil {
return err
}

//ensure each jmesPath entry has a path and an objectalias
for _, jmesPathEntry := range p.JMESPath {
if len(jmesPathEntry.Path) == 0 {
Expand All @@ -215,6 +251,12 @@ func (p *SecretDescriptor) validateSecretDescriptor(regions []string) error {
if len(jmesPathEntry.ObjectAlias) == 0 {
return fmt.Errorf("Object alias must be specified for JMES object")
}

err = p.validateFilePermission(jmesPathEntry.FilePermission)
if err != nil {
return err
}

}

if len(p.FailoverObject.ObjectName) > 0 {
Expand Down
143 changes: 143 additions & 0 deletions provider/secret_descriptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,3 +620,146 @@ func TestVersionidsMatch(t *testing.T) {
}

}

// Test validateFilePermission function
func TestValidateFilePermission(t *testing.T) {
descriptor := SecretDescriptor{}

checkNoError := func(t testing.TB, got error) {
t.Helper()

if got != nil {
t.Errorf("Unexpected error: %v", got.Error())
}
}

checkErrorMessage := func(t testing.TB, got error, want string) {
t.Helper()

if got == nil {
t.Errorf("No error when expected error: %v ", want)
}

if got.Error() != want {
t.Errorf("%v != %v", got, want)
}
}

t.Run("EmptyFilePermission", func(t *testing.T) {
got := descriptor.validateFilePermission("")
checkNoError(t, got)
})

t.Run("CorrectOctalFilePermission", func(t *testing.T) {
got := descriptor.validateFilePermission("0600")
checkNoError(t, got)
})

t.Run("InvalidFilePermission", func(t *testing.T) {
got := descriptor.validateFilePermission("abc9")
want := "File permission must be valid 4 digit octal string: abc9"
checkErrorMessage(t, got, want)
})

t.Run("ShortFilePermission", func(t *testing.T) {
got := descriptor.validateFilePermission("000")
want := "File permission must be valid 4 digit octal string: 000"
checkErrorMessage(t, got, want)
})

t.Run("LongFilePermission", func(t *testing.T) {
got := descriptor.validateFilePermission("00000")
want := "File permission must be valid 4 digit octal string: 00000"
checkErrorMessage(t, got, want)
})
}

// Test getJmesSecretDescriptor function
func TestGetJmesEntrySecretDescriptor(t *testing.T) {

TestDescriptor := func(filePermission string) SecretDescriptor {
return SecretDescriptor{
ObjectType: "SecretsManager",
FilePermission: filePermission,
}
}

TestJmesPath := func(filePermission string) JMESPathEntry {
return JMESPathEntry{
FilePermission: filePermission,
}
}

checkPermissions := func(t testing.TB, got *SecretDescriptor, want string) {
t.Helper()
if got.FilePermission != want {
t.Errorf("got: %v want: %v", got, want)
}
}

t.Run("EmptyFilePermission", func(t *testing.T) {
descriptor := TestDescriptor("")
jmesPath := TestJmesPath("")
got := descriptor.getJmesEntrySecretDescriptor(&jmesPath)
checkPermissions(t, &got, "")
})

t.Run("InheritFromParent", func(t *testing.T) {
descriptor := TestDescriptor("0600")
jmesPath := TestJmesPath("")
got := descriptor.getJmesEntrySecretDescriptor(&jmesPath)
checkPermissions(t, &got, descriptor.FilePermission)
})

t.Run("OverrideParent", func(t *testing.T) {
descriptor := TestDescriptor("0600")
jmesPath := TestJmesPath("0777")
got := descriptor.getJmesEntrySecretDescriptor(&jmesPath)
checkPermissions(t, &got, jmesPath.FilePermission)
})
}

// Test the validatedescriptor function calls validate file permission
func TestValidateDescriptorFilePermission(t *testing.T) {

TestDescriptor := func(descriptorPermission string, jmesPermission string) SecretDescriptor {
return SecretDescriptor{
ObjectName: "foo",
ObjectType: "secretsmanager",
FilePermission: descriptorPermission,
JMESPath: []JMESPathEntry{
{
Path: "bar",
ObjectAlias: "foobar",
FilePermission: jmesPermission,
},
},
}
}

t.Run("DescriptorValidPermission", func(t *testing.T) {
descriptor := TestDescriptor("0600", "0700")
got := descriptor.validateSecretDescriptor(singleRegion)
if got != nil {
t.Errorf("Unexpected Error %v", got)
}
})

t.Run("DescriptorInvalidPermission", func(t *testing.T) {
descriptor := TestDescriptor("abcd", "")
got := descriptor.validateSecretDescriptor(singleRegion)
want := "File permission must be valid 4 digit octal string: abcd"
if got == nil || got.Error() != want {
t.Errorf("got: %v want: %v", got, want)
}
})

t.Run("DescriptorInvalidjmesPermission", func(t *testing.T) {
descriptor := TestDescriptor("", "efgh")
got := descriptor.validateSecretDescriptor(singleRegion)
want := "File permission must be valid 4 digit octal string: efgh"
if got == nil || got.Error() != want {
t.Errorf("got: %v want: %v", got, want)
}
})
}
15 changes: 13 additions & 2 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ func NewServer(
// Store or Secrets Manager) and write the secrets to the mount point. The
// version ids of the secrets are then returned to the driver.
func (s *CSIDriverProviderServer) Mount(ctx context.Context, req *v1alpha1.MountRequest) (response *v1alpha1.MountResponse, e error) {
// Log out the write mode
if s.driverWriteSecrets {
klog.Infof("Driver is configured to write secrets")
} else {
klog.Infof("Provider is configured to write secrets")
}

// Basic sanity check
if len(req.GetTargetPath()) == 0 {
Expand Down Expand Up @@ -168,9 +174,14 @@ func (s *CSIDriverProviderServer) Mount(ctx context.Context, req *v1alpha1.Mount

// Write out the secrets to the mount point after everything is fetched.
var files []*v1alpha1.File
var permission os.FileMode
for _, secret := range fetchedSecrets {

file, err := s.writeFile(secret, filePermission)
permission = filePermission
if secret.Descriptor.FilePermission != "" {
parsedPermission, _ := strconv.ParseInt(secret.Descriptor.FilePermission, 8, 32)
permission = os.FileMode(parsedPermission)
}
file, err := s.writeFile(secret, permission)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit ddae436

Please sign in to comment.