Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions docs/resources/item.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,16 @@ resource "onepassword_item" "example" {

### Optional

> **NOTE**: [Write-only arguments](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments) are supported in Terraform 1.11 and later.

- `category` (String) The category of the item. One of ["login" "password" "database" "secure_note"]
- `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))
- `password_wo` (String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) A write-only password for this item. This value is not stored in the state and is intended for use with ephemeral values.
- `password_wo_version` (Number) An integer that must be incremented to trigger an update to the 'password_wo' field.
- `port` (String) (Only applies to the database category) The port the database is listening on.
- `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.
Expand Down Expand Up @@ -117,6 +121,8 @@ Optional:

Import is supported using the following syntax:

The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example:

```shell
# import an existing 1Password item
terraform import onepassword_item.myitem vaults/<vault uuid>/items/<item uuid>
Expand Down
2 changes: 2 additions & 0 deletions internal/provider/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const (
tagsDescription = "An array of strings of the tags assigned to the item."
usernameDescription = "Username for this item."
passwordDescription = "Password for this item."
passwordWriteOnceDescription = "A write-only password for this item. This value is not stored in the state and is intended for use with ephemeral values."
passwordWriteOnceVersionDescription = "An integer that must be incremented to trigger an update to the 'password_wo' field."
credentialDescription = "API credential for this item."
noteValueDescription = "Secure Note value."
publicKeyDescription = "SSH Public Key for this item."
Expand Down
115 changes: 99 additions & 16 deletions internal/provider/onepassword_item_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,24 @@

// OnePasswordItemResourceModel describes the resource data model.
type OnePasswordItemResourceModel struct {
ID types.String `tfsdk:"id"`
UUID types.String `tfsdk:"uuid"`
Vault types.String `tfsdk:"vault"`
Category types.String `tfsdk:"category"`
Title types.String `tfsdk:"title"`
URL types.String `tfsdk:"url"`
Hostname types.String `tfsdk:"hostname"`
Database types.String `tfsdk:"database"`
Port types.String `tfsdk:"port"`
Type types.String `tfsdk:"type"`
Tags types.List `tfsdk:"tags"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
NoteValue types.String `tfsdk:"note_value"`
Section []OnePasswordItemResourceSectionModel `tfsdk:"section"`
Recipe []PasswordRecipeModel `tfsdk:"password_recipe"`
ID types.String `tfsdk:"id"`
UUID types.String `tfsdk:"uuid"`
Vault types.String `tfsdk:"vault"`
Category types.String `tfsdk:"category"`
Title types.String `tfsdk:"title"`
URL types.String `tfsdk:"url"`
Hostname types.String `tfsdk:"hostname"`
Database types.String `tfsdk:"database"`
Port types.String `tfsdk:"port"`
Type types.String `tfsdk:"type"`
Tags types.List `tfsdk:"tags"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
PasswordWO types.String `tfsdk:"password_wo"`
PasswordWOVersion types.Int64 `tfsdk:"password_wo_version"`
NoteValue types.String `tfsdk:"note_value"`
Section []OnePasswordItemResourceSectionModel `tfsdk:"section"`
Recipe []PasswordRecipeModel `tfsdk:"password_recipe"`
}

type PasswordRecipeModel struct {
Expand Down Expand Up @@ -211,6 +213,32 @@
ValueModifier(),
},
},
"password_wo": schema.StringAttribute{
MarkdownDescription: passwordWriteOnceDescription,
Optional: true,
Sensitive: true,
WriteOnly: true,

Check failure on line 220 in internal/provider/onepassword_item_resource.go

View workflow job for this annotation

GitHub Actions / Verify docs

unknown field WriteOnly in struct literal of type "github.com/hashicorp/terraform-plugin-framework/resource/schema".StringAttribute

Check failure on line 220 in internal/provider/onepassword_item_resource.go

View workflow job for this annotation

GitHub Actions / Build

unknown field WriteOnly in struct literal of type "github.com/hashicorp/terraform-plugin-framework/resource/schema".StringAttribute
Validators: []validator.String{
stringvalidator.ConflictsWith(
path.Expressions{path.MatchRoot("password")}...,
),
stringvalidator.AlsoRequires(
path.Expressions{path.MatchRoot("password_wo_version")}...,
),
},
},
"password_wo_version": schema.Int64Attribute{
MarkdownDescription: passwordWriteOnceVersionDescription,
Optional: true,
Validators: []validator.Int64{
int64validator.ConflictsWith(
path.Expressions{path.MatchRoot("password")}...,
),
int64validator.AlsoRequires(
path.Expressions{path.MatchRoot("password_wo")}...,
),
},
},
"note_value": schema.StringAttribute{
MarkdownDescription: noteValueDescription,
Optional: true,
Expand Down Expand Up @@ -312,14 +340,27 @@

func (r *OnePasswordItemResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data OnePasswordItemResourceModel
var config OnePasswordItemResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

// Read the config to get the original password_wo value as it's not stored nor inside plan neither the state.
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

// Use the password_wo as password for creation when wo variant is used.
writeOnly := false
if !config.PasswordWO.IsNull() && !config.PasswordWO.IsUnknown() {
data.Password = config.PasswordWO
writeOnly = true
}

// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
item, diagnostics := dataToItem(ctx, data)
Expand All @@ -339,6 +380,11 @@
return
}

// Once created, clear password from state if wo variant is used as password should never be stored
if writeOnly {
data.Password = types.StringNull()
}

// Write logs using the tflog package
// Documentation: https://terraform.io/plugin/log
tflog.Trace(ctx, "created a resource")
Expand All @@ -357,6 +403,12 @@
return
}

// Check if wo variant is used based on the wo_version stored in the prior state
writeOnly := false
if !data.PasswordWOVersion.IsNull() {
writeOnly = true
}

// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
vaultUUID, itemUUID := vaultAndItemUUID(data.ID.ValueString())
Expand All @@ -377,19 +429,45 @@
return
}

// Once read, clear password from state if wo variant is used as password should never be stored
if writeOnly {
data.Password = types.StringNull()
}

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *OnePasswordItemResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data OnePasswordItemResourceModel
var config OnePasswordItemResourceModel
var state OnePasswordItemResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

// Read the config to get the current password_wo value as it's not stored nor inside plan neither the state.
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

// Read the previous state to detect if the password_wo_version should trigger a password update.
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

// Ensure password is field with new wo value if the current config version is != from the previous state one.
writeOnce := false
if !config.PasswordWOVersion.IsNull() && config.PasswordWOVersion != state.PasswordWOVersion {
data.Password = config.PasswordWO
writeOnce = true
}

// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
item, diagnostics := dataToItem(ctx, data)
Expand All @@ -412,6 +490,11 @@
return
}

// Once updated, always clear password from state - as it should never be stored when wo variant is used.
if writeOnce {
data.Password = types.StringNull()
}

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
Expand Down
120 changes: 120 additions & 0 deletions internal/provider/onepassword_item_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,63 @@ func TestAccItemResourceDocument(t *testing.T) {
})
}

func TestAccItemResource_PasswordWriteOnly(t *testing.T) {
expectedItem := generatePasswordItem()
expectedVault := op.Vault{
ID: expectedItem.Vault.ID,
Name: "VaultName",
}

testServer := setupTestServer(expectedItem, expectedVault, t)
defer testServer.Close()

resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
// Test read
Config: testAccProviderConfig(testServer.URL) + testAccPasswordWriteOnlyResourceConfig(expectedItem, "1"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("onepassword_item.test_wo", "title", expectedItem.Title),
resource.TestCheckResourceAttr("onepassword_item.test_wo", "category", strings.ToLower(string(expectedItem.Category))),
resource.TestCheckResourceAttr("onepassword_item.test_wo", "password_wo_version", "1"),
resource.TestCheckNoResourceAttr("onepassword_item.test_wo", "password"),
resource.TestCheckNoResourceAttr("onepassword_item.test_wo", "password_wo"),
),
},
},
})
}

func TestAccItemResource_PasswordWriteOnlyAttributes(t *testing.T) {
expectedItem := generatePasswordItem()
expectedVault := op.Vault{
ID: expectedItem.Vault.ID,
Name: "VaultName",
}

testServer := setupTestServer(expectedItem, expectedVault, t)
defer testServer.Close()

resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccProviderConfig(testServer.URL) + testAccPasswordWriteOnlyMissingVersionConfig(expectedItem),
ExpectError: regexp.MustCompile("Attribute \"password_wo_version\" must be specified when \"password_wo\" is"),
},
{
Config: testAccProviderConfig(testServer.URL) + testAccPasswordWriteOnlyMissingPasswordConfig(expectedItem),
ExpectError: regexp.MustCompile("Attribute \"password_wo\" must be specified when \"password_wo_version\" is"),
},
{
Config: testAccProviderConfig(testServer.URL) + testAccPasswordWriteOnlyConflictPasswordConfig(expectedItem),
ExpectError: regexp.MustCompile("Attribute \"password\" cannot be specified when \"password_wo\" is specified"),
},
},
})
}

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

Expand Down Expand Up @@ -214,6 +271,69 @@ resource "onepassword_item" "test-database" {
}`, expectedItem.VaultID, expectedItem.Title, strings.ToLower(string(expectedItem.Category)), expectedItem.Fields[0].Value)
}

