Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
9 changes: 7 additions & 2 deletions docs/resources/item.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ resource "onepassword_item" "example" {

### 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" "ssh_key"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ssh_key category should be added to internal/provider/const.go as well.

- `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.
- `password` (String, Sensitive) Password for this item.
- `password_recipe` (Block List) The recipe used to generate a new value for a password. (see [below for nested schema](#nestedblock--password_recipe))
- `port` (String) (Only applies to the database category) The port the database is listening on.
- `private_key` (String, Sensitive) SSH Private Key for this item.
- `public_key` (String) SSH Public Key for this item.
Comment on lines +55 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 1Password it represents 4 fields for ssh key

Image

But the item created in 1password contains only 2 fields
Image

I think it would be good to have all 4 fields when creating an item via Terraform to keep consistency.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The connect sdk that the provider uses appears to have some issues creating ssh keys. I've noticed that in testing this fork, ssh keys I create via connect cannot be read back by op cli; when I try the cli gives this error:

[ERROR] 2025/10/24 16:47:19 "private_key" isn't a field in the "Example SSH Key Item" item

This causes issues because op cli (or terraform connecting via service account rather than via connect) cannot delete/modify the resulting resource.

I can't see a 'proper' way to create SSH keys via connect. Everything I try results in a record op cli cannot process.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the same is true for items created via service account. The resulting item cannot be read by op cli, causing terraform destroy to fail:

│ Error: 1Password Item read error │ │ with onepassword_item.demo_ssh_key, │ on main.tf line 127, in resource "onepassword_item" "demo_ssh_key": │ 127: resource "onepassword_item" "demo_ssh_key" { │ │ Could not get item 'snip' from vault 'snip', got error: op error: "private_key" isn't a field in the "Example Terraform SSH Key Item" item

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have got my fork working for generating an SSH key from scratch when using a service account:

i.e. in OP.create:

...
	if item.Category == onepassword.SSHKey {
		return op.createSSHKey(ctx, item, vaultUuid)
	}
...

and then a new method for ssh keys:

func (op *OP) createSSHKey(ctx context.Context, item *onepassword.Item, vaultUuid string) (*onepassword.Item, error) {
	args := []opArg{
		p("item"), p("create"),
		f("category", "SSH Key"),
		f("title", item.Title),
		f("vault", vaultUuid),
	}

	if len(item.Tags) > 0 {
		args = append(args, f("tags", strings.Join(item.Tags, ",")))
	}

	var res *onepassword.Item
	err := op.execJson(ctx, &res, nil, args...)
	if err != nil {
		return nil, err
	}
	return res, nil
}

I will revisit the logic for when we use 1Password Connect on Monday, but I think it will require a patch to the connect SDK to work consistently there.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@twbrowning @volodymyrZotov, thanks for reviewing this! Unfortunately I've had to deprioritize my Terraform work for now. If you have any ideas on how to solve the issues, and properly integrate with the Connect server, then feel free to submit patches to this branch. Otherwise I will pick this up when I can return to Terraform again.

- `section` (Block List) A list of custom sections in an item (see [below for nested schema](#nestedblock--section))
- `tags` (List of String) An array of strings of the tags assigned to the item.
- `title` (String) The title of the item.
Expand All @@ -65,6 +67,7 @@ resource "onepassword_item" "example" {
- `uuid` (String) The UUID of the item. Item identifiers are unique within a specific vault.

<a id="nestedblock--password_recipe"></a>

### Nested Schema for `password_recipe`

Optional:
Expand All @@ -74,8 +77,8 @@ Optional:
- `letters` (Boolean) Use letters [a-zA-Z] when generating the password.
- `symbols` (Boolean) Use symbols [[email protected]_*] when generating the password.


<a id="nestedblock--section"></a>

### Nested Schema for `section`

Required:
Expand All @@ -91,6 +94,7 @@ Read-Only:
- `id` (String) A unique identifier for the section.

<a id="nestedblock--section--field"></a>

### Nested Schema for `section.field`

Required:
Expand All @@ -106,6 +110,7 @@ Optional:
- `value` (String, Sensitive) The value of the field.

<a id="nestedblock--section--field--password_recipe"></a>

### Nested Schema for `section.field.password_recipe`

Optional:
Expand Down
45 changes: 45 additions & 0 deletions internal/provider/onepassword_item_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ type OnePasswordItemResourceModel struct {
NoteValue types.String `tfsdk:"note_value"`
Section []OnePasswordItemResourceSectionModel `tfsdk:"section"`
Recipe []PasswordRecipeModel `tfsdk:"password_recipe"`
PrivateKey types.String `tfsdk:"private_key"`
PublicKey types.String `tfsdk:"public_key"`
}

type PasswordRecipeModel struct {
Expand Down Expand Up @@ -223,6 +225,23 @@ func (r *OnePasswordItemResource) Schema(ctx context.Context, req resource.Schem
Optional: true,
Sensitive: true,
},
"private_key": schema.StringAttribute{
MarkdownDescription: publicKeyDescription,
Computed: true,
Optional: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"public_key": schema.StringAttribute{
MarkdownDescription: privateKeyDescription,
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
Blocks: map[string]schema.Block{
"section": schema.ListNestedBlock{
Expand Down Expand Up @@ -609,6 +628,13 @@ func itemToData(ctx context.Context, item *op.Item, data *OnePasswordItemResourc
data.Password = types.StringNull()
}

if data.PrivateKey.IsUnknown() {
data.PrivateKey = types.StringNull()
}
if data.PublicKey.IsUnknown() {
data.PublicKey = types.StringNull()
}

return nil
}

Expand Down Expand Up @@ -740,6 +766,25 @@ func dataToItem(ctx context.Context, data OnePasswordItemResourceModel) (*op.Ite
Value: data.NoteValue.ValueString(),
},
}
case "ssh_key":
item.Category = op.SSHKey
item.Fields = []*op.ItemField{}
if !data.PrivateKey.IsNull() && !data.PrivateKey.IsUnknown() {
item.Fields = append(item.Fields, &op.ItemField{
ID: "private_key",
Label: "private key",
Type: op.FieldTypeConcealed,
Value: data.PrivateKey.ValueString(),
})
}
if !data.PublicKey.IsNull() && !data.PublicKey.IsUnknown() {
item.Fields = append(item.Fields, &op.ItemField{
ID: "public_key",
Label: "public key",
Type: op.FieldTypeString,
Value: data.PublicKey.ValueString(),
})
}
case "secure_note":
item.Category = op.SecureNote
item.Fields = []*op.ItemField{
Expand Down
30 changes: 30 additions & 0 deletions internal/provider/onepassword_item_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,36 @@ func TestAccItemResourceDocument(t *testing.T) {
})
}

func TestAccItemResourceSSHKey(t *testing.T) {
expectedItem := generateSSHKeyItem()
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()

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", "private_key", expectedItem.Fields[0].Value),
resource.TestCheckResourceAttr("data.onepassword_item.test", "public_key", expectedItem.Fields[1].Value),
),
},
},
})
}

func testAccDataBaseResourceConfig(expectedItem *op.Item) string {
return fmt.Sprintf(`

Expand Down