diff --git a/go.mod b/go.mod index 565f620..791c535 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/prometheus/common v0.51.1 // indirect github.com/prometheus/procfs v0.13.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect diff --git a/pkg/onepassword/client/connect/connect.go b/pkg/onepassword/client/connect/connect.go new file mode 100644 index 0000000..d1da4ef --- /dev/null +++ b/pkg/onepassword/client/connect/connect.go @@ -0,0 +1,79 @@ +package connect + +import ( + "fmt" + + "github.com/1Password/connect-sdk-go/connect" + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" +) + +// Config holds the configuration for the Connect client. +type Config struct { + ConnectHost string + ConnectToken string + UserAgent string +} + +// Connect is a client for interacting with 1Password using the Connect API. +type Connect struct { + client connect.Client +} + +func NewClient(config Config) *Connect { + return &Connect{ + client: connect.NewClientWithUserAgent(config.ConnectHost, config.ConnectToken, config.UserAgent), + } +} + +func (c *Connect) GetItemByID(vaultID, itemID string) (*model.Item, error) { + connectItem, err := c.client.GetItemByUUID(itemID, vaultID) + if err != nil { + return nil, err + } + + var item model.Item + item.FromConnectItem(connectItem) + return &item, nil +} + +func (c *Connect) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) { + // Get all items in the vault with the specified title + connectItems, err := c.client.GetItemsByTitle(itemTitle, vaultID) + if err != nil { + return nil, err + } + + var items []model.Item + for _, connectItem := range connectItems { + var item model.Item + item.FromConnectItem(&connectItem) + items = append(items, item) + } + + return items, nil +} + +func (c *Connect) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) { + return c.client.GetFileContent(&onepassword.File{ + ContentPath: fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s/content", vaultID, itemID, fileID), + }) +} + +func (c *Connect) GetVaultsByTitle(vaultQuery string) ([]model.Vault, error) { + connectVaults, err := c.client.GetVaultsByTitle(vaultQuery) + if err != nil { + return nil, err + } + + var vaults []model.Vault + for _, connectVault := range connectVaults { + if vaultQuery == connectVault.Name { + var vault model.Vault + vault.FromConnectVault(&connectVault) + vaults = append(vaults, vault) + } + } + + return vaults, nil +} diff --git a/pkg/onepassword/client/connect/connect_test.go b/pkg/onepassword/client/connect/connect_test.go new file mode 100644 index 0000000..557e9ff --- /dev/null +++ b/pkg/onepassword/client/connect/connect_test.go @@ -0,0 +1,269 @@ +package connect + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/1Password/onepassword-operator/pkg/onepassword/client/mock" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" +) + +const VaultTitleEmployee = "Employee" + +func TestConnect_GetItemByID(t *testing.T) { + connectItem := createItem() + + testCases := map[string]struct { + mockClient func() *mock.ConnectClientMock + check func(t *testing.T, item *model.Item, err error) + }{ + "should return an item": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetItemByUUID", "item-id", "vault-id").Return(connectItem, nil) + return mockConnectClient + }, + check: func(t *testing.T, item *model.Item, err error) { + require.NoError(t, err) + checkItem(t, connectItem, item) + }, + }, + "should return an error": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetItemByUUID", "item-id", "vault-id").Return((*onepassword.Item)(nil), errors.New("error")) + return mockConnectClient + }, + check: func(t *testing.T, item *model.Item, err error) { + require.Error(t, err) + require.Nil(t, item) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &Connect{client: tc.mockClient()} + item, err := client.GetItemByID("vault-id", "item-id") + tc.check(t, item, err) + }) + } +} + +func TestConnect_GetItemsByTitle(t *testing.T) { + connectItem1 := createItem() + connectItem2 := createItem() + + testCases := map[string]struct { + mockClient func() *mock.ConnectClientMock + check func(t *testing.T, items []model.Item, err error) + }{ + "should return a single item": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return( + []onepassword.Item{ + *connectItem1, + }, nil) + return mockConnectClient + }, + check: func(t *testing.T, items []model.Item, err error) { + require.NoError(t, err) + require.Len(t, items, 1) + require.Equal(t, connectItem1.ID, items[0].ID) + }, + }, + "should return two items": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return( + []onepassword.Item{ + *connectItem1, + *connectItem2, + }, nil) + return mockConnectClient + }, + check: func(t *testing.T, items []model.Item, err error) { + require.NoError(t, err) + require.Len(t, items, 2) + checkItem(t, connectItem1, &items[0]) + checkItem(t, connectItem2, &items[1]) + }, + }, + "should return an error": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return([]onepassword.Item{}, errors.New("error")) + return mockConnectClient + }, + check: func(t *testing.T, items []model.Item, err error) { + require.Error(t, err) + require.Nil(t, items) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &Connect{client: tc.mockClient()} + items, err := client.GetItemsByTitle("vault-id", "item-title") + tc.check(t, items, err) + }) + } +} + +func TestConnect_GetFileContent(t *testing.T) { + testCases := map[string]struct { + mockClient func() *mock.ConnectClientMock + check func(t *testing.T, content []byte, err error) + }{ + "should return file content": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetFileContent", &onepassword.File{ + ContentPath: "/v1/vaults/vault-id/items/item-id/files/file-id/content", + }).Return([]byte("file content"), nil) + return mockConnectClient + }, + check: func(t *testing.T, content []byte, err error) { + require.NoError(t, err) + require.Equal(t, []byte("file content"), content) + }, + }, + "should return an error": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetFileContent", &onepassword.File{ + ContentPath: "/v1/vaults/vault-id/items/item-id/files/file-id/content", + }).Return(nil, errors.New("error")) + return mockConnectClient + }, + check: func(t *testing.T, content []byte, err error) { + require.Error(t, err) + require.Nil(t, content) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &Connect{client: tc.mockClient()} + content, err := client.GetFileContent("vault-id", "item-id", "file-id") + tc.check(t, content, err) + }) + } +} + +func TestConnect_GetVaultsByTitle(t *testing.T) { + testCases := map[string]struct { + mockClient func() *mock.ConnectClientMock + check func(t *testing.T, vaults []model.Vault, err error) + }{ + "should return a single vault": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{ + { + ID: "test-id", + Name: VaultTitleEmployee, + }, + { + ID: "test-id-2", + Name: "Some other vault", + }, + }, nil) + return mockConnectClient + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.NoError(t, err) + require.Len(t, vaults, 1) + require.Equal(t, "test-id", vaults[0].ID) + }, + }, + "should return a two vaults": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{ + { + ID: "test-id", + Name: VaultTitleEmployee, + }, + { + ID: "test-id-2", + Name: VaultTitleEmployee, + }, + }, nil) + return mockConnectClient + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.NoError(t, err) + require.Len(t, vaults, 2) + // Check the first vault + require.Equal(t, "test-id", vaults[0].ID) + // Check the second vault + require.Equal(t, "test-id-2", vaults[1].ID) + }, + }, + "should return an error": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{}, errors.New("error")) + return mockConnectClient + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.Error(t, err) + require.Empty(t, vaults) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &Connect{client: tc.mockClient()} + vault, err := client.GetVaultsByTitle(VaultTitleEmployee) + tc.check(t, vault, err) + }) + } +} + +func createItem() *onepassword.Item { + return &onepassword.Item{ + ID: "test-id", + Vault: onepassword.ItemVault{ID: "test-vault-id"}, + Version: 1, + Tags: []string{"tag1", "tag2"}, + Fields: []*onepassword.ItemField{ + {Label: "label1", Value: "value1"}, + {Label: "label2", Value: "value2"}, + }, + Files: []*onepassword.File{ + {ID: "file-id-1", Name: "file1.txt", Size: 1234}, + {ID: "file-id-2", Name: "file2.txt", Size: 1234}, + }, + } +} + +func checkItem(t *testing.T, expected *onepassword.Item, actual *model.Item) { + t.Helper() + + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.Vault.ID, actual.VaultID) + require.Equal(t, expected.Version, actual.Version) + require.ElementsMatch(t, expected.Tags, actual.Tags) + + for i, field := range expected.Fields { + require.Equal(t, field.Label, actual.Fields[i].Label) + require.Equal(t, field.Value, actual.Fields[i].Value) + } + + for i, file := range expected.Files { + require.Equal(t, file.ID, actual.Files[i].ID) + require.Equal(t, file.Name, actual.Files[i].Name) + require.Equal(t, file.Size, actual.Files[i].Size) + } + + require.Equal(t, expected.CreatedAt, actual.CreatedAt) +} diff --git a/pkg/onepassword/client/mock/connect.go b/pkg/onepassword/client/mock/connect.go new file mode 100644 index 0000000..a38c4f6 --- /dev/null +++ b/pkg/onepassword/client/mock/connect.go @@ -0,0 +1,130 @@ +package mock + +import ( + "github.com/stretchr/testify/mock" + + "github.com/1Password/connect-sdk-go/onepassword" +) + +// ConnectClientMock is a mock implementation of the ConnectClient interface +type ConnectClientMock struct { + mock.Mock +} + +func (c *ConnectClientMock) GetVaults() ([]onepassword.Vault, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetVault(uuid string) (*onepassword.Vault, error) { + args := c.Called(uuid) + return args.Get(0).(*onepassword.Vault), args.Error(1) +} + +func (c *ConnectClientMock) GetVaultByUUID(uuid string) (*onepassword.Vault, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetVaultByTitle(title string) (*onepassword.Vault, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetVaultsByTitle(title string) ([]onepassword.Vault, error) { + args := c.Called(title) + return args.Get(0).([]onepassword.Vault), args.Error(1) +} + +func (c *ConnectClientMock) GetItems(vaultQuery string) ([]onepassword.Item, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetItem(itemQuery, vaultQuery string) (*onepassword.Item, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) { + args := c.Called(uuid, vaultQuery) + return args.Get(0).(*onepassword.Item), args.Error(1) +} + +func (c *ConnectClientMock) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) { + args := c.Called(title, vaultQuery) + return args.Get(0).([]onepassword.Item), args.Error(1) +} + +func (c *ConnectClientMock) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) DeleteItem(item *onepassword.Item, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) DeleteItemByID(itemUUID string, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) DeleteItemByTitle(title string, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetFileContent(file *onepassword.File) ([]byte, error) { + args := c.Called(file) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]byte), args.Error(1) +} + +func (c *ConnectClientMock) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) LoadStruct(config interface{}) error { + //TODO implement me + panic("implement me") +}