diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index dbf2fb2f6..783bf7138 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -98,6 +98,15 @@ type KeyVaultObject struct { // The encoding of the object in KeyVault // Supported encodings are Base64, Hex, Utf-8 ObjectEncoding string `json:"objectEncoding" yaml:"objectEncoding"` + // FilePermission is the file permissions + FilePermission string `json:"filePermission" yaml:"filePermission"` +} + +// SecretFile holds content and metadata of a secret file +type SecretFile struct { + Content []byte + Path string + FileMode int32 } // StringArray ... @@ -175,7 +184,7 @@ func (mc *mountConfig) GetServicePrincipalToken(resource string) (*adal.ServiceP } // MountSecretsStoreObjectContent mounts content of the secrets store object to target path -func (p *Provider) MountSecretsStoreObjectContent(ctx context.Context, attrib map[string]string, secrets map[string]string, targetPath string, permission os.FileMode) (map[string][]byte, map[string]string, error) { +func (p *Provider) MountSecretsStoreObjectContent(ctx context.Context, attrib map[string]string, secrets map[string]string, targetPath string, defaultFilePermission os.FileMode) ([]SecretFile, map[string]string, error) { keyvaultName := strings.TrimSpace(attrib["keyvaultName"]) cloudName := strings.TrimSpace(attrib["cloudName"]) usePodIdentityStr := strings.TrimSpace(attrib["usePodIdentity"]) @@ -257,7 +266,7 @@ func (p *Provider) MountSecretsStoreObjectContent(ctx context.Context, attrib ma klog.V(5).InfoS("unmarshaled key vault objects", "keyVaultObjects", keyVaultObjects, "count", len(keyVaultObjects), "pod", klog.ObjectRef{Namespace: podNamespace, Name: podName}) if len(keyVaultObjects) == 0 { - return make(map[string][]byte), make(map[string]string), nil + return nil, make(map[string]string), nil } vaultURL, err := mc.getVaultURL() @@ -273,7 +282,7 @@ func (p *Provider) MountSecretsStoreObjectContent(ctx context.Context, attrib ma } objectVersionMap := make(map[string]string) - files := make(map[string][]byte) + files := []SecretFile{} for _, keyVaultObject := range keyVaultObjects { klog.V(5).InfoS("fetching object from key vault", "objectName", keyVaultObject.ObjectName, "objectType", keyVaultObject.ObjectType, "keyvault", mc.keyvaultName, "pod", klog.ObjectRef{Namespace: podNamespace, Name: podName}) if err := validateObjectFormat(keyVaultObject.ObjectFormat, keyVaultObject.ObjectType); err != nil { @@ -290,6 +299,11 @@ func (p *Provider) MountSecretsStoreObjectContent(ctx context.Context, attrib ma return nil, nil, wrapObjectTypeError(err, keyVaultObject.ObjectType, keyVaultObject.ObjectName, keyVaultObject.ObjectVersion) } + filePermission, err := validateFilePermission(keyVaultObject.FilePermission, defaultFilePermission) + if err != nil { + return nil, nil, err + } + // fetch the object from Key Vault content, newObjectVersion, err := p.GetKeyVaultObjectContent(ctx, kvClient, keyVaultObject, *vaultURL) if err != nil { @@ -307,7 +321,11 @@ func (p *Provider) MountSecretsStoreObjectContent(ctx context.Context, attrib ma } // these files will be returned to the CSI driver as part of gRPC response - files[fileName] = objectContent + files = append(files, SecretFile{ + Path: fileName, + Content: objectContent, + FileMode: filePermission, + }) klog.V(5).InfoS("added file to the gRPC response", "file", fileName, "pod", klog.ObjectRef{Namespace: podNamespace, Name: podName}) } @@ -783,3 +801,20 @@ func fetchCertChains(data []byte) ([]byte, error) { } return pemData, nil } + +// validateFilePermission checks if the given file permission is correct octal number and returns +// a. decimal equivalent of the default file permission (0644) if file permission is not provided Or +// b. decimal equivalent Or +// c. error if it's not valid +func validateFilePermission(filePermission string, defaultFilePermission os.FileMode) (int32, error) { + if filePermission == "" { + return int32(defaultFilePermission), nil + } + + permission, err := strconv.ParseInt(filePermission, 8, 32) + if err != nil { + return 0, fmt.Errorf("file permission must be a valid octal number: %w", err) + } + + return int32(permission), nil +} diff --git a/pkg/provider/provider_test.go b/pkg/provider/provider_test.go index 52e6a6396..695854d02 100644 --- a/pkg/provider/provider_test.go +++ b/pkg/provider/provider_test.go @@ -983,3 +983,48 @@ func TestGetObjectVersion(t *testing.T) { actual := getObjectVersion(id) assert.Equal(t, expectedVersion, actual) } + +func TestValidateFilePermisssion(t *testing.T) { + cases := []struct { + desc string + filePermission string + defaultFilePermission os.FileMode + isErrorExpected bool + }{ + { + desc: "valid file permission", + filePermission: "0600", + defaultFilePermission: os.FileMode(0644), + isErrorExpected: false, + }, + { + desc: "empty file permission", + filePermission: "", + defaultFilePermission: os.FileMode(0644), + isErrorExpected: false, + }, + { + desc: "invalid file permission", + filePermission: "0900", + defaultFilePermission: os.FileMode(0644), + isErrorExpected: true, + }, + { + desc: "invalid octal number", + filePermission: "900", + defaultFilePermission: os.FileMode(0644), + isErrorExpected: true, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + _, err := validateFilePermission(tc.filePermission, tc.defaultFilePermission) + if tc.isErrorExpected { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go index dd56d3f14..5d494d4ef 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -34,7 +34,7 @@ func New() *CSIDriverProviderServer { // writes the contents to the pod mount and returns the object versions as part of MountResponse func (s *CSIDriverProviderServer) Mount(ctx context.Context, req *v1alpha1.MountRequest) (*v1alpha1.MountResponse, error) { var attrib, secret map[string]string - var filePermission os.FileMode + var defaultFilePermission os.FileMode var err error err = json.Unmarshal([]byte(req.GetAttributes()), &attrib) @@ -47,13 +47,13 @@ func (s *CSIDriverProviderServer) Mount(ctx context.Context, req *v1alpha1.Mount klog.ErrorS(err, "failed to unmarshal node publish secrets ref") return &v1alpha1.MountResponse{}, fmt.Errorf("failed to unmarshal secrets, error: %w", err) } - err = json.Unmarshal([]byte(req.GetPermission()), &filePermission) + err = json.Unmarshal([]byte(req.GetPermission()), &defaultFilePermission) if err != nil { klog.ErrorS(err, "failed to unmarshal file permission") return &v1alpha1.MountResponse{}, fmt.Errorf("failed to unmarshal file permission, error: %w", err) } - files, objectVersions, err := s.Provider.MountSecretsStoreObjectContent(ctx, attrib, secret, req.GetTargetPath(), filePermission) + files, objectVersions, err := s.Provider.MountSecretsStoreObjectContent(ctx, attrib, secret, req.GetTargetPath(), defaultFilePermission) if err != nil { klog.ErrorS(err, "failed to process mount request") return &v1alpha1.MountResponse{}, fmt.Errorf("failed to mount objects, error: %w", err) @@ -66,11 +66,11 @@ func (s *CSIDriverProviderServer) Mount(ctx context.Context, req *v1alpha1.Mount f := []*v1alpha1.File{} // CSI driver v0.0.21+ will write to the filesystem if the files are in the response. // No files in the response translates to "not implemented" in the CSI driver. - for k, v := range files { + for _, file := range files { f = append(f, &v1alpha1.File{ - Path: k, - Contents: v, - Mode: int32(filePermission), + Path: file.Path, + Contents: file.Content, + Mode: file.FileMode, }) } diff --git a/test/e2e/secret_file_permission_test.go b/test/e2e/secret_file_permission_test.go new file mode 100644 index 000000000..b1f582943 --- /dev/null +++ b/test/e2e/secret_file_permission_test.go @@ -0,0 +1,118 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "fmt" + "strings" + + "github.com/Azure/secrets-store-csi-driver-provider-azure/pkg/provider" + "github.com/Azure/secrets-store-csi-driver-provider-azure/test/e2e/framework/exec" + "github.com/Azure/secrets-store-csi-driver-provider-azure/test/e2e/framework/namespace" + "github.com/Azure/secrets-store-csi-driver-provider-azure/test/e2e/framework/pod" + "github.com/Azure/secrets-store-csi-driver-provider-azure/test/e2e/framework/secret" + "github.com/Azure/secrets-store-csi-driver-provider-azure/test/e2e/framework/secretproviderclass" + + "github.com/ghodss/yaml" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/secrets-store-csi-driver/apis/v1alpha1" +) + +var _ = Describe("When user provides file permission for secrets", func() { + var ( + specName = "secret-file-permission" + spc *v1alpha1.SecretProviderClass + ns *corev1.Namespace + nodePublishSecretRef *corev1.Secret + p *corev1.Pod + expectedFilePermission = "755" + ) + + BeforeEach(func() { + ns = namespace.Create(namespace.CreateInput{ + Creator: kubeClient, + Name: specName, + }) + + nodePublishSecretRef = secret.Create(secret.CreateInput{ + Creator: kubeClient, + Name: "secrets-store-creds", + Namespace: ns.Name, + Data: map[string][]byte{"clientid": []byte(config.AzureClientID), "clientsecret": []byte(config.AzureClientSecret)}, + Labels: map[string]string{"secrets-store.csi.k8s.io/used": "true"}, + }) + + keyVaultObjects := []provider.KeyVaultObject{ + { + ObjectName: "secret1", + ObjectType: provider.VaultObjectTypeSecret, + FilePermission: fmt.Sprintf("0%s", expectedFilePermission), + }, + } + + yamlArray := provider.StringArray{Array: []string{}} + for _, object := range keyVaultObjects { + obj, err := yaml.Marshal(object) + Expect(err).To(BeNil()) + yamlArray.Array = append(yamlArray.Array, string(obj)) + } + + objects, err := yaml.Marshal(yamlArray) + Expect(err).To(BeNil()) + + spc = secretproviderclass.Create(secretproviderclass.CreateInput{ + Creator: kubeClient, + Config: config, + Name: "azure", + Namespace: ns.Name, + Spec: v1alpha1.SecretProviderClassSpec{ + Provider: "azure", + Parameters: map[string]string{ + "keyvaultName": config.KeyvaultName, + "tenantId": config.TenantID, + "objects": string(objects), + }, + }, + }) + + p = pod.Create(pod.CreateInput{ + Creator: kubeClient, + Config: config, + Name: "busybox-secrets-store-inline-crd", + Namespace: ns.Name, + SecretProviderClassName: spc.Name, + NodePublishSecretRefName: nodePublishSecretRef.Name, + }) + }) + + AfterEach(func() { + Cleanup(CleanupInput{ + Namespace: ns, + Getter: kubeClient, + Lister: kubeClient, + Deleter: kubeClient, + }) + }) + + It("should mount secret file with given permission", func() { + if !config.IsKindCluster { + Skip("test case currently supported for kind cluster only") + } + + pod.WaitFor(pod.WaitForInput{ + Getter: kubeClient, + KubeconfigPath: kubeconfigPath, + Config: config, + PodName: p.Name, + Namespace: ns.Name, + }) + + cmd := getPodExecCommand("stat -c '%a' /mnt/secrets-store/..data/secret1") + filePermission, err := exec.KubectlExec(kubeconfigPath, p.Name, p.Namespace, strings.Split(cmd, " ")) + Expect(err).To(BeNil()) + Expect(strings.Trim(filePermission, "'")).To(Equal(expectedFilePermission)) + }) +}) diff --git a/website/content/en/getting-started/usage/_index.md b/website/content/en/getting-started/usage/_index.md index 500d4cecd..bf2a8e37e 100644 --- a/website/content/en/getting-started/usage/_index.md +++ b/website/content/en/getting-started/usage/_index.md @@ -51,6 +51,7 @@ To provide identity to access key vault, refer to the following [section](#provi objectAlias: SECRET_1 # [OPTIONAL available for version > 0.0.4] object alias objectType: secret # object types: secret, key or cert. For Key Vault certificates, refer to https://azure.github.io/secrets-store-csi-driver-provider-azure/configurations/getting-certs-and-keys/ for the object type to use objectVersion: "" # [OPTIONAL] object versions, default to latest if empty + filePermission: 0755 # [OPTIONAL] permission for secret file being mounted into the pod, default is 0644 if not specified. - | objectName: key1 objectAlias: "" # If provided then it has to be referenced in [secretObjects].[objectName] to sync with Kubernetes secrets @@ -76,6 +77,7 @@ To provide identity to access key vault, refer to the following [section](#provi | objectVersion | no | version of a Key Vault object, if not provided, will use latest | "" | | objectFormat | no | [__*available for version > 0.0.7*__] the format of the Azure Key Vault object, supported types are pem and pfx. `objectFormat: pfx` is only supported with `objectType: secret` and PKCS12 or ECC certificates | "pem" | | objectEncoding | no | [__*available for version > 0.0.8*__] the encoding of the Azure Key Vault secret object, supported types are `utf-8`, `hex` and `base64`. This option is supported only with `objectType: secret` | "utf-8" | + | filePermission | no | [__*available for version > v1.1.0*__] permission for secret file being mounted into the pod | "0644" | | tenantId | yes | tenant ID containing key vault instance | "" | #### Provide Identity to Access Key Vault