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

Migration to Terraform provider framework #167

Merged
merged 36 commits into from
May 15, 2024

Conversation

jillianwilson
Copy link
Contributor

@jillianwilson jillianwilson commented May 15, 2024

Migration to Terraform provider framework

TL;DR

Due to a bug in the Hashicorp terraform-plugin-sdk it is possible for sensitive data pulled from 1Password items to be shown in plaintext when a user runs terraform plan. This only affects the sensitive data pulled from custom sections within 1Password items.

Affected sensitive data pulled from 1Password, that isn’t marked sensitive in the Terraform plan, is only shown to the user running terraform plan or any third-party terraform providers used to run the plan. The sensitive data is otherwise not unmasked to any other sources.

Previously the 1Password Terraform provider was built with the latest version of the terraform-plugin-sdk which was impacted by this bug. We have since upgraded our provider to use the terraform-plugin-framework which no longer has this bug.

Determining impact

In a Terraform plan, a block can be marked as sensitive so that fields cannot be shown in plaintext. Due to the bug in terraform-plugin-sdk, if nested blocks are marked as sensitive, the property is ignored and sensitive fields can be shown in plaintext.

Have the team responsible for the implementation of the Terraform provider determine if — in their Terraform configuration file — they have any provider that consumes data from an item’s section or if they output it.

This looks something like this:

resource "checkly_environment_variable" "variable_1" {
      value  = data.onepassword_item.my_login.section[0].field[0].value
}

Or like this:

output "poc" {
    value = jsonencode(data.onepassword_item.my_login.section[0])
}

Note that if your configuration is impacted, sensitive data is only viewable in plaintext in authenticated flows. This means that only the users and third-party integrations that are authorized to read those secrets from 1Password can print or read them as plaintext. Affected sensitive data is not otherwise unmasked to any other sources.

What this PR does

The PR migrates the 1Password Terraform provider from using the terraform-plugin-sdk to using terraform-plugin-framework. The new framework has a more robust handling of the schema and nested blocks, thereby no longer ignoring the sensitivity of a field’s value from a custom section. The outcome is that now, Terraform will take appropriate action to mask the field and mark it as sensitive. As an example, if a user tries to print an item’s section, Terraform will display an appropriate error.

This fix will be available in the upcoming Terraform provider release v2.0.0 shortly. Please update to that version if you are affected. This update does not contain any breaking changes. In the meantime, if applicable, remove all outputs of Terraform plans and configurations that may contain sensitive data.

Additional improvements as part of this PR

With the migration to the new terraform-plugin-framework, we've had the chance to improve the provider. Here's a list with the improvements:

How to test the PR

Setup

  1. Checkout this branch:

    git checkout feat/migrate-to-provider-framework
  2. Build the provider:

    make build
  3. Get your full path to the locally-built provider:

    echo $PwD/dist
  4. Configure your ~/.terraformrc file to use the local 1Password Terraform provider:

    provider_installation {
    
      dev_overrides {
          "1Password/onepassword" = "<path-from-step-3>"
    
          # Other overrides
      }
    
      # For all other providers, install them directly from their origin provider
      # registries as normal. If you omit this, Terraform will _only_ use
      # the dev_overrides block, and so no other providers will be available.
      direct {}
    }
    

