Skip to content

Commit

Permalink
feat: add PushSecret and DeleteSecret to onepassword provider (extern…
Browse files Browse the repository at this point in the history
…al-secrets#2646)

* feat: add PushSecret and DeleteSecret to onepassword provider

Signed-off-by: Bryce Thuilot <bryce@thuilot.io>

* refactor: clean code based on suggestions

Signed-off-by: Bryce Thuilot <bryce@thuilot.io>

* refactor: make suggested sonar cube changes

Signed-off-by: Bryce Thuilot <bryce@thuilot.io>

---------

Signed-off-by: Bryce Thuilot <bryce@thuilot.io>
Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>
  • Loading branch information
bthuilot authored and limistah committed Jan 8, 2024
1 parent 74ca97a commit f216407
Show file tree
Hide file tree
Showing 4 changed files with 913 additions and 36 deletions.
2 changes: 1 addition & 1 deletion docs/introduction/stability-support.md
Expand Up @@ -72,7 +72,7 @@ The following table show the support for features across different providers.
| Alibaba Cloud KMS | | | | | x | | |
| Oracle Vault | | | | | x | | |
| Akeyless | x | x | | | x | | |
| 1Password | x | | | | x | | |
| 1Password | x | | | | x | x | x |
| Generic Webhook | | | | | | | x |
| senhasegura DSM | | | | | x | | |
| Doppler | x | | | | x | | |
Expand Down
32 changes: 22 additions & 10 deletions pkg/provider/onepassword/fake/fake.go
Expand Up @@ -22,10 +22,13 @@ import (

// OnePasswordMockClient is a fake connect.Client.
type OnePasswordMockClient struct {
MockVaults map[string][]onepassword.Vault
MockItems map[string][]onepassword.Item // ID and Title only
MockItemFields map[string]map[string][]*onepassword.ItemField
MockFileContents map[string][]byte
MockVaults map[string][]onepassword.Vault
MockItems map[string][]onepassword.Item // ID and Title only
MockItemFields map[string]map[string][]*onepassword.ItemField
MockFileContents map[string][]byte
UpdateItemValidateFunc func(*onepassword.Item, string) (*onepassword.Item, error)
CreateItemValidateFunc func(*onepassword.Item, string) (*onepassword.Item, error)
DeleteItemValidateFunc func(*onepassword.Item, string) error
}

// NewMockClient returns an instantiated mock client.
Expand Down Expand Up @@ -116,18 +119,27 @@ func (mockClient *OnePasswordMockClient) GetItemsByTitle(itemUUID, vaultUUID str
return items, nil
}

// CreateItem unused fake.
func (mockClient *OnePasswordMockClient) CreateItem(_ *onepassword.Item, _ string) (*onepassword.Item, error) {
// CreateItem will call a validation function if set.
func (mockClient *OnePasswordMockClient) CreateItem(i *onepassword.Item, s string) (*onepassword.Item, error) {
if mockClient.CreateItemValidateFunc != nil {
return mockClient.CreateItemValidateFunc(i, s)
}
return &onepassword.Item{}, nil
}

// UpdateItem unused fake.
func (mockClient *OnePasswordMockClient) UpdateItem(_ *onepassword.Item, _ string) (*onepassword.Item, error) {
// UpdateItem will call a validation function if set.
func (mockClient *OnePasswordMockClient) UpdateItem(i *onepassword.Item, s string) (*onepassword.Item, error) {
if mockClient.UpdateItemValidateFunc != nil {
return mockClient.UpdateItemValidateFunc(i, s)
}
return &onepassword.Item{}, nil
}

// DeleteItem unused fake.
func (mockClient *OnePasswordMockClient) DeleteItem(_ *onepassword.Item, _ string) error {
// DeleteItem will call a validation function if set.
func (mockClient *OnePasswordMockClient) DeleteItem(i *onepassword.Item, s string) error {
if mockClient.DeleteItemValidateFunc != nil {
return mockClient.DeleteItemValidateFunc(i, s)
}
return nil
}

Expand Down
208 changes: 187 additions & 21 deletions pkg/provider/onepassword/onepassword.go
Expand Up @@ -15,6 +15,7 @@ package onepassword

import (
"context"
"errors"
"fmt"
"net/url"
"sort"
Expand Down Expand Up @@ -45,17 +46,35 @@ const (
errFetchK8sSecret = "could not fetch ConnectToken Secret: %w"
errMissingToken = "missing Secret Token"
errGetVault = "error finding 1Password Vault: %w"
errExpectedOneItem = "expected one 1Password Item matching %w"
errGetItem = "error finding 1Password Item: %w"
errKeyNotFound = "key not found in 1Password Vaults: %w"
errDocumentNotFound = "error finding 1Password Document: %w"
errExpectedOneField = "expected one 1Password ItemField matching %w"
errTagsNotImplemented = "'find.tags' is not implemented in the 1Password provider"
errVersionNotImplemented = "'remoteRef.version' is not implemented in the 1Password provider"

documentCategory = "DOCUMENT"
fieldsWithLabelFormat = "'%s' in '%s', got %d"
incorrectCountFormat = "'%s', got %d"

errGetItem = "error finding 1Password Item: %w"
errUpdateItem = "error updating 1Password Item: %w"
errDocumentNotFound = "error finding 1Password Document: %w"
errTagsNotImplemented = "'find.tags' is not implemented in the 1Password provider"
errVersionNotImplemented = "'remoteRef.version' is not implemented in the 1Password provider"
errCreateItem = "error creating 1Password Item: %w"
errDeleteItem = "error deleting 1Password Item: %w"
// custom error messages.
errKeyNotFoundMsg = "key not found in 1Password Vaults"
errNoVaultsMsg = "no vaults found"
errNoChangesMsg = "no changes made to 1Password Item"
errExpectedOneItemMsg = "expected one 1Password Item matching"
errExpectedOneFieldMsg = "expected one 1Password ItemField matching"
errExpectedOneFieldMsgF = "%w: '%s' in '%s', got %d"

documentCategory = "DOCUMENT"
)

// Custom Errors //.
var (
// ErrKeyNotFound is returned when a key is not found in the 1Password Vaults.
ErrKeyNotFound = errors.New(errKeyNotFoundMsg)
// ErrNoVaults is returned when no vaults are found in the 1Password provider.
ErrNoVaults = errors.New(errNoVaultsMsg)
// ErrExpectedOneField is returned when more than 1 field is found in the 1Password Vaults.
ErrExpectedOneField = errors.New(errExpectedOneFieldMsg)
// ErrExpectedOneItem is returned when more than 1 item is found in the 1Password Vaults.
ErrExpectedOneItem = errors.New(errExpectedOneItemMsg)
)

// ProviderOnePassword is a provider for 1Password.
Expand Down Expand Up @@ -152,13 +171,160 @@ func validateStore(store esv1beta1.GenericStore) error {
return nil
}

func (provider *ProviderOnePassword) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
return fmt.Errorf("not implemented")
func deleteField(fields []*onepassword.ItemField, label string) ([]*onepassword.ItemField, error) {
// This will always iterate over all items
// but its done to ensure that two fields with the same label
// exist resulting in undefined behavior
var (
found bool
fieldsF = make([]*onepassword.ItemField, 0, len(fields))
)
for _, item := range fields {
if item.Label == label {
if found {
return nil, ErrExpectedOneField
}
found = true
continue
}
fieldsF = append(fieldsF, item)
}
return fieldsF, nil
}

func (provider *ProviderOnePassword) DeleteSecret(_ context.Context, ref esv1beta1.PushSecretRemoteRef) error {
providerItem, err := provider.findItem(ref.GetRemoteKey())
if err != nil {
return err
}

providerItem.Fields, err = deleteField(providerItem.Fields, ref.GetProperty())
if err != nil {
return fmt.Errorf(errUpdateItem, err)
}

if len(providerItem.Fields) == 0 && len(providerItem.Files) == 0 && len(providerItem.Sections) == 0 {
// Delete the item if there are no fields, files or sections
if err = provider.client.DeleteItem(providerItem, providerItem.Vault.ID); err != nil {
return fmt.Errorf(errDeleteItem, err)
}
return nil
}

if _, err = provider.client.UpdateItem(providerItem, providerItem.Vault.ID); err != nil {
return fmt.Errorf(errDeleteItem, err)
}
return nil
}

const (
passwordLabel = "password"
)

// createItem creates a new item in the first vault. If no vaults exist, it returns an error.
func (provider *ProviderOnePassword) createItem(val []byte, ref esv1beta1.PushSecretData) error {
// Get the first vault
sortedVaults := sortVaults(provider.vaults)
if len(sortedVaults) == 0 {
return ErrNoVaults
}
vaultID := sortedVaults[0]
// Get the label
label := ref.GetProperty()
if label == "" {
label = passwordLabel
}

// Create the item
item := &onepassword.Item{
Title: ref.GetRemoteKey(),
Category: onepassword.Server,
Vault: onepassword.ItemVault{
ID: vaultID,
},
Fields: []*onepassword.ItemField{
generateNewItemField(label, string(val)),
},
}

_, err := provider.client.CreateItem(item, vaultID)
return err
}

// updateFieldValue updates the fields value of an item with the given label.
// If the label does not exist, a new field is created. If the label exists but
// the value is different, the value is updated. If the label exists and the
// value is the same, nothing is done.
func updateFieldValue(fields []*onepassword.ItemField, label, newVal string) ([]*onepassword.ItemField, error) {
// This will always iterate over all items
// but its done to ensure that two fields with the same label
// exist resulting in undefined behavior
var (
found bool
index int
)
for i, item := range fields {
if item.Label == label {
if found {
return nil, ErrExpectedOneField
}
found = true
index = i
}
}
if !found {
return append(fields, generateNewItemField(label, newVal)), nil
}
if field := fields[index]; newVal != field.Value {
field.Value = newVal
fields[index] = field
}

return fields, nil
}

// generateNewItemField generates a new item field with the given label and value.
func generateNewItemField(label, newVal string) *onepassword.ItemField {
field := &onepassword.ItemField{
Label: label,
Value: newVal,
Type: onepassword.FieldTypeConcealed,
}

return field
}

// Not Implemented PushSecret.
func (provider *ProviderOnePassword) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
return fmt.Errorf("not implemented")
func (provider *ProviderOnePassword) PushSecret(_ context.Context, secret *corev1.Secret, ref esv1beta1.PushSecretData) error {
val, ok := secret.Data[ref.GetSecretKey()]
if !ok {
return ErrKeyNotFound
}

title := ref.GetRemoteKey()
providerItem, err := provider.findItem(title)
if errors.Is(err, ErrKeyNotFound) {
if err = provider.createItem(val, ref); err != nil {
return fmt.Errorf(errCreateItem, err)
}
return nil
} else if err != nil {
return err
}

label := ref.GetProperty()
if label == "" {
label = passwordLabel
}

providerItem.Fields, err = updateFieldValue(providerItem.Fields, label, string(val))
if err != nil {
return fmt.Errorf(errUpdateItem, err)
}

if _, err = provider.client.UpdateItem(providerItem, providerItem.Vault.ID); err != nil {
return fmt.Errorf(errUpdateItem, err)
}
return nil
}

// GetSecret returns a single secret from the provider.
Expand Down Expand Up @@ -260,11 +426,11 @@ func (provider *ProviderOnePassword) findItem(name string) (*onepassword.Item, e
case len(items) == 1:
return provider.client.GetItemByUUID(items[0].ID, items[0].Vault.ID)
case len(items) > 1:
return nil, fmt.Errorf(errExpectedOneItem, fmt.Errorf(incorrectCountFormat, name, len(items)))
return nil, fmt.Errorf("%w: '%s', got %d", ErrExpectedOneItem, name, len(items))
}
}

return nil, fmt.Errorf(errKeyNotFound, fmt.Errorf("%s in: %v", name, provider.vaults))
return nil, fmt.Errorf("%w: %s in: %v", ErrKeyNotFound, name, provider.vaults)
}

func (provider *ProviderOnePassword) getField(item *onepassword.Item, property string) ([]byte, error) {
Expand All @@ -275,7 +441,7 @@ func (provider *ProviderOnePassword) getField(item *onepassword.Item, property s
}

if length := countFieldsWithLabel(fieldLabel, item.Fields); length != 1 {
return nil, fmt.Errorf(errExpectedOneField, fmt.Errorf(fieldsWithLabelFormat, fieldLabel, item.Title, length))
return nil, fmt.Errorf("%w: '%s' in '%s', got %d", ErrExpectedOneField, fieldLabel, item.Title, length)
}

// caution: do not use client.GetValue here because it has undesirable behavior on keys with a dot in them
Expand All @@ -297,7 +463,7 @@ func (provider *ProviderOnePassword) getFields(item *onepassword.Item, property
continue
}
if length := countFieldsWithLabel(field.Label, item.Fields); length != 1 {
return nil, fmt.Errorf(errExpectedOneField, fmt.Errorf(fieldsWithLabelFormat, field.Label, item.Title, length))
return nil, fmt.Errorf(errExpectedOneFieldMsgF, ErrExpectedOneField, field.Label, item.Title, length)
}

// caution: do not use client.GetValue here because it has undesirable behavior on keys with a dot in them
Expand All @@ -315,7 +481,7 @@ func (provider *ProviderOnePassword) getAllFields(item onepassword.Item, ref esv
item = *i
for _, field := range item.Fields {
if length := countFieldsWithLabel(field.Label, item.Fields); length != 1 {
return fmt.Errorf(errExpectedOneField, fmt.Errorf(fieldsWithLabelFormat, field.Label, item.Title, length))
return fmt.Errorf(errExpectedOneFieldMsgF, ErrExpectedOneField, field.Label, item.Title, length)
}
if ref.Name != nil {
matcher, err := find.New(*ref.Name)
Expand Down

0 comments on commit f216407

Please sign in to comment.