Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Re-implement support for files/documents using the plugin framework
Browse files Browse the repository at this point in the history
sdahlbac committed May 20, 2024
1 parent cdd2787 commit cb1775c
Showing 10 changed files with 220 additions and 7 deletions.
14 changes: 14 additions & 0 deletions internal/onepassword/cli/op.go
Original file line number Diff line number Diff line change
@@ -208,6 +208,20 @@ func (op *OP) delete(ctx context.Context, item *onepassword.Item, vaultUuid stri
return nil, op.execJson(ctx, nil, nil, p("item"), p("delete"), p(item.ID), f("vault", vaultUuid))
}

func (op *OP) GetFileContent(ctx context.Context, file *onepassword.File, itemUuid, vaultUuid string) ([]byte, error) {
versionErr := op.checkCliVersion(ctx)
if versionErr != nil {
return nil, versionErr
}
ref := fmt.Sprintf("op://%s/%s/%s", vaultUuid, itemUuid, file.ID)
tflog.Debug(ctx, "reading file content from: "+ref)
res, err := op.execRaw(ctx, nil, p("read"), p(ref))
if err != nil {
return nil, err
}
return res, nil
}

func (op *OP) execJson(ctx context.Context, dst any, stdin []byte, args ...opArg) error {
result, err := op.execRaw(ctx, stdin, args...)
if err != nil {
1 change: 1 addition & 0 deletions internal/onepassword/client.go
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ type Client interface {
CreateItem(ctx context.Context, item *onepassword.Item, vaultUuid string) (*onepassword.Item, error)
UpdateItem(ctx context.Context, item *onepassword.Item, vaultUuid string) (*onepassword.Item, error)
DeleteItem(ctx context.Context, item *onepassword.Item, vaultUuid string) error
GetFileContent(ctx context.Context, file *onepassword.File, itemUUid, vaultUuid string) ([]byte, error)
}

type ClientConfig struct {
4 changes: 4 additions & 0 deletions internal/onepassword/connect/connect_client.go
Original file line number Diff line number Diff line change
@@ -39,6 +39,10 @@ func (c *Client) DeleteItem(_ context.Context, item *onepassword.Item, vaultUuid
return c.connectClient.DeleteItem(item, vaultUuid)
}

func (w *Client) GetFileContent(_ context.Context, file *onepassword.File, itemUUID, vaultUUID string) ([]byte, error) {
return w.connectClient.GetFileContent(file)
}

func NewClient(connectHost, connectToken, providerUserAgent string) *Client {
return &Client{connectClient: connect.NewClientWithUserAgent(connectHost, connectToken, providerUserAgent)}
}
8 changes: 8 additions & 0 deletions internal/provider/const.go
Original file line number Diff line number Diff line change
@@ -30,6 +30,13 @@ const (
sectionLabelDescription = "The label for the section."
sectionFieldsDescription = "A list of custom fields in the section."

filesDescription = "A list of files attached to the item."
fileDescription = "A file attached to the item."
fileIDDescription = "A UUID for the file."
fileNameDescription = "The name of the file."
fileContentDescription = "The content of the file."
fileContentBase64Description = "The content of the file in base64 encoding. (Use this for binary files.)"

fieldDescription = "A custom field."
fieldIDDescription = "A unique identifier for the field."
fieldLabelDescription = "The label for the field."
@@ -57,6 +64,7 @@ var (
strings.ToLower(string(op.Password)),
strings.ToLower(string(op.Database)),
strings.ToLower(string(op.SecureNote)),
strings.ToLower(string(op.Document)),
}

fieldPurposes = []string{
47 changes: 47 additions & 0 deletions internal/provider/onepassword_item_data_source.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package provider

import (
"context"
"encoding/base64"
"errors"
"fmt"
"strings"
@@ -47,6 +48,14 @@ type OnePasswordItemDataSourceModel struct {
Password types.String `tfsdk:"password"`
NoteValue types.String `tfsdk:"note_value"`
Section []OnePasswordItemSectionModel `tfsdk:"section"`
File []OnePasswordItemFileModel `tfsdk:"file"`
}

type OnePasswordItemFileModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Content types.String `tfsdk:"content"`
ContentBase64 types.String `tfsdk:"content_base64"`
}

type OnePasswordItemSectionModel struct {
@@ -187,6 +196,31 @@ func (d *OnePasswordItemDataSource) Schema(ctx context.Context, req datasource.S
},
},
},
"file": schema.ListNestedBlock{
MarkdownDescription: filesDescription,
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: fileIDDescription,
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: fileNameDescription,
Computed: true,
},
"content": schema.StringAttribute{
MarkdownDescription: fileContentDescription,
Computed: true,
Sensitive: true,
},
"content_base64": schema.StringAttribute{
MarkdownDescription: fileContentBase64Description,
Computed: true,
Sensitive: true,
},
},
},
},
},
}
}
@@ -298,6 +332,19 @@ func (d *OnePasswordItemDataSource) Read(ctx context.Context, req datasource.Rea
}
}