Test that the bug is fixed

  1. In a Terraform file, add the following snippets:

    • Get an item from a vault:

      data "onepassword_vault" "test-vault" {
          name = "<your-vault-name>"
      }
      
      data "onepassword_item" "test-item" {
          vault = data.onepassword_vault.test-vault.uuid
          title = "<your-item-title>"
      }
    • Print an item's section to the output:

      output "poc" {
          value = jsonencode(data.onepassword_item.test-item.section[0])
      }
  2. Run a terraform plan:

    terraform plan
    • It shuold throw the following error:

      Planning failed. Terraform encountered an error while generating this plan.
      
      ╷
      │ Error: Output refers to sensitive values
      │
      │   on main.tf line 79:
      │   79: output "poc" {
      │
      │ To reduce the risk of accidentally exporting sensitive data that was
      │ intended to be only internal, Terraform requires that any root module
      │ output containing sensitive data be explicitly marked as sensitive,
      │ to confirm your intent.
      │
      │ If you do intend to export this data, annotate the output value as sensitive
      │ by adding the following argument:
      │     sensitive = true
      ╵
      

Test that the migration didn't cause any breaking changes

Vault data source

  1. Create a Terraform file, add the following snippet:

    terraform {
      required_providers {
        onepassword = {
          source  = "1Password/onepassword"
        }
      }
    }
    
    provider "onepassword" {}
    
    data "onepassword_vault" "test-vault" {
        name = "<your-vault-name>"
    }
    
    output "vault_res" {
        value = jsonencode(data.onepassword_vault.test-vault)
    }
  2. Make a plan for the configuration:

    terraform plan
    • The Changes to outputs section should look like this one:

      vault_res = jsonencode(
          {
            + description = "This is a description of my vault"
            + id          = "vaults/c2fcrvudd2ptjezfmmrkzxoawq"
            + name        = "test"
            + uuid        = "c2fcrvudd2ptjezfmmrkzxoawq"
          }
      )
      

Item data source

  1. Create a Terraform file, add the following snippet:

    terraform {
      required_providers {
        onepassword = {
          source  = "1Password/onepassword"
        }
      }
    }
    
    provider "onepassword" {}
    
    data "onepassword_vault" "test-vault" {
        name = "<your-vault-name>"
    }
    
    data "onepassword_item" "test-item" {
        vault = data.onepassword_vault.test-vault.uuid
        title = "<your-item-title>"
    }
  2. Make a plan for the configuration:

    terraform plan
    • It should succeed without any failures.

Item resource

  1. In a Terraform file, add a complex onepassword_item resource. You can use the snippets below as an inspiration.

    Login item example snippet
    resource "onepassword_item" "demo_login" {
        vault = data.onepassword_vault.test-vault.uuid
    
        title = "Test TF Resource - Login"
        category = "login"
    
        username = "MyUserName123"
        password_recipe {
          length = 30
          letters=false
        }
    
        url = "http://google.com"
    }
    Database item example snippet
    resource "onepassword_item" "demo_database" {
        vault = data.onepassword_vault.test-vault.uuid
    
        title = "Demo TF Resource - Database"
        category = "database"
    
        type = "MySQL"
        hostname = "http://my.fancydomain.com"
        port = "3000"
        database = "badass"
        username = "theboss"
    }
    Secure Note example snippet (new feature)
    resource "onepassword_item" "test-secure-note" {
        vault = data.onepassword_vault.test-vault.uuid
    
        title = "Test TF Resource - Secure Note"
        category = "secure_note"
    
        note_value = <<EOT
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec et volutpat lorem. Integer quis felis porttitor, lobortis purus id, sodales nisi. Suspendisse dictum nibh lorem, vel dapibus sapien sodales pulvinar. Donec euismod, dolor vitae fermentum faucibus, elit orci ullamcorper nisi, eget ultrices turpis elit non odio. Integer dictum risus ante, finibus venenatis sem varius bibendum. Sed pellentesque ultrices diam vel viverra. Duis odio mi, malesuada quis luctus a, lacinia quis orci. Vestibulum hendrerit lorem a odio tristique, quis blandit purus sollicitudin.
    
        Nullam ut hendrerit lacus, at consectetur risus. Vestibulum at diam a arcu fermentum eleifend vel at mi. Cras auctor, massa in blandit rutrum, risus nibh euismod massa, quis porttitor libero mauris id nibh. Sed lobortis ipsum in ipsum dapibus, commodo eleifend velit dignissim. Donec tincidunt turpis mi, vitae venenatis urna laoreet vel. Integer ut scelerisque massa, at varius est. Proin porta quam a dolor euismod, quis iaculis odio dictum. Cras velit neque, vestibulum et consequat at, ultrices quis enim. Nulla sed augue ac odio scelerisque facilisis. Nunc ultricies tristique eros et ullamcorper. Curabitur et est eu nibh viverra feugiat. Pellentesque viverra vitae lacus sed consectetur. Aliquam nec tempor ipsum, quis dapibus quam. Nulla suscipit enim vel porta egestas. Nulla interdum congue erat at fermentum. Quisque viverra tincidunt metus non malesuada.
        EOT
    }
    Complex item example snippet
    resource "onepassword_item" "demo_complex" {
        vault = data.onepassword_vault.test-vault.uuid
    
        title = "Test TF Resource - Complex"
        category = "login"
    
        username = "monseur.user@crazy-IT.com"
        password_recipe {
          length = 64
          digits = false
        }
    
        url = "https://github.com"
    
        tags = ["Terraform", "1password-connect", "test"]
    
        section {
            label = "Some addition info 🛣️"
    
            field {
                label = "Destination"
                value = "⛰️ Mount Everest"
            }
    
            field {
                label = "📧 Information"
                type = "EMAIL"
                value = "awesome-location@cabin.nl"
            }
    
            # This is a new feature added with the migrated framework
            field {
                label = "Mobile"
                type = "PHONE"
                value = "+154242342343128"
            }
    
            field {
                id = "trip-creation-time"
                label = "Planned since"
                type = "MONTH_YEAR"
                value = "202201"
            }
    
            field {
                label = "Check-in"
                type = "DATE"
                value = "2024-05-03"
            }
    
            field {
                label = "Check-out"
                type = "DATE"
                value = "2024-05-10"
            }
        }
    
        section {
            label = "Other details"
    
            field {
                label = "Rub rub said..."
                type = "CONCEALED"
    
                password_recipe {
                  length = 21
                  letters = false
                  symbols = false
                }
            }
    
            field {
                id = "TOTP_key-to-success"
                label = "Fine, you may leave..."
                type = "OTP"
                value = "otpauth://totp/IT-Tools:demo-user?issuer=IT-Tools&secret=TWODB7HMGDABG3LY&algorithm=SHA1&digits=6&period=30"
            }
    
            field {
                label = "Protocol"
                type = "URL"
                value = "https://www.robtopgames.com/"
            }
        }
    
        section {
            label = "🍜 Some last bits"
    
            field {
                label = "Airline"
                value = "✈️ Qatar"
            }
    
            field {
                label = "Reach us at"
                type = "PHONE"
                value = "+322134697057"
            }
    
            field {
                id = "custom-travel-code"
                label = "coupon_code"
                type = "CONCEALED"
    
                password_recipe {
                  length = 12
                  symbols = false
                }
            }
        }
    }
  2. Apply the changes to your configuration to create the 1Password item:

    terraform apply
  3. Check the created item in 1Password

    • The item should be succesfully created with the appropriate data
  4. Make a change to the onepassword_item resource. A couple of examples:

    • Change the recipe for the password
    • Add a new section with fields
    • Change the password recipe of a custom field
    • Add a note_value to the item (new feature)
  5. Apply the changes again and then check the item in 1Password again.

    • The item should reflect the latest changes.
  6. Remove the code for the onepassword_item resource and apply the changes again.

    • The item in 1Password shuold be deleted.

edif2008 and others added 30 commits May 2, 2024 18:56
Run the following commands:
- `go get -u ./...`
- `go mod tidy`
- `go mod vendor`

With this, we also update Go version to 1.21.
- Move `github.com/hashicorp/terraform-plugin-framework` package from indirect dependency to direct dependency
- `go get -u ./...`
- `go mod tidy`
- `go mod vendor`
Currently, the provider doesn't implement any functions. Therefore, these files are removed.
This client will be used by the provider to create, read, update and delete items using either Connect or 1Password CLI.

This client is also capable of fetching vault data.
Changes made compared to `onepassword/connectctx/wrapper.go`
- NewClient function is now responsible of creating the Connect client with user agent.
- Renaming the struct for clarity and cleanliness.
Changes made compared to the code in `internal/onepassword/cli` directory:
- New function is renamed to NewClient and implements the initializeCLI function
- Enable password generation when updating an item (to be consistent with the Connect client).
This function will return either the Connect or the CLI client.
- Name the struct as OnePasswordProvider
- Implement provider schema
- Implement provider data model

What changed from the previous implementation is that we marked the service account token and connect token as sensitive.
- Load default values from environment variables
- Initialize the 1Password client (either Connect or CLI client based on the configuration)
This is used to add validators to schemas (e.g. dependencies between attributes). Some of these will be used by the vault data source.
With the vault data source fully implemented, we add it to the list of data sources the provider supports.
Changes made here as part of the migration:
- Add a new constant `terraformItemIDDescription` which will be used for both item resource and data source for consistency.
- The slices of categories, field purposes, and field types are now based on the constants available in the Connect Go SDK to eliminate hardcoding the string values (these apply to both Connect and CLI clients).
These new ones are used by the item resource schema.
With the new framework, Terraform wants to be more predictable, therefore any computed values will be recomputed whenever something in the resource changes.

What this modifier does is that it will use the existing value in the state unless one of the following two scenarios happen:
- The value is set to a specific new one in the plan.
- The password recipe (which was used to generate it) is changed.
This converts an item into a data source model. It’s a rewrite of the same function that exists in the old provider code.

A couple of improvements have been added as part of the migration:
- No longer throw cause Terraform to trigger a change or an inconsistency error if the tags are the same, but in different order. This is because 1Password sorts the tags alphabetically, which can have a different order than the one tags passed in Terraform have.
- With the new framework, Terraform will throw an inconsistency error if the planned value is null (i.e. not set) and the result is empty string). That's why we use the custom `setStringValue` function to set a value on each attribute based on the actual value in 1Password.
This converts the Terraform data into a 1Password item. It's a rewrite of the same function that exists in the old provider code.