func testAccPasswordWriteOnlyResourceConfig(expectedItem *op.Item, version string) string {
return fmt.Sprintf(`

data "onepassword_vault" "acceptance-tests" {
uuid = "%s"
}
resource "onepassword_item" "test_wo" {
vault = data.onepassword_vault.acceptance-tests.uuid
title = "%s"
category = "%s"
username = "%s"
password_wo = "%s"
password_wo_version = "%s"
}`, expectedItem.Vault.ID, expectedItem.Title, strings.ToLower(string(expectedItem.Category)), expectedItem.Fields[0].Value, expectedItem.Fields[1].Value, version)
}

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

data "onepassword_vault" "acceptance-tests" {
uuid = "%s"
}
resource "onepassword_item" "test_wo" {
vault = data.onepassword_vault.acceptance-tests.uuid
title = "%s"
category = "%s"
username = "%s"
password_wo = "%s"
}`, expectedItem.Vault.ID, expectedItem.Title, strings.ToLower(string(expectedItem.Category)), expectedItem.Fields[0].Value, expectedItem.Fields[1].Value)
}

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

data "onepassword_vault" "acceptance-tests" {
uuid = "%s"
}
resource "onepassword_item" "test_wo" {
vault = data.onepassword_vault.acceptance-tests.uuid
title = "%s"
category = "%s"
username = "%s"
password_wo_version = "1"
}`, expectedItem.Vault.ID, expectedItem.Title, strings.ToLower(string(expectedItem.Category)), expectedItem.Fields[0].Value)
}

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

data "onepassword_vault" "acceptance-tests" {
uuid = "%s"
}
resource "onepassword_item" "test_wo" {
vault = data.onepassword_vault.acceptance-tests.uuid
title = "%s"
category = "%s"
username = "%s"
password = "%s"
password_wo = "%s"
password_wo_version = "1"
}`, expectedItem.Vault.ID, expectedItem.Title, strings.ToLower(string(expectedItem.Category)), expectedItem.Fields[0].Value, expectedItem.Fields[1].Value, expectedItem.Fields[1].Value)
}

func testAccLoginResourceConfig(expectedItem *model.Item) string {
return fmt.Sprintf(`

Expand Down
Loading