Skip to content

Commit

Permalink
feat: add functionality for syncing version history (#834)
Browse files Browse the repository at this point in the history
* feat: add functionality for syncing version history

If specified with a value greater than 1, the ObjectVersionHistory tells
the provider how many versions of the secret (including the latest
version)to sync from key vault. It will then put each secret into a file
named {objectName\objectAlias}/{secretVersion}. To use this
functionality, the provider will need the secrets/list permission in
addition to the permissions it already requires. If this functionality
is not used, then there are no additional permissions needed.
  • Loading branch information
lnr0626 committed Jul 22, 2022
1 parent 1ba0d55 commit 5c39064
Show file tree
Hide file tree
Showing 14 changed files with 1,497 additions and 48 deletions.
43 changes: 21 additions & 22 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,27 @@ RUN apt-get update \
# Install Go tools w/module support
&& mkdir -p /tmp/gotools \
&& cd /tmp/gotools \
&& GO111MODULE=on go get -v golang.org/x/tools/gopls@latest 2>&1 \
&& GO111MODULE=on go get -v \
honnef.co/go/tools/...@latest \
golang.org/x/tools/cmd/gorename@latest \
golang.org/x/tools/cmd/goimports@latest \
golang.org/x/tools/cmd/guru@latest \
golang.org/x/lint/golint@latest \
github.com/mdempsky/gocode@latest \
github.com/cweill/gotests/...@latest \
github.com/haya14busa/goplay/cmd/goplay@latest \
github.com/sqs/goreturns@latest \
github.com/josharian/impl@latest \
github.com/davidrjenni/reftools/cmd/fillstruct@latest \
github.com/uudashr/gopkgs/v2/cmd/gopkgs@latest \
github.com/ramya-rao-a/go-outline@latest \
github.com/acroca/go-symbols@latest \
github.com/godoctor/godoctor@latest \
github.com/rogpeppe/godef@latest \
github.com/zmb3/gogetdoc@latest \
github.com/fatih/gomodifytags@latest \
github.com/mgechev/revive@latest \
github.com/go-delve/delve/cmd/dlv@latest 2>&1 \
&& go install -v golang.org/x/tools/gopls@latest 2>&1 \
&& go install -v honnef.co/go/tools/...@latest \
&& go install -v golang.org/x/tools/cmd/gorename@latest \
&& go install -v golang.org/x/tools/cmd/goimports@latest \
&& go install -v golang.org/x/tools/cmd/guru@latest \
&& go install -v golang.org/x/lint/golint@latest \
&& go install -v github.com/mdempsky/gocode@latest \
&& go install -v github.com/cweill/gotests/...@latest \
&& go install -v github.com/haya14busa/goplay/cmd/goplay@latest \
&& go install -v github.com/sqs/goreturns@latest \
&& go install -v github.com/josharian/impl@latest \
&& go install -v github.com/davidrjenni/reftools/cmd/fillstruct@latest \
&& go install -v github.com/uudashr/gopkgs/v2/cmd/gopkgs@latest \
&& go install -v github.com/ramya-rao-a/go-outline@latest \
&& go install -v github.com/acroca/go-symbols@latest \
&& go install -v github.com/godoctor/godoctor@latest \
&& go install -v github.com/rogpeppe/godef@latest \
&& go install -v github.com/zmb3/gogetdoc@latest \
&& go install -v github.com/fatih/gomodifytags@latest \
&& go install -v github.com/mgechev/revive@latest \
&& go install -v github.com/go-delve/delve/cmd/dlv@latest 2>&1 \
#
# Install Go tools w/o module support
&& go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 2>&1 \
Expand Down
4 changes: 2 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind",
"source=${env:HOME}${env:USERPROFILE}/.azure,target=/root/.azure,type=bind"
],
"workspaceMount": "src=${localWorkspaceFolder},dst=${env:GOPATH}/src/secrets-store-csi-driver-provider-azure,type=bind,consistency=cached",
"workspaceFolder": "${env:GOPATH}/src/secrets-store-csi-driver-provider-azure",
"workspaceMount": "src=${localWorkspaceFolder},dst=/go/src/secrets-store-csi-driver-provider-azure,type=bind,consistency=cached",
"workspaceFolder": "/go/src/secrets-store-csi-driver-provider-azure",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/Azure/azure-sdk-for-go v65.0.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.27
github.com/Azure/go-autorest/autorest/adal v0.9.20
github.com/Azure/go-autorest/autorest/date v0.3.0
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1
github.com/google/go-cmp v0.5.6
github.com/pkg/errors v0.9.1
Expand All @@ -24,7 +25,6 @@ require (

require (
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
Expand Down
236 changes: 213 additions & 23 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import (
"fmt"
"math/big"
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"time"

Expand All @@ -25,6 +28,7 @@ import (
kv "github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/date"
"github.com/pkg/errors"
"golang.org/x/crypto/pkcs12"
"golang.org/x/net/context"
Expand Down Expand Up @@ -225,34 +229,226 @@ func (p *Provider) GetSecretsStoreObjectContent(ctx context.Context, attrib, sec
files := []types.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})
// fetch the object from Key Vault
content, newObjectVersion, err := p.getKeyVaultObjectContent(ctx, kvClient, keyVaultObject, *vaultURL)

resolvedKvObjects, err := p.resolveObjectVersions(ctx, kvClient, keyVaultObject, *vaultURL)
if err != nil {
return nil, err
}

objectContent, err := getContentBytes(content, keyVaultObject.ObjectType, keyVaultObject.ObjectEncoding)
for _, resolvedKvObject := range resolvedKvObjects {
// fetch the object from Key Vault
content, newObjectVersion, err := p.getKeyVaultObjectContent(ctx, kvClient, resolvedKvObject, *vaultURL)
if err != nil {
return nil, err
}

objectContent, err := getContentBytes(content, resolvedKvObject.ObjectType, resolvedKvObject.ObjectEncoding)
if err != nil {
return nil, err
}

// objectUID is a unique identifier in the format <object type>/<object name>
// This is the object id the user sees in the SecretProviderClassPodStatus
objectUID := resolvedKvObject.GetObjectUID()
file := types.SecretFile{
Path: resolvedKvObject.GetFileName(),
Content: objectContent,
UID: objectUID,
Version: newObjectVersion,
}
// the validity of file permission is already checked in the validate function above
file.FileMode, _ = resolvedKvObject.GetFilePermission(defaultFilePermission)

files = append(files, file)
klog.V(5).InfoS("added file to the gRPC response", "file", file.Path, "pod", klog.ObjectRef{Namespace: podNamespace, Name: podName})
}
}

return files, nil
}

func (p *Provider) resolveObjectVersions(ctx context.Context, kvClient *kv.BaseClient, kvObject types.KeyVaultObject, vaultURL string) (versions []types.KeyVaultObject, err error) {
if kvObject.IsSyncingSingleVersion() {
// version history less than or equal to 1 means only sync the latest and
// don't add anything to the file name
return []types.KeyVaultObject{kvObject}, nil
}

kvObjectVersions, err := p.getKeyVaultObjectVersions(ctx, kvClient, kvObject, vaultURL)
if err != nil {
return nil, err
}

return getLatestNKeyVaultObjects(kvObject, kvObjectVersions), nil
}

/*
Given a base key vault object and a list of object versions and their created dates, find
the latest kvObject.ObjectVersionHistory versions and return key vault objects with the
appropriate alias and version.
The alias is determine by the index of the version starting with 0 at the specified version (or
latest if no version is specified).
*/
func getLatestNKeyVaultObjects(kvObject types.KeyVaultObject, kvObjectVersions types.KeyVaultObjectVersionList) []types.KeyVaultObject {
baseFileName := kvObject.GetFileName()
objects := []types.KeyVaultObject{}

sort.Sort(kvObjectVersions)

// if we're being asked for the latest, then there's no need to skip any versions
foundFirst := kvObject.ObjectVersion == "" || kvObject.ObjectVersion == "latest"

for _, objectVersion := range kvObjectVersions {
foundFirst = foundFirst || objectVersion.Version == kvObject.ObjectVersion

if foundFirst {
length := len(objects)
newObject := kvObject

newObject.ObjectAlias = filepath.Join(baseFileName, strconv.Itoa(length))
newObject.ObjectVersion = objectVersion.Version

objects = append(objects, newObject)

if length+1 > int(kvObject.ObjectVersionHistory) {
break
}
}
}

return objects
}

func (p *Provider) getKeyVaultObjectVersions(ctx context.Context, kvClient *kv.BaseClient, kvObject types.KeyVaultObject, vaultURL string) (versions types.KeyVaultObjectVersionList, err error) {
start := time.Now()
defer func() {
var errMsg string
if err != nil {
return nil, err
errMsg = err.Error()
}
p.reporter.ReportKeyvaultRequest(ctx, time.Since(start).Seconds(), kvObject.ObjectType, kvObject.ObjectName, errMsg)
}()

// objectUID is a unique identifier in the format <object type>/<object name>
// This is the object id the user sees in the SecretProviderClassPodStatus
objectUID := getObjectUID(keyVaultObject.ObjectName, keyVaultObject.ObjectType)
file := types.SecretFile{
Path: keyVaultObject.GetFileName(),
Content: objectContent,
UID: objectUID,
Version: newObjectVersion,
switch kvObject.ObjectType {
case types.VaultObjectTypeSecret:
return getSecretVersions(ctx, kvClient, vaultURL, kvObject)
case types.VaultObjectTypeKey:
return getKeyVersions(ctx, kvClient, vaultURL, kvObject)
case types.VaultObjectTypeCertificate:
return getCertificateVersions(ctx, kvClient, vaultURL, kvObject)
default:
err := errors.Errorf("Invalid vaultObjectTypes. Should be secret, key, or cert")
return nil, wrapObjectTypeError(err, kvObject.ObjectType, kvObject.ObjectName, kvObject.ObjectVersion)
}
}

func getSecretVersions(ctx context.Context, kvClient *kv.BaseClient, vaultURL string, kvObject types.KeyVaultObject) ([]types.KeyVaultObjectVersion, error) {
kvVersionsList, err := kvClient.GetSecretVersions(ctx, vaultURL, kvObject.ObjectName, nil)
if err != nil {
return nil, wrapObjectTypeError(err, kvObject.ObjectType, kvObject.ObjectName, kvObject.ObjectVersion)
}

secretVersions := types.KeyVaultObjectVersionList{}

for notDone := true; notDone; notDone = kvVersionsList.NotDone() {
for _, secret := range kvVersionsList.Values() {
if secret.Attributes != nil {
objectVersion := getObjectVersion(*secret.ID)
created := date.UnixEpoch()

if secret.Attributes.Created != nil {
created = time.Time(*secret.Attributes.Created)
}

if secret.Attributes.Enabled != nil && *secret.Attributes.Enabled {
secretVersions = append(secretVersions, types.KeyVaultObjectVersion{
Version: objectVersion,
Created: created,
})
}
}
}
// the validity of file permission is already checked in the validate function above
file.FileMode, _ = keyVaultObject.GetFilePermission(defaultFilePermission)

files = append(files, file)
klog.V(5).InfoS("added file to the gRPC response", "file", file.Path, "pod", klog.ObjectRef{Namespace: podNamespace, Name: podName})
err = kvVersionsList.NextWithContext(ctx)
if err != nil {
return nil, wrapObjectTypeError(err, kvObject.ObjectType, kvObject.ObjectName, kvObject.ObjectVersion)
}
}

return files, nil
return secretVersions, nil
}

func getKeyVersions(ctx context.Context, kvClient *kv.BaseClient, vaultURL string, kvObject types.KeyVaultObject) ([]types.KeyVaultObjectVersion, error) {
kvVersionsList, err := kvClient.GetKeyVersions(ctx, vaultURL, kvObject.ObjectName, nil)
if err != nil {
return nil, wrapObjectTypeError(err, kvObject.ObjectType, kvObject.ObjectName, kvObject.ObjectVersion)
}

keyVersions := types.KeyVaultObjectVersionList{}

for notDone := true; notDone; notDone = kvVersionsList.NotDone() {
for _, key := range kvVersionsList.Values() {
if key.Attributes != nil {
objectVersion := getObjectVersion(*key.Kid)
created := date.UnixEpoch()

if key.Attributes.Created != nil {
created = time.Time(*key.Attributes.Created)
}

if key.Attributes.Enabled != nil && *key.Attributes.Enabled {
keyVersions = append(keyVersions, types.KeyVaultObjectVersion{
Version: objectVersion,
Created: created,
})
}
}
}

err = kvVersionsList.NextWithContext(ctx)
if err != nil {
return nil, wrapObjectTypeError(err, kvObject.ObjectType, kvObject.ObjectName, kvObject.ObjectVersion)
}
}

return keyVersions, nil
}

func getCertificateVersions(ctx context.Context, kvClient *kv.BaseClient, vaultURL string, kvObject types.KeyVaultObject) ([]types.KeyVaultObjectVersion, error) {
kvVersionsList, err := kvClient.GetCertificateVersions(ctx, vaultURL, kvObject.ObjectName, nil)
if err != nil {
return nil, wrapObjectTypeError(err, kvObject.ObjectType, kvObject.ObjectName, kvObject.ObjectVersion)
}

certVersions := types.KeyVaultObjectVersionList{}

for notDone := true; notDone; notDone = kvVersionsList.NotDone() {
for _, cert := range kvVersionsList.Values() {
if cert.Attributes != nil {
objectVersion := getObjectVersion(*cert.ID)
created := date.UnixEpoch()

if cert.Attributes.Created != nil {
created = time.Time(*cert.Attributes.Created)
}

if cert.Attributes.Enabled != nil && *cert.Attributes.Enabled {
certVersions = append(certVersions, types.KeyVaultObjectVersion{
Version: objectVersion,
Created: created,
})
}
}
}

err = kvVersionsList.NextWithContext(ctx)
if err != nil {
return nil, wrapObjectTypeError(err, kvObject.ObjectType, kvObject.ObjectName, kvObject.ObjectVersion)
}
}

return certVersions, nil
}

// GetKeyVaultObjectContent get content of the keyvault object
Expand Down Expand Up @@ -526,12 +722,6 @@ func getObjectVersion(id string) string {
return splitID[len(splitID)-1]
}

// getObjectUID returns UID for the object with the format
// <object type>/<object name>
func getObjectUID(objectName, objectType string) string {
return fmt.Sprintf("%s/%s", objectType, objectName)
}

// getContentBytes takes the given content string and returns the bytes to write to disk
// If an encoding is specified it will decode the string first
func getContentBytes(content, objectType, objectEncoding string) ([]byte, error) {
Expand Down
Loading

0 comments on commit 5c39064

Please sign in to comment.