We also implemented the functions associated to it:
- parseGeneratorRecipe - currently missing the case in which the attribute is not defined (or Nil)
- addRecipe - identical to the one in the old provider code
This function extracts the vault and item UUIDs from the terraform ID.

This matches the function with the same name from the old provider code.
Implement configure, create, read, update and delete functions for item resource
These will help with the following:
- easily generate the items that we will use for testing.
- have a mock server that will act as a Connect server during the tests.

Co-authored-by: jillianwilson <jillian.morgan@agilebits.com>
Co-authored-by: jillianwilson <jillian.morgan@agilebits.com>
Co-authored-by: jillianwilson <jillian.morgan@agilebits.com>
Co-authored-by: jillianwilson <jillian.morgan@agilebits.com>
Co-authored-by: jillianwilson <jillian.morgan@agilebits.com>
This came as part of the scaffolding. We remove it since the 1Password Terraform provider is under the MIT license.
- Add make command for running acceptance tests.
- Update conditional compilation for tools package. Since Go 1.18 the new conditional compilation is `//go:build` instead of `// + build`.
- Add terraform-registry-manifest.json file. This file is part of the new framework structure. It's used by the terraform registry to get additional data about the provider being published. For details check this link: https://developer.hashicorp.com/terraform/registry/providers/publishing#terraform-registry-manifest-file
- Update goreleaser to use the registry manifest file
Update the docs based on the migrated provider and add small improvements to existing examples.
We also do the following adjustments:
- Update Go version in the pipeline to 1.21.
- Run the test step with TF_ACC=1 to enable acceptance tests (which are the ones we implemented in the migration).
- Switch the `paultyng/ghaction-import-gpg@v2.1.0` action to `crazy-max/ghaction-import-gpg@v6` since the previous one is deprecated and it was recommended to use the upstream action that we now use.
Field of type OTP require a special field ID. Adding this validator achieves the following:
- Ensures that the user will have a functional OTP field.
- Terraform will not throw inconsistency errors when the CLI tries to correct the custom ID for OTP field.
@jillianwilson jillianwilson changed the title Feat/migrate to plugin framework Migration to Terraform provider framework May 15, 2024
@volodymyrZotov
Copy link
Collaborator

✅ Tested it and could confirm that this fixes the issue with printing sensitive data in the sections.

✅ In addition, I tested other scenarios (create/read/update/delete) items, and it works well after migration.

⚠️ However, there is an issue with secure_note item that I faced.
To reproduce it, I used the Secure Note snippet from the testing steps and when running terraform apply I saw the following error in the console:

╷
│ Error: Provider returned invalid result object after apply
│ 
│ After the apply operation, the provider still indicated an unknown value for onepassword_item.test-secure-note.password. All values must be known after apply, so this is always a bug in the provider and should be reported in the
│ provider's own repository. Terraform will still save the other known object values in the state.
╵

Nevertheless, there is an error in the console, I see that the Secure Note item is created in 1Password.

I approve this PR as it contains an important fix to not print sensitive data in the sections, but let's address the Security Note issue in the following PR.

@jillianwilson jillianwilson merged commit f3d6fdb into main May 15, 2024
6 checks passed
@jillianwilson jillianwilson deleted the feat/migrate-to-plugin-framework branch May 15, 2024 21:40
@github-actions github-actions bot mentioned this pull request May 15, 2024
2 tasks
@volodymyrZotov
Copy link
Collaborator

Filed an issue for the secure note #173

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment