Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add PushSecret and DeleteSecret to onepassword provider #2646

Merged
merged 4 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/introduction/stability-support.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.
bthuilot marked this conversation as resolved.
Show resolved Hide resolved
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]
bthuilot marked this conversation as resolved.
Show resolved Hide resolved
// Get the label
label := ref.GetProperty()
if label == "" {
label = passwordLabel
bthuilot marked this conversation as resolved.
Show resolved Hide resolved
}

// 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()]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to update your branch. :) GetSecretKey is no longer a required value. And it's possible to push a whole secret now as is. So defining a key might not be required as is.

It doesn't mean that it's not required here, but keep in mind that people might want to push the entire secret to 1password as is.

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
bthuilot marked this conversation as resolved.
Show resolved Hide resolved
}

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))
bthuilot marked this conversation as resolved.
Show resolved Hide resolved
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