for _, f := range item.Files {
content, err := d.client.GetFileContent(ctx, f, item.ID, item.Vault.ID)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read file, got error: %s", err))
}
file := OnePasswordItemFileModel{
ID: types.StringValue(f.ID),
Name: types.StringValue(f.Name),
Content: types.StringValue(string(content)),
ContentBase64: types.StringValue(base64.StdEncoding.EncodeToString(content)),
}
data.File = append(data.File, file)
}
// Write logs using the tflog package
// Documentation: https://terraform.io/plugin/log
tflog.Trace(ctx, "read an item data source")
45 changes: 45 additions & 0 deletions internal/provider/onepassword_item_data_source_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package provider

import (
"encoding/base64"
"fmt"
"strings"
"testing"
@@ -138,6 +139,50 @@ func TestAccItemPasswordDatabase(t *testing.T) {
})
}

func TestAccItemDocument(t *testing.T) {
expectedItem := generateDocumentItem()
expectedVault := op.Vault{
ID: expectedItem.Vault.ID,
Name: "Name of the vault",
Description: "This vault will be retrieved",
}

testServer := setupTestServer(expectedItem, expectedVault, t)
defer testServer.Close()

first_content, err := expectedItem.Files[0].Content()
if err != nil {
t.Fatalf("Error getting content of first file: %v", err)
}

second_content, err := expectedItem.Files[1].Content()
if err != nil {
t.Fatalf("Error getting content of second file: %v", err)
}

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccProviderConfig(testServer.URL) + testAccItemDataSourceConfig(expectedItem.Vault.ID, expectedItem.ID),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.onepassword_item.test", "id", fmt.Sprintf("vaults/%s/items/%s", expectedVault.ID, expectedItem.ID)),
resource.TestCheckResourceAttr("data.onepassword_item.test", "vault", expectedVault.ID),
resource.TestCheckResourceAttr("data.onepassword_item.test", "title", expectedItem.Title),
resource.TestCheckResourceAttr("data.onepassword_item.test", "uuid", expectedItem.ID),
resource.TestCheckResourceAttr("data.onepassword_item.test", "category", strings.ToLower(string(expectedItem.Category))),
resource.TestCheckResourceAttr("data.onepassword_item.test", "file.0.id", expectedItem.Files[0].ID),
resource.TestCheckResourceAttr("data.onepassword_item.test", "file.0.name", expectedItem.Files[0].Name),
resource.TestCheckResourceAttr("data.onepassword_item.test", "file.0.content", string(first_content)),
resource.TestCheckResourceAttr("data.onepassword_item.test", "file.1.id", expectedItem.Files[1].ID),
resource.TestCheckResourceAttr("data.onepassword_item.test", "file.1.name", expectedItem.Files[1].Name),
resource.TestCheckResourceAttr("data.onepassword_item.test", "file.1.content_base64", base64.StdEncoding.EncodeToString(second_content)),
),
},
},
})
}

