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/document on plugin framework #171

Merged
merged 6 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/data-sources/item.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ data "onepassword_item" "example" {

### Read-Only

- `category` (String) The category of the item. One of ["login" "password" "database" "secure_note"]
- `category` (String) The category of the item. One of ["login" "password" "database" "secure_note" "document"]
- `database` (String) (Only applies to the database category) The name of the database.
- `file` (Block List) A list of files attached to the item. (see [below for nested schema](#nestedblock--file))
- `hostname` (String) (Only applies to the database category) The address where the database can be found
- `id` (String) The Terraform resource identifier for this item in the format `vaults/<vault_id>/items/<item_id>`.
- `password` (String, Sensitive) Password for this item.
Expand All @@ -46,6 +47,17 @@ data "onepassword_item" "example" {
- `url` (String) The primary URL for the item.
- `username` (String) Username for this item.

<a id="nestedblock--file"></a>
### Nested Schema for `file`

Read-Only:

- `content` (String, Sensitive) The content of the file.
- `content_base64` (String, Sensitive) The content of the file in base64 encoding. (Use this for binary files.)
- `id` (String) A UUID for the file.
- `name` (String) The name of the file.


<a id="nestedblock--section"></a>
### Nested Schema for `section`

Expand Down
2 changes: 1 addition & 1 deletion docs/resources/item.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ resource "onepassword_item" "demo_db" {

### Optional

- `category` (String) The category of the item. One of ["login" "password" "database" "secure_note"]
- `category` (String) The category of the item. One of ["login" "password" "database" "secure_note" "document"]
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
- `database` (String) (Only applies to the database category) The name of the database.
- `hostname` (String) (Only applies to the database category) The address where the database can be found
- `note_value` (String, Sensitive) Secure Note value.
Expand Down
14 changes: 14 additions & 0 deletions internal/onepassword/cli/op.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down
1 change: 1 addition & 0 deletions internal/onepassword/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions internal/onepassword/connect/connect_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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."
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
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."
Expand Down Expand Up @@ -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)),
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
}

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

import (
"context"
"encoding/base64"
"errors"
"fmt"
"strings"
Expand Down Expand Up @@ -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"`
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
}

type OnePasswordItemSectionModel struct {
Expand Down Expand Up @@ -187,6 +196,31 @@ func (d *OnePasswordItemDataSource) Schema(ctx context.Context, req datasource.S
},
},
},
"file": schema.ListNestedBlock{
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
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,
},
},
},
},
},
}
}
Expand Down Expand Up @@ -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)
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
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")
Expand Down
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"
Expand Down Expand Up @@ -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" {
Expand Down
2 changes: 2 additions & 0 deletions internal/provider/onepassword_item_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
47 changes: 41 additions & 6 deletions internal/provider/onepassword_item_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package provider

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

Expand Down Expand Up @@ -155,13 +156,34 @@ func TestAccItemResourceWithSections(t *testing.T) {
})
}

func TestAccItemResourceDocument(t *testing.T) {
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
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),
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" {
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
vault = data.onepassword_vault.acceptance-tests.uuid
title = "%s"
category = "%s"
Expand All @@ -179,7 +201,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"
Expand Down Expand Up @@ -221,6 +243,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(`

Expand Down
26 changes: 26 additions & 0 deletions internal/provider/test_http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"io"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"

"github.com/1Password/connect-sdk-go/onepassword"
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
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())
}
Expand Down
Loading
Loading