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 4 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
26 changes: 25 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,12 +47,24 @@ 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) The UUID for the file.
- `name` (String) The name of the file.


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

Read-Only:

- `field` (Block List) (see [below for nested schema](#nestedblock--section--field))
- `file` (Block List) A list of files attached to the section. (see [below for nested schema](#nestedblock--section--file))
- `id` (String) A unique identifier for the section.
- `label` (String) The label for the section.

Expand All @@ -65,3 +78,14 @@ Read-Only:
- `purpose` (String) Purpose indicates this is a special field: a username, password, or notes field.
- `type` (String) The type of value stored in the field.
- `value` (String, Sensitive) The value of the field.


<a id="nestedblock--section--file"></a>
### Nested Schema for `section.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) The UUID for the file.
- `name` (String) The name of the file.
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)}
}
9 changes: 9 additions & 0 deletions internal/provider/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ const (
sectionIDDescription = "A unique identifier for the section."
sectionLabelDescription = "The label for the section."
sectionFieldsDescription = "A list of custom fields in the section."
sectionFilesDescription = "A list of files attached to the section."

filesDescription = "A list of files attached to the item."
fileDescription = "A file attached to the item."
fileIDDescription = "The UUID of 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."
Expand Down Expand Up @@ -58,6 +66,7 @@ var (
strings.ToLower(string(op.Database)),
strings.ToLower(string(op.SecureNote)),
}
dataSourceCategories = append(categories, strings.ToLower(string(op.Document)))

fieldPurposes = []string{
string(op.FieldPurposeUsername),
Expand Down
101 changes: 100 additions & 1 deletion 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,12 +48,21 @@ 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 {
ID types.String `tfsdk:"id"`
Label types.String `tfsdk:"label"`
Field []OnePasswordItemFieldModel `tfsdk:"field"`
File []OnePasswordItemFileModel `tfsdk:"file"`
}

type OnePasswordItemFieldModel struct {
Expand Down Expand Up @@ -98,7 +108,7 @@ func (d *OnePasswordItemDataSource) Schema(ctx context.Context, req datasource.S
Computed: true,
},
"category": schema.StringAttribute{
MarkdownDescription: fmt.Sprintf(enumDescription, categoryDescription, categories),
MarkdownDescription: fmt.Sprintf(enumDescription, categoryDescription, dataSourceCategories),
Computed: true,
},
"url": schema.StringAttribute{
Expand Down Expand Up @@ -184,6 +194,56 @@ func (d *OnePasswordItemDataSource) Schema(ctx context.Context, req datasource.S
},
},
},
"file": schema.ListNestedBlock{
MarkdownDescription: sectionFilesDescription,
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,
},
},
},
},
sdahlbac marked this conversation as resolved.
Show resolved Hide resolved
},
},
},
"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 @@ -267,6 +327,26 @@ func (d *OnePasswordItemDataSource) Read(ctx context.Context, req datasource.Rea
}
}

for _, f := range item.Files {
if f.Section != nil && f.Section.ID == s.ID {
content, err := f.Content()
if err != nil {
// content has not yet been loaded, fetch it
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)),
}
section.File = append(section.File, file)
}
}

data.Section = append(data.Section, section)
}

Expand Down Expand Up @@ -298,6 +378,25 @@ func (d *OnePasswordItemDataSource) Read(ctx context.Context, req datasource.Rea
}
}

for _, f := range item.Files {
if f.Section == nil {
content, err := f.Content()
if err != nil {
// content has not yet been loaded, fetch it
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")
Expand Down
89 changes: 89 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,94 @@ 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 TestAccItemLoginWithFiles(t *testing.T) {
expectedItem := generateLoginItemWithFiles()
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", "section.0.file.0.id", expectedItem.Files[0].ID),
resource.TestCheckResourceAttr("data.onepassword_item.test", "section.0.file.0.name", expectedItem.Files[0].Name),
resource.TestCheckResourceAttr("data.onepassword_item.test", "section.0.file.0.content", string(first_content)),
resource.TestCheckResourceAttr("data.onepassword_item.test", "section.0.file.1.id", expectedItem.Files[1].ID),
resource.TestCheckResourceAttr("data.onepassword_item.test", "section.0.file.1.name", expectedItem.Files[1].Name),
resource.TestCheckResourceAttr("data.onepassword_item.test", "section.0.file.1.content_base64", base64.StdEncoding.EncodeToString(second_content)),
),
},
},
})
}

func testAccItemDataSourceConfig(vault, uuid string) string {
return fmt.Sprintf(`
data "onepassword_item" "test" {
Expand Down
Loading
Loading