diff --git a/docs/data-sources/item.md b/docs/data-sources/item.md index b41f47b9..c0e4fe29 100644 --- a/docs/data-sources/item.md +++ b/docs/data-sources/item.md @@ -34,8 +34,9 @@ data "onepassword_item" "example" { ### Read-Only -- `category` (String) The category of the item. One of ["login" "password" "database"] +- `category` (String) The category of the item. One of ["login" "password" "database" "document"] - `database` (String) (Only applies to the database category) The name of the database. +- `file` (List of Object) A list of files in the section. (see [below for nested schema](#nestedatt--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//items/` - `password` (String, Sensitive) Password for this item. @@ -46,6 +47,17 @@ data "onepassword_item" "example" { - `url` (String) The primary URL for the item. - `username` (String) Username for this item. + +### Nested Schema for `file` + +Read-Only: + +- `content` (String) +- `content_base64` (String) +- `id` (String) +- `name` (String) + + ### Nested Schema for `section` diff --git a/docs/resources/item.md b/docs/resources/item.md index 51ba93b9..43bd364e 100644 --- a/docs/resources/item.md +++ b/docs/resources/item.md @@ -66,7 +66,7 @@ resource "onepassword_item" "demo_db" { ### Optional -- `category` (String) The category of the item. One of ["login" "password" "database"] +- `category` (String) The category of the item. One of ["login" "password" "database" "document"] - `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 - `password` (String, Sensitive) Password for this item. diff --git a/onepassword/cli/op.go b/onepassword/cli/op.go index 6f914002..12201244 100644 --- a/onepassword/cli/op.go +++ b/onepassword/cli/op.go @@ -107,6 +107,19 @@ func (op *OP) GetItemByTitle(ctx context.Context, title string, vaultUuid string return op.GetItem(ctx, title, 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) + res, err := op.execRaw(ctx, nil, p("read"), p(ref)) + if err != nil { + return nil, err + } + return res, nil +} + func (op *OP) CreateItem(ctx context.Context, item *onepassword.Item, vaultUuid string) (*onepassword.Item, error) { versionErr := op.checkCliVersion(ctx) if versionErr != nil { diff --git a/onepassword/connectctx/wrapper.go b/onepassword/connectctx/wrapper.go index 0e2678b4..b15edce8 100644 --- a/onepassword/connectctx/wrapper.go +++ b/onepassword/connectctx/wrapper.go @@ -39,6 +39,10 @@ func (w *Wrapper) DeleteItem(_ context.Context, item *onepassword.Item, vaultUui return w.client.DeleteItem(item, vaultUuid) } +func (w *Wrapper) GetFileContent(_ context.Context, file *onepassword.File, itemUUID, vaultUUID string) ([]byte, error) { + return w.client.GetFileContent(file) +} + func Wrap(client connect.Client) *Wrapper { return &Wrapper{client: client} } diff --git a/onepassword/data_source_onepassword_item.go b/onepassword/data_source_onepassword_item.go index 9d1088ac..b71b7c4f 100644 --- a/onepassword/data_source_onepassword_item.go +++ b/onepassword/data_source_onepassword_item.go @@ -2,6 +2,7 @@ package onepassword import ( "context" + "encoding/base64" "errors" "fmt" "strings" @@ -11,6 +12,22 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +type FileSchema struct { + Name string + Id string + Content string + ContentBase64 string +} + +func (f FileSchema) toResourceData() map[string]string { + return map[string]string { + "name": f.Name, + "id": f.Id, + "content": f.Content, + "content_base64": f.ContentBase64, + } +} + func dataSourceOnepasswordItem() *schema.Resource { exactlyOneOfUUIDAndTitle := []string{"uuid", "title"} @@ -154,6 +171,39 @@ func dataSourceOnepasswordItem() *schema.Resource { }, }, }, + "file": { + Description: filesListDescription, + Type: schema.TypeList, + Computed: true, + MinItems: 0, + Elem: &schema.Resource{ + Description: fileDescription, + Schema: map[string]*schema.Schema{ + "id": { + Description: fileIDDescription, + Type: schema.TypeString, + Computed: true, + }, + "name": { + Description: fileNameDescription, + Type: schema.TypeString, + Computed: true, + }, + "content": { + Description: fileContentDescription, + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + "content_base64": { + Description: fileContentBase64Description, + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + }, + }, }, } } @@ -177,6 +227,22 @@ func dataSourceOnepasswordItemRead(ctx context.Context, data *schema.ResourceDat } } + dataFiles := []map[string]string{} + for _, f := range item.Files { + file := FileSchema{} + file.Name = f.Name + file.Id = f.ID + data, err := client.GetFileContent(ctx, f, item.ID, item.Vault.ID) + if err != nil { + return diag.FromErr(err) + } + file.Content = string(data) + file.ContentBase64 = base64.StdEncoding.EncodeToString(data) + + dataFiles = append(dataFiles, file.toResourceData()) + } + data.Set("file", dataFiles) + data.Set("tags", item.Tags) data.Set("category", strings.ToLower(string(item.Category))) diff --git a/onepassword/data_source_onepassword_item_test.go b/onepassword/data_source_onepassword_item_test.go index e8d694a5..543cb38a 100644 --- a/onepassword/data_source_onepassword_item_test.go +++ b/onepassword/data_source_onepassword_item_test.go @@ -28,6 +28,34 @@ func TestDataSourceOnePasswordItemRead(t *testing.T) { compareItemToSource(t, dataSourceData, expectedItem) } +func TestDataSourceOnePasswordItemDocumentRead(t *testing.T) { + expectedItem := generateItem() + expectedItem.Category = "DOCUMENT" + expectedItem.Files = []*onepassword.File{ + { + Name: "test_file", + }, + } + expectedItem.Files[0].SetContent([]byte("test_content")) + meta := &testClient{ + GetItemFunc: func(uuid string, vaultUUID string) (*onepassword.Item, error) { + return expectedItem, nil + }, + GetFileFunc: func(file *onepassword.File, itemUUID, vaultUUID string) ([]byte, error) { + return []byte("test_content"), nil + }, + } + + dataSourceData := generateDataSource(t, expectedItem) + dataSourceData.Set("uuid", expectedItem.ID) + + err := dataSourceOnepasswordItemRead(context.Background(), dataSourceData, meta) + if err != nil { + t.Errorf("Unexpected error occured") + } + compareItemToSource(t, dataSourceData, expectedItem) +} + func TestDataSourceOnePasswordItemReadByTitle(t *testing.T) { expectedItem := generateItem() meta := &testClient{ @@ -126,6 +154,22 @@ func compareItemToSource(t *testing.T, dataSourceData *schema.ResourceData, item t.Errorf("Expected field %v to be %v got %v", f.Label, f.Value, dataSourceData.Get(path)) } } + if files := dataSourceData.Get("file"); files != nil && len(item.Files) != len(files.([]interface{})) { + got := len(files.([]interface{})) + t.Errorf("Expected %v files got %v", len(item.Files), got) + } + for i, file := range item.Files { + if dataSourceData.Get(fmt.Sprintf("file.%s", file.Name)) == nil { + t.Errorf("Expected file %v to be present", file.Name) + } + want, err := file.Content() + if err != nil { + t.Errorf("Unexpected error occured") + } + if dataSourceData.Get(fmt.Sprintf("file.%d.content", i)).(string) != string(want) { + t.Errorf("Expected file %v to have content %v, got %v", file.Name, string(want), dataSourceData.Get(fmt.Sprintf("file.%d.content", i))) + } + } } func generateDataSource(t *testing.T, item *onepassword.Item) *schema.ResourceData { diff --git a/onepassword/mock_client_test.go b/onepassword/mock_client_test.go index bffb07f4..2737a2a9 100644 --- a/onepassword/mock_client_test.go +++ b/onepassword/mock_client_test.go @@ -14,6 +14,7 @@ type testClient struct { CreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) UpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) DeleteItemFunc func(item *onepassword.Item, vaultUUID string) error + GetFileFunc func(file *onepassword.File, itemUUID, vaultUUID string) ([]byte, error) } var _ Client = (*testClient)(nil) @@ -45,3 +46,7 @@ func (m *testClient) DeleteItem(_ context.Context, item *onepassword.Item, vault func (m *testClient) UpdateItem(_ context.Context, item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) { return m.UpdateItemFunc(item, vaultUUID) } + +func (m *testClient) GetFileContent(_ context.Context, file *onepassword.File, itemUUID, vaultUUID string) ([]byte, error) { + return m.GetFileFunc(file, itemUUID, vaultUUID) +} diff --git a/onepassword/provider.go b/onepassword/provider.go index 1430551d..a3a04dc4 100644 --- a/onepassword/provider.go +++ b/onepassword/provider.go @@ -134,4 +134,5 @@ 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) } diff --git a/onepassword/resource_onepassword_item.go b/onepassword/resource_onepassword_item.go index 64749a52..5e22ab59 100644 --- a/onepassword/resource_onepassword_item.go +++ b/onepassword/resource_onepassword_item.go @@ -38,6 +38,8 @@ const ( sectionLabelDescription = "The label for the section." sectionFieldsDescription = "A list of custom fields in the section." + filesListDescription = "A list of files in an item." + fieldDescription = "A custom field." fieldIDDescription = "A unique identifier for the field." fieldLabelDescription = "The label for the field." @@ -45,6 +47,12 @@ const ( fieldTypeDescription = "The type of value stored in the field." fieldValueDescription = "The value of the field." + 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.)" + passwordRecipeDescription = "The recipe used to generate a new value for a password." passwordElementDescription = "The kinds of characters to include in the password." passwordLengthDescription = "The length of the password to be generated." @@ -55,7 +63,7 @@ const ( enumDescription = "%s One of %q" ) -var categories = []string{"login", "password", "database"} +var categories = []string{"login", "password", "database", "document"} var dbTypes = []string{"db2", "filemaker", "msaccess", "mssql", "mysql", "oracle", "postgresql", "sqlite", "other"} var fieldPurposes = []string{"USERNAME", "PASSWORD", "NOTES"} var fieldTypes = []string{"STRING", "EMAIL", "CONCEALED", "URL", "OTP", "DATE", "MONTH_YEAR", "MENU"} @@ -133,12 +141,20 @@ func resourceOnepasswordItem() *schema.Resource { ForceNew: true, }, "category": { - Description: fmt.Sprintf(enumDescription, categoryDescription, categories), - Type: schema.TypeString, - Optional: true, - Default: "login", - ValidateFunc: validation.StringInSlice(categories, true), - ForceNew: true, + Description: fmt.Sprintf(enumDescription, categoryDescription, categories), + Type: schema.TypeString, + Optional: true, + Default: "login", + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + f := validation.StringInSlice(categories, true) + warns, errs = f(v, key) + if v == "document" { + errs = append(errs, fmt.Errorf("cannot create document category, connect api do not support it")) + } + return warns, errs + }, + ForceNew: true, }, "title": { Description: itemTitleDescription,