Skip to content

Commit

Permalink
Enable retrieving file attachments and Document items (#171)
Browse files Browse the repository at this point in the history
- Add support for Document item category for the Item data source.
- Add support for fetching file attachments when retrieving an item.
- Add support for both text documents and binary documents (provided one uses the content_base64 version)
  • Loading branch information
sdahlbac authored Jun 17, 2024
1 parent bcbfdb0 commit 1e24ccf
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 5 deletions.
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 of 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. One of ["USERNAME" "PASSWORD" "NOTES"]
- `type` (String) The type of value stored in the field. One of ["STRING" "CONCEALED" "EMAIL" "URL" "OTP" "DATE" "MONTH_YEAR" "MENU"]
- `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 of 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) {
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
82 changes: 81 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"`
}

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 All @@ -68,6 +78,29 @@ func (d *OnePasswordItemDataSource) Metadata(ctx context.Context, req datasource
}

func (d *OnePasswordItemDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
fileNestedObjectSchema := 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,
},
},
}

resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "Use this data source to get details of an item by its vault uuid and either the title or the uuid of the item.",
Expand Down Expand Up @@ -98,7 +131,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,9 +217,17 @@ func (d *OnePasswordItemDataSource) Schema(ctx context.Context, req datasource.S
},
},
},
"file": schema.ListNestedBlock{
MarkdownDescription: sectionFilesDescription,
NestedObject: fileNestedObjectSchema,
},
},
},
},
"file": schema.ListNestedBlock{
MarkdownDescription: filesDescription,
NestedObject: fileNestedObjectSchema,
},
},
}
}
Expand Down Expand Up @@ -267,6 +308,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 +359,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

0 comments on commit 1e24ccf

Please sign in to comment.