func testAccItemDataSourceConfig(vault, uuid string) string {
return fmt.Sprintf(`
data "onepassword_item" "test" {
2 changes: 2 additions & 0 deletions internal/provider/onepassword_item_resource.go
Original file line number Diff line number Diff line change
@@ -167,6 +167,8 @@ func (r *OnePasswordItemResource) Schema(ctx context.Context, req resource.Schem
Default: stringdefault.StaticString("login"),
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(categories...),
// connect api does not support creating document category
stringvalidator.NoneOf("document"),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
53 changes: 47 additions & 6 deletions internal/provider/onepassword_item_resource_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package provider

import (
"fmt"
"regexp"
"strings"
"testing"

@@ -155,13 +156,40 @@ func TestAccItemResourceWithSections(t *testing.T) {
})
}

func TestAccItemResourceDocument(t *testing.T) {
expectedItem := generateDocumentItem()
expectedVault := op.Vault{
ID: expectedItem.Vault.ID,
Name: "VaultName",
Description: "This vault will be retrieved for testing",
}

testServer := setupTestServer(expectedItem, expectedVault, t)
defer testServer.Close()

resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccProviderConfig(testServer.URL) + testAccDocumentResourceConfig(expectedItem),
// ConfigPlanChecks: resource.ConfigPlanChecks{
// PreApply: []plancheck.PlanCheck{

// plancheck.ExpectUnknownOutputValue("test"),
// },
// },
ExpectError: regexp.MustCompile("Invalid Attribute Value Match"),
},
},
})
}

func testAccDataBaseResourceConfig(expectedItem *op.Item) string {
return fmt.Sprintf(`
data "onepassword_vault" "acceptance-tests" {
uuid = "%s"
}
resource "onepassword_item" "test-database" {
data "onepassword_vault" "acceptance-tests" {
uuid = "%s"
}
resource "onepassword_item" "test-database" {
vault = data.onepassword_vault.acceptance-tests.uuid
title = "%s"
category = "%s"
@@ -179,7 +207,7 @@ func testAccPasswordResourceConfig(expectedItem *op.Item) string {
data "onepassword_vault" "acceptance-tests" {
uuid = "%s"
}
}
resource "onepassword_item" "test-database" {
vault = data.onepassword_vault.acceptance-tests.uuid
title = "%s"
@@ -221,6 +249,19 @@ EOT
}`, expectedItem.Vault.ID, expectedItem.Title, strings.ToLower(string(expectedItem.Category)), strings.TrimSuffix(expectedItem.Fields[0].Value, "\n"))
}

func testAccDocumentResourceConfig(expectedItem *op.Item) string {
return fmt.Sprintf(`
data "onepassword_vault" "acceptance-tests" {
uuid = "%s"
}
resource "onepassword_item" "test-document" {
vault = data.onepassword_vault.acceptance-tests.uuid
title = "%s"
category = "%s"
}`, expectedItem.Vault.ID, expectedItem.Title, strings.ToLower(string(expectedItem.Category)))
}

func testAccResourceWithSectionsConfig(expectedItem *op.Item) string {
return fmt.Sprintf(`
26 changes: 26 additions & 0 deletions internal/provider/test_http_server.go
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ import (
"io"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"

"github.com/1Password/connect-sdk-go/onepassword"
@@ -18,6 +20,16 @@ func setupTestServer(expectedItem *onepassword.Item, expectedVault onepassword.V
t.Errorf("error marshaling item for testing: %s", err)
}

files := expectedItem.Files
var fileBytes [][]byte
for _, file := range files {
c, err := file.Content()
if err != nil {
t.Errorf("error getting file content: %s", err)
}
fileBytes = append(fileBytes, c)
}

vaultBytes, err := json.Marshal(expectedVault)
if err != nil {
t.Errorf("error marshaling vault for testing: %s", err)
@@ -53,6 +65,20 @@ func setupTestServer(expectedItem *onepassword.Item, expectedVault onepassword.V
if err != nil {
t.Errorf("error writing body: %s", err)
}
} else if r.URL.String() == fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s/content", expectedItem.Vault.ID, expectedItem.ID, files[0].ID) ||
r.URL.String() == fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s/content", expectedItem.Vault.ID, expectedItem.ID, files[1].ID) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("1Password-Connect-Version", "1.3.0") // must be >= 1.3.0
i := slices.IndexFunc(files, func(f *onepassword.File) bool {
return f.ID == strings.Split(r.URL.Path, "/")[7]
})
if i == -1 {
t.Errorf("file not found")
}
_, err := w.Write(fileBytes[i])
if err != nil {
t.Errorf("error writing body: %s", err)
}
} else {
t.Errorf("Unexpected request: %s Consider adding this endpoint to the test server", r.URL.String())
}
27 changes: 26 additions & 1 deletion internal/provider/test_utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package provider

import "github.com/1Password/connect-sdk-go/onepassword"
import (
"fmt"

"github.com/1Password/connect-sdk-go/onepassword"
)

func generateBaseItem() onepassword.Item {
item := onepassword.Item{}
@@ -80,6 +84,27 @@ notes
return &item
}

func generateDocumentItem() *onepassword.Item {
item := generateBaseItem()
item.Category = onepassword.Document
item.Files = []*onepassword.File{
{
ID: "ascii",
Name: "ascii",
ContentPath: fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s/content", item.Vault.ID, item.ID, "ascii"),
},
{
ID: "binary",
Name: "binary",
ContentPath: fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s/content", item.Vault.ID, item.ID, "binary"),
},
}
item.Files[0].SetContent([]byte("ascii"))
item.Files[1].SetContent([]byte{0xDE, 0xAD, 0xBE, 0xEF})

return &item
}

func generateDatabaseFields() []*onepassword.ItemField {
fields := []*onepassword.ItemField{
{

0 comments on commit cb1775c

Please sign in to comment.