diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 879fe5c2..393bc90d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,69 +44,69 @@ jobs: git diff --compact-summary --exit-code || \ (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) # Run acceptance tests in a matrix with Terraform CLI versions - test: - name: Terraform Provider Acceptance Tests - needs: build - runs-on: ubuntu-latest - timeout-minutes: 15 - strategy: - fail-fast: false - max-parallel: 1 - matrix: - # list whatever Terraform versions here you would like to support - terraform: - - '1.0.*' - - '1.1.*' - - '1.2.*' - - '1.3.*' - - '1.4.*' - - '1.5.*' - - '1.6.*' - - '1.7.*' - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 - with: - go-version-file: 'go.mod' - cache: true - - uses: hashicorp/setup-terraform@v2 - with: - terraform_version: ${{ matrix.terraform }} - terraform_wrapper: false - - run: go mod download - - env: - TF_ACC: "1" - KEYFACTOR_HOSTNAME: ${{secrets.KEYFACTOR_HOSTNAME}} - KEYFACTOR_USERNAME: ${{secrets.KEYFACTOR_USERNAME}} - KEYFACTOR_PASSWORD: ${{secrets.KEYFACTOR_PASSWORD}} - KEYFACTOR_DOMAIN: ${{secrets.KEYFACTOR_DOMAIN}} - - KEYFACTOR_CERTIFICATE_ID: ${{secrets.KEYFACTOR_CERTIFICATE_ID}} - KEYFACTOR_CERTIFICATE_PASSWORD: ${{secrets.KEYFACTOR_CERTIFICATE_PASSWORD}} - KEYFACTOR_CERTIFICATE_TEMPLATE_NAME: ${{secrets.KEYFACTOR_CERTIFICATE_TEMPLATE_NAME}} - KEYFACTOR_CERTIFICATE_CA_DOMAIN: ${{secrets.KEYFACTOR_CERTIFICATE_CA_DOMAIN}} - KEYFACTOR_CERTIFICATE_CA_NAME: ${{secrets.KEYFACTOR_CERTIFICATE_CA_NAME}} - - KEYFACTOR_CERTIFICATE_STORE_ID: ${{secrets.KEYFACTOR_CERTIFICATE_STORE_ID}} - KEYFACTOR_CERTIFICATE_STORE_CLIENT_MACHINE: ${{secrets.KEYFACTOR_CERTIFICATE_STORE_CLIENT_MACHINE}} - KEYFACTOR_CERTIFICATE_STORE_ORCHESTRATOR_AGENT_ID: ${{secrets.KEYFACTOR_CERTIFICATE_STORE_ORCHESTRATOR_AGENT_ID}} - KEYFACTOR_CERTIFICATE_STORE_CONTAINER_ID1: ${{secrets.KEYFACTOR_CERTIFICATE_STORE_CONTAINER_ID1}} - KEYFACTOR_CERTIFICATE_STORE_CONTAINER_ID2: ${{secrets.KEYFACTOR_CERTIFICATE_STORE_CONTAINER_ID2}} - KEYFACTOR_CERTIFICATE_STORE_PASS: ${{secrets.TEST_CERTSTORE_PASS}} - - KEYFACTOR_DEPLOY_CERT_STOREID1: ${{secrets.KEYFACTOR_DEPLOY_CERT_STOREID1}} - KEYFACTOR_DEPLOY_CERT_STOREID2: ${{secrets.KEYFACTOR_DEPLOY_CERT_STOREID2}} - - KEYFACTOR_SECURITY_ROLE_NAME: ${{secrets.KEYFACTOR_SECURITY_ROLE_NAME}} - - KEYFACTOR_SECURITY_IDENTITY_ACCOUNTNAME: ${{secrets.KEYFACTOR_SECURITY_IDENTITY_ACCOUNTNAME}} - KEYFACTOR_SECURITY_IDENTITY_ROLE1: ${{secrets.KEYFACTOR_SECURITY_IDENTITY_ROLE1}} - KEYFACTOR_SECURITY_IDENTITY_ROLE2: ${{secrets.KEYFACTOR_SECURITY_IDENTITY_ROLE2}} - - KEYFACTOR_TEMPLATE_ROLE_BINDING_ROLE_NAME: ${{secrets.KEYFACTOR_TEMPLATE_ROLE_BINDING_ROLE_NAME}} - KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME1: ${{secrets.KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME1}} - KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME2: ${{secrets.KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME2}} - KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME3: ${{secrets.KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME3}} - - run: go test -v -cover ./keyfactor - timeout-minutes: 10 \ No newline at end of file +# test: +# name: Terraform Provider Acceptance Tests +# needs: build +# runs-on: ubuntu-latest +# timeout-minutes: 15 +# strategy: +# fail-fast: false +# max-parallel: 1 +# matrix: +# # list whatever Terraform versions here you would like to support +# terraform: +# - '1.0.*' +# - '1.1.*' +# - '1.2.*' +# - '1.3.*' +# - '1.4.*' +# - '1.5.*' +# - '1.6.*' +# - '1.7.*' +# steps: +# - uses: actions/checkout@v3 +# - uses: actions/setup-go@v3 +# with: +# go-version-file: 'go.mod' +# cache: true +# - uses: hashicorp/setup-terraform@v2 +# with: +# terraform_version: ${{ matrix.terraform }} +# terraform_wrapper: false +# - run: go mod download +# - env: +# TF_ACC: "1" +# KEYFACTOR_HOSTNAME: ${{secrets.KEYFACTOR_HOSTNAME}} +# KEYFACTOR_USERNAME: ${{secrets.KEYFACTOR_USERNAME}} +# KEYFACTOR_PASSWORD: ${{secrets.KEYFACTOR_PASSWORD}} +# KEYFACTOR_DOMAIN: ${{secrets.KEYFACTOR_DOMAIN}} +# +# KEYFACTOR_CERTIFICATE_ID: ${{secrets.KEYFACTOR_CERTIFICATE_ID}} +# KEYFACTOR_CERTIFICATE_PASSWORD: ${{secrets.KEYFACTOR_CERTIFICATE_PASSWORD}} +# KEYFACTOR_CERTIFICATE_TEMPLATE_NAME: ${{secrets.KEYFACTOR_CERTIFICATE_TEMPLATE_NAME}} +# KEYFACTOR_CERTIFICATE_CA_DOMAIN: ${{secrets.KEYFACTOR_CERTIFICATE_CA_DOMAIN}} +# KEYFACTOR_CERTIFICATE_CA_NAME: ${{secrets.KEYFACTOR_CERTIFICATE_CA_NAME}} +# +# KEYFACTOR_CERTIFICATE_STORE_ID: ${{secrets.KEYFACTOR_CERTIFICATE_STORE_ID}} +# KEYFACTOR_CERTIFICATE_STORE_CLIENT_MACHINE: ${{secrets.KEYFACTOR_CERTIFICATE_STORE_CLIENT_MACHINE}} +# KEYFACTOR_CERTIFICATE_STORE_ORCHESTRATOR_AGENT_ID: ${{secrets.KEYFACTOR_CERTIFICATE_STORE_ORCHESTRATOR_AGENT_ID}} +# KEYFACTOR_CERTIFICATE_STORE_CONTAINER_ID1: ${{secrets.KEYFACTOR_CERTIFICATE_STORE_CONTAINER_ID1}} +# KEYFACTOR_CERTIFICATE_STORE_CONTAINER_ID2: ${{secrets.KEYFACTOR_CERTIFICATE_STORE_CONTAINER_ID2}} +# KEYFACTOR_CERTIFICATE_STORE_PASS: ${{secrets.TEST_CERTSTORE_PASS}} +# +# KEYFACTOR_DEPLOY_CERT_STOREID1: ${{secrets.KEYFACTOR_DEPLOY_CERT_STOREID1}} +# KEYFACTOR_DEPLOY_CERT_STOREID2: ${{secrets.KEYFACTOR_DEPLOY_CERT_STOREID2}} +# +# KEYFACTOR_SECURITY_ROLE_NAME: ${{secrets.KEYFACTOR_SECURITY_ROLE_NAME}} +# +# KEYFACTOR_SECURITY_IDENTITY_ACCOUNTNAME: ${{secrets.KEYFACTOR_SECURITY_IDENTITY_ACCOUNTNAME}} +# KEYFACTOR_SECURITY_IDENTITY_ROLE1: ${{secrets.KEYFACTOR_SECURITY_IDENTITY_ROLE1}} +# KEYFACTOR_SECURITY_IDENTITY_ROLE2: ${{secrets.KEYFACTOR_SECURITY_IDENTITY_ROLE2}} +# +# KEYFACTOR_TEMPLATE_ROLE_BINDING_ROLE_NAME: ${{secrets.KEYFACTOR_TEMPLATE_ROLE_BINDING_ROLE_NAME}} +# KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME1: ${{secrets.KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME1}} +# KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME2: ${{secrets.KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME2}} +# KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME3: ${{secrets.KEYFACTOR_TEMPLATE_ROLE_BINDING_TEMPLATE_NAME3}} +# +# run: go test -v -cover ./keyfactor +# timeout-minutes: 10 \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index ee36b641..ebd2e7d4 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,14 +1,11 @@ # Visit https://goreleaser.com for documentation on how to customize this # behavior. +version: 2 before: hooks: - # this is just an example and not a requirement for provider building/publishing - go mod tidy builds: - env: - # goreleaser does not work with CGO, it could also complicate - # usage by users in CI/CD systems like Terraform Cloud where - # they are unable to install libraries. - CGO_ENABLED=0 mod_timestamp: '{{ .CommitTimestamp }}' flags: @@ -39,13 +36,11 @@ checksum: name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' algorithm: sha256 signs: - - artifacts: checksum + - artifacts: all # Sign all artifacts (binaries, archives, etc.) args: - # if you are using this in a GitHub action or some other automated pipeline, you - # need to pass the batch flag to indicate its not interactive. - "--batch" - "--local-user" - - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key + - "{{ .Env.GPG_FINGERPRINT }}" - "--output" - "${signature}" - "--detach-sign" @@ -57,15 +52,10 @@ release: github: owner: keyfactor-pub name: terraform-provider-keyfactor - extra_files: - glob: 'terraform-registry-manifest.json' name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' - # If you want to manually examine the release before its live, uncomment this line: - # draft: true changelog: - skip: false - sort: desc filters: exclude: - - docs \ No newline at end of file + - docs diff --git a/CHANGELOG.md b/CHANGELOG.md index 970e9d7e..fbb76687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# v2.2.0 +### Certificates + +#### Features +* feat(certificates): `keyfactor_certificate` resource has a new parameter `collection_enrollment_wait` that allows the user to define an arbitrary wait time for the collection enrollment to complete. The default is 0 seconds. +* feat(certificates): `keyfactor_certificate` resources now support `collection_enrollment_wait_max_retries` + +#### Fixes + +* fix(certificates): `keyfactor_certificate` resource `metadata` updates are now correctly scoped by `collection_id` if provided. + # v2.1.11 ### Certificates diff --git a/GNUmakefile b/GNUmakefile index d3726bb0..a9822528 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -5,7 +5,7 @@ GOFMT_FILES := $$(find $(PROVIDER_DIR) -name '*.go' |grep -v vendor) NAMESPACE=keyfactor-pub WEBSITE_REPO=https://github.com/Keyfactor/terraform-provider-keyfactor NAME=keyfactor -VERSION=2.1.10 +VERSION=2.2.0-rc.8 BINARY=terraform-provider-${NAME} BINARYV2=terraform-provider-${NAME}_${VERSION} OS_ARCH := $(shell go env GOOS)_$(shell go env GOARCH) diff --git a/docs/resources/certificate.md b/docs/resources/certificate.md index 4fe20a0f..be7bdb9d 100644 --- a/docs/resources/certificate.md +++ b/docs/resources/certificate.md @@ -88,6 +88,8 @@ resource "keyfactor_certificate" "kf_csr_cert" { ### Optional +- `collection_enrollment_wait` (String) The maximum time to wait for a certificate to be added to a collection, post enrollment. This is useful for certificates that trigger issue handlers and/or workflows post enrollment and will delay the certificate being added to the expected collection. Format: 1h, 1m, 1s. Default: 0. +- `collection_enrollment_wait_max_retries` (Number) The maximum number of times to retry checking if a certificate has been added to a collection in 30 second intervals. This takes precedence over `collection_enrollment_wait`. Default: 0. - `collection_id` (Number) Optional certificate collection ID. This is required if enrollment permissions have been granted at the collection level. NOTE: This will *not* assign the cert to the specified collection ID; assignment is based the collection's associated query. For more information on collection permissions see the Keyfactor Command docs: https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/CertificatePermissions.htm?Highlight=collection%20permissions - `common_name` (String) Subject common name (CN) of the certificate. - `country` (String) Subject country of the certificate diff --git a/examples/architecture/keyfactor_certificate_resource__pfx.md b/examples/architecture/keyfactor_certificate_resource__pfx.md new file mode 100644 index 00000000..fe0de27a --- /dev/null +++ b/examples/architecture/keyfactor_certificate_resource__pfx.md @@ -0,0 +1,184 @@ +# keyfactor_certificate resource API Calls: + +Below summarizes the API calls made by the `keyfactor_certificate` resource for each of the `Terraform` operations. + +Supported Operations: + +- [READ](#PFX-Resource-READ) +- [CREATE](#PFX-Resource-CREATE) +- [UPDATE](#PFX-Resource-UPDATE) +- [DELETE](#PFX-Resource-DELETE) + +## Table of Contents + +- [keyfactor\_certificate resource API Calls](#keyfactor_certificate-resource-api-calls) + - [Supported Operations](#supported-operations) + - [PFX Resource READ](#pfx-resource-read) + - [Required Permissions](#required-permissions) + - [PFX Resource CREATE](#pfx-resource-create) + - [Required Permissions](#required-permissions-1) + - [PFX Resource UPDATE](#pfx-resource-update) + - [Required Permissions](#required-permissions-2) + - [PFX Resource DELETE](#pfx-resource-delete) + - [Required Permissions](#required-permissions-3) + - [Permissions](#permissions) + +## PFX Resource READ: + +1. Call `GET: certificate/{Id}` to get the certificate ID, can be scoped using `collection_id` from plan. This requires + `certificate:read` permissions, and can be scoped using `collection_id` from plan. This provides certificate + `metadata` and `has_private_key` information. +2. If `has_private_key` is `true` call `POST /Certificates/Recover` to recover the private key, this requires + `certificate:recover` permissions, and can be scoped using `collection_id` from plan. This provides `certificate` in + PEM format and `private_key` in unencrypted `pkcs1 or pkcs8` format. +3. Else `has_private_key` is `false` call `POST /Certificates/Download` to download the certificate, this requires + `certificate:download` permissions, and can be scoped using `collection_id` from plan. This provides `certificate` in + PEM format. + +```mermaid +flowchart TD + A1[Using `certificate_id` from Terraform state call `GET: Certificates/ID`] + A1 --> A2{has_private_key == true} + A2 -- Yes --> A3[Call `POST: /Certificates/Recover`] + A2 -- No --> A4[Call `POST: /Certificates/Download`] + A3 --> A99[END] + A4 --> A99[END] +``` + +### Required Permissions: + +| Name | Description | Global Permission Required? | Collection Scopable? | +|------------------------|--------------------------------------------------------------------------------------------------|-----------------------------|----------------------| +| `certificate:read` | Validate certificate exists, read certificate metadata and determine if private key is available | No | Yes | +| `certificate:recover` | Recover private key if available | No | Yes | +| `certificate:download` | Download certificate if no private key | No | Yes | + +## PFX Resource CREATE: + +0. If certificate is in state then call [Resource UPDATE](#PFX-Resource-UPDATE) else continue +1. Enroll PFX call `POST: /Enrollment/PFX `, can be scoped using `collection_id` from plan. This requires + `global:certificate:enroll:pfx` permissions. +2. If enrollment `disposition` is `Pending`, sleep and repeat until unable to find cert in `pending` queues, or is found + in the `denied` queue calling: + - `GET: /Workflow/Certificates/Pending` requires `global:workflow:read:started_by_me` permissions + - `GET /Workflow/Certificates/ExternalValidation` requires `global:workflow:read:started_by_me` permissions + - `GET /Workflow/Certificates/Denied` requires `global:workflow:read:started_by_me` permissions + - If `CertRequestId` found in `denied` queue `fail`. + - Else consider request `approved`. + - Call `GET /Certificates` filter by `CertRequestId` and if unable to find, `fail`. This can be scoped using + `collection_id` from plan. +3. Else `KeyfactorId` it not empty and use as `CertificateId` for next step. +4. Call [Resource READ](#PFX-Resource-READ) using `CertificateId` to validate certificate has been created and able to + recover private key. + +```mermaid +flowchart TD + subgraph PFX Resource CREATE + B0{certificate is in state?} + B0 -- Yes --> B1[Call PFX Resource UPDATE] + B1 --> B99[End] + B0 -- No --> B2[Enroll PFX call `POST: /Enrollment/PFX`] + B2 --> B3{enrollment disposition == Pending} + B3 -- Yes --> B4[Call `GET: /Workflow/Certificates/Pending`] + B4 --> B5[Call `GET /Workflow/Certificates/ExternalValidation`] + B5 --> B98{Certificate found?} + B98 -- Yes --> B10[Sleep and repeat until timeout] + B98 -- No --> B6[Call `GET /Workflow/Certificates/Denied`] + B6 --> B7{`CertRequestId` found in `denied` queue?} + B7 -- NO, assume certificate has been created --> B8[Use `KeyfactorId` from response and call `GET: /Certificates/ID`] + B8 --> B12[Call PFX Resource READ] + B7 -- Yes --> B13[Fail] + B12{Certificate found?} -- No --> B13[Fail] + B13 --> B99[End] + B12 -- Yes --> B14[Call PFX Resource READ] + B14 --> B99[End] + end +``` + +### Required Permissions: + +| Name | Description | Global Permission Required? | Collection Scopable? | +|------------------------------------------|--------------------------------------------------------------------------------------------------|-----------------------------|----------------------| +| `certificate_enrollment:pfx` | Enroll PFX certificate | Yes | No | +| `certificates:read` | Validate certificate exists, read certificate metadata and determine if private key is available | No | Yes | +| `certificates:download_with_private_key` | Download certificate | No | Yes | +| `certificates:edit_metadata` | Update certificate metadata | No | Yes | +| `workflow_instances:read:started_by_me` | (Optional) Used if certificate enrollments require an approval workflow to run. | Yes | No | + +## PFX Resource UPDATE: + +0. Call [Resource READ](#PFX-Resource-READ) to validate certificate exists and compare `metadata` with the plan + `metadata`. +1. If only `metadata` has changed call `PUT: /Certificates/Metadata` to update the certificate `metadata`, can be scoped + using `collection_id` from plan. This requires `certificate:update` permissions. +2. if anything else has changed call [Resource DELETE](#PFX-Resource-DELETE) then [ + Resource CREATE](#PFX-Resource-CREATE). +3. Else no changes have been made. + +```mermaid +flowchart TD + subgraph PFX Resource UPDATE + C0[Call PFX Resource READ] + C0 --> C1{Certificate exists?} + C1 -- Yes --> C2{Non-metadata changes?} + C1 -- No --> C3[Call PFX Resource CREATE] + C3 --> C99[End] + C2 -- Yes --> C4[Call PFX Resource DELETE] + C4 --> C5[Call PFX Resource CREATE] + C5 --> C99[End] + C2 -- No --> C6{metadata has changed?} + C6 -- Yes --> C7[Call `PUT: /Certificates/Metadata`] + C7 --> C8[Call PFX Resource READ] + C6 -- No --> C9[No changes have been made] + C9 --> C99[End] + end +``` + +### Required Permissions: + +| Name | Description | Global Permission Required? | Collection Scopable? | +|------------------------------------------|--------------------------------------------------------------------------------------------------|-----------------------------|----------------------| +| `certificates:read` | Validate certificate exists, read certificate metadata and determine if private key is available | No | Yes | +| `certificates:edit_metadata` | Update certificate metadata | No | Yes | +| `certificates:download_with_private_key` | Download certificate | No | Yes | + +## PFX Resource DELETE: + +0. Call [`Resource READ`](#PFX-Resource-READ) to validate certificate exists. +1. Call `POST: /Certificates/Revoke`, can be scoped using `collection_id` from plan. This + requires `certificate:revoke` permissions. + +```mermaid +flowchart TD + subgraph PFX Resource DELETE + D0[Call PFX Resource READ] + D0 --> D1{Certificate exists?} + D1 -- Yes --> D2[Call POST: /Certificates/Revoke] + D2 --> D99[Remove from state and END] + D1 -- No --> D99[Remove from state and END] + end +``` + +### Required Permissions: + +| Name | Description | Global Permission Required? | Collection Scopable? | +|-----------------------|--------------------------------------------------------------------------------------------------|-----------------------------|----------------------| +| `certificates:read` | Validate certificate exists, read certificate metadata and determine if private key is available | No | Yes | +| `certificates:revoke` | Revoke certificate | No | Yes | +| `certificates:delete` | Delete certificate | No | Yes | + +## Permissions + +Below is a summary of the permissions required for a complete lifecycle of a `keyfactor_certificate` resource: + +| Name | Description | Global Permission Required? | Collection Scopable? | +|------------------------------|--------------------------------------------------------------------------------------------------|-----------------------------|----------------------| +| `certificate_enrollment:pfx` | Allowed to perform PFX enrollments. | Yes | No | +| `certificates:read` | Validate certificate exists, read certificate metadata and determine if private key is available | No | Yes | +| `certificates:recover` | Recover private key if available. | No | Yes | +| `certificates:download` | Download certificate if no private key. | No | Yes | +| `certificates:revoke` | Revoke certificate | No | Yes | +| `certificates:delete` | Delete certificate | No | Yes | +| `certificates:edit_metadata` | Update certificate metadata. | No | Yes | +| `workflow_instances:read` | (Optional) Used if certificate enrollments require an approval workflow to run. | Yes | No | + diff --git a/go.mod b/go.mod index 9992ad29..e0aca44d 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,11 @@ module github.com/keyfactor-pub/terraform-provider-keyfactor -go 1.21 +go 1.22.0 + +toolchain go1.23.0 require ( - github.com/Keyfactor/keyfactor-go-client/v2 v2.2.9 + github.com/Keyfactor/keyfactor-go-client/v2 v2.2.11 github.com/hashicorp/terraform-plugin-framework v0.10.0 github.com/hashicorp/terraform-plugin-go v0.18.0 github.com/hashicorp/terraform-plugin-log v0.9.0 @@ -15,7 +17,7 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect - github.com/cloudflare/circl v1.3.3 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect @@ -51,13 +53,13 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.13.2 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/mod v0.10.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.56.3 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect ) diff --git a/go.sum b/go.sum index 93739f9d..5110d8fa 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/Keyfactor/keyfactor-go-client v1.4.3 h1:CmGvWcuIbDRFM0PfYOQH6UdtAgplv github.com/Keyfactor/keyfactor-go-client v1.4.3/go.mod h1:3ZymLNCaSazglcuYeNfm9nrzn22wcwLjIWURrnUygBo= github.com/Keyfactor/keyfactor-go-client-sdk v1.0.1 h1:cs8hhvsY3MJ2o1K11HLTRCjRT8SbsKhhi73Y4By2CI0= github.com/Keyfactor/keyfactor-go-client-sdk v1.0.1/go.mod h1:Z5pSk8YFGXHbKeQ1wTzVN8A4P/fZmtAwqu3NgBHbDOs= -github.com/Keyfactor/keyfactor-go-client/v2 v2.2.9 h1:E35dVylP5HfULDYRcY7vjGZ7wPDYd7ThG7H3ll9MvXs= -github.com/Keyfactor/keyfactor-go-client/v2 v2.2.9/go.mod h1:fiv/ai955uffPu+ZVye5OfOR+fHoVS/sbfVwTWokNrc= +github.com/Keyfactor/keyfactor-go-client/v2 v2.2.11 h1:Tpk/AKQZmfCZFpODTpLO2+T0XUOgOrvp1ZhQq6J+RSo= +github.com/Keyfactor/keyfactor-go-client/v2 v2.2.11/go.mod h1:fiv/ai955uffPu+ZVye5OfOR+fHoVS/sbfVwTWokNrc= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= @@ -17,8 +17,8 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -38,6 +38,7 @@ github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -152,22 +153,36 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -177,29 +192,43 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/keyfactor/constants.go b/keyfactor/constants.go index ff9dbc8b..06f82e9a 100644 --- a/keyfactor/constants.go +++ b/keyfactor/constants.go @@ -18,11 +18,14 @@ const ( ERR_SUMMARY_TEMPLATE_READ = "Unable to read Keyfactor Command template." ERR_SUMMARY_IDENTITY_DELETE = "Unable to delete security identity." - //EnvCommandHostname = "KEYFACTOR_HOSTNAME" + ERR_COLLECTION_WAIT = "does not have the required permissions: Certificates - Read" + + EnvCommandHostname = "KEYFACTOR_HOSTNAME" EnvCommandUsername = "KEYFACTOR_USERNAME" - //EnvCommandPassword = "KEYFACTOR_PASSWORD" - //EnvCommandDomain = "KEYFACTOR_DOMAIN" - //EnvCommandAPI = "KEYFACTOR_API_PATH" - //EnvCommandTimeout = "KEYFACTOR_TIMEOUT" - //DefaultAPIPath = "KeyfactorAPI" + EnvCommandPassword = "KEYFACTOR_PASSWORD" + EnvCommandAppKey = "KEYFACTOR_APPKEY" + EnvCommandDomain = "KEYFACTOR_DOMAIN" + EnvCommandAPI = "KEYFACTOR_API_PATH" + EnvCommandTimeout = "KEYFACTOR_TIMEOUT" + DefaultAPIPath = "KeyfactorAPI" ) diff --git a/keyfactor/helpers.go b/keyfactor/helpers.go index 298d630b..333527a0 100644 --- a/keyfactor/helpers.go +++ b/keyfactor/helpers.go @@ -1,6 +1,7 @@ package keyfactor import ( + "context" "crypto/ecdsa" rsa2 "crypto/rsa" "crypto/x509" @@ -13,11 +14,13 @@ import ( "sort" "strconv" "strings" + "time" "github.com/Keyfactor/keyfactor-go-client/v2/api" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" ) var ( @@ -372,31 +375,153 @@ func isNullId(i int) bool { return false } -func downloadCertificate(id int, collectionId int, kfClient *api.Client, password string, csrEnrollment bool) ( +// retryCertCollectionHandler is a helper function to retry downloading a certificate from a collection +// This is needed because the Keyfactor Command API may return an error if the certificate is not ready to be downloaded +// The function will retry a set number of times before returning an error. +func retryCertCollectionHandler( + kfClient *api.Client, + certID int, + collectionID, maxSleep int, maxRetry int, +) (*api.GetCertificateResponse, error) { + req := api.GetCertificateContextArgs{ + Id: certID, + CollectionId: intToPointer(collectionID), + } + + totalSleep := 0 + for i := 0; i <= maxSleep; i++ { + tflog.Debug( + context.Background(), + fmt.Sprintf("Attempting to download certificate with ID: %d from Collection ID: %d", certID, collectionID), + ) + certificateContext, err := kfClient.GetCertificateContext(&req) + if err != nil { + if maxRetry > 0 && i >= maxRetry { + tflog.Warn(context.Background(), fmt.Sprintf("Max retry attempts reached, returning error: %s", err)) + return nil, err + } + if totalSleep >= maxSleep { + //log.Printf("[WARNING] Max retry attempts reached, returning error: %s", err) + tflog.Warn(context.Background(), fmt.Sprintf("Max retry attempts reached, returning error: %s", err)) + return nil, err + } + if strings.Contains(strings.ToLower(err.Error()), strings.ToLower(ERR_COLLECTION_WAIT)) { + //log.Printf("[WARNING] Error downloading certificate: %s, will attempt to retry ", err) + tflog.Warn( + context.Background(), + fmt.Sprintf("Error downloading certificate: %s, will attempt to retry ", err), + ) + + delay := time.Duration(i) * time.Second + if maxRetry > 0 { + tflog.Debug( + context.Background(), + fmt.Sprintf( + "Max retry attempts set to %d, defaulting to %ss intervals", + maxRetry, + MAX_WAIT_SECONDS, + ), + ) + delay = time.Duration(MAX_WAIT_SECONDS) * time.Second + } + if delay > time.Duration(MAX_WAIT_SECONDS)*time.Second { + delay = time.Duration(MAX_WAIT_SECONDS) * time.Second + } + //log.Printf("[DEBUG] Sleeping for %s seconds before retrying", delay) + tflog.Debug(context.Background(), fmt.Sprintf("Sleeping for %s seconds before retrying", delay)) + time.Sleep(delay) + totalSleep += int(delay.Seconds()) + continue + } + tflog.Error(context.Background(), fmt.Sprintf("Error downloading certificate: %s", err)) + return nil, err + } + tflog.Debug( + context.Background(), + fmt.Sprintf("Successfully downloaded certificate with ID: %d from Collection ID: %d", certID, collectionID), + ) + return certificateContext, nil + } + + tflog.Warn( + context.Background(), + fmt.Sprintf( + "This should be unreachable, attempting one last time to download certificate with ID: %d from Collection ID: %d", + certID, + collectionID, + ), + ) + certificateContext, err := kfClient.GetCertificateContext(&req) + if err != nil { + tflog.Error(context.Background(), fmt.Sprintf("Error downloading certificate: %s", err)) + return nil, err + } + tflog.Debug( + context.Background(), + fmt.Sprintf("Successfully downloaded certificate with ID: %d from Collection ID: %d", certID, collectionID), + ) + return certificateContext, nil +} + +func downloadCertificate( + id int, + collectionId int, + kfClient *api.Client, + password string, + csrEnrollment bool, + maxCollectionEnrollWait int, + maxCollectionEnrollWaitRetry int, +) ( string, string, string, error, ) { - log.Printf("[DEBUG] enter downloadCertificate") - log.Printf("[INFO] Downloading certificate with ID: %d", id) + //log.Printf("[DEBUG] enter downloadCertificate") + //log.Printf("[INFO] Downloading certificate with ID: %d", id) + tflog.Debug(context.Background(), fmt.Sprintf("Entered downloadCertificate for certificate ID: %d", id)) req := api.GetCertificateContextArgs{ Id: id, } + var ( + certificateContext *api.GetCertificateResponse + err error + ) + if collectionId > 0 { - log.Printf("[INFO] Downloading certificate '%d' from Collection ID: %d", id, collectionId) + //log.Printf("[DEBUG] Downloading certificate '%d' from Collection ID: %d", id, collectionId) + tflog.Debug( + context.Background(), + fmt.Sprintf("Downloading certificate '%d' from Collection ID: %d", id, collectionId), + ) req.CollectionId = &collectionId - } - log.Printf("[INFO] Downloading certificate from Keyfactor Command") - log.Printf("[DEBUG] Request: %+v", req) - certificateContext, err := kfClient.GetCertificateContext(&req) - if err != nil { - log.Printf("[ERROR] Error downloading certificate: %s", err) - return "", "", "", err + tflog.Debug(context.Background(), fmt.Sprintf("Calling retryCertCollectionHandler for certificate ID: %d", id)) + certificateContext, err = retryCertCollectionHandler( + kfClient, + id, + collectionId, + maxCollectionEnrollWait, + maxCollectionEnrollWaitRetry, + ) + if err != nil { + return "", "", "", err + } + } else { + //log.Printf("[INFO] Downloading certificate from Keyfactor Command") + //log.Printf("[DEBUG] Request: %+v", req) + tflog.Debug(context.Background(), fmt.Sprintf("Downloading certificate from Keyfactor Command")) + certificateContext, err = kfClient.GetCertificateContext(&req) + if err != nil { + return "", "", "", err + } } - log.Printf("[INFO] Looking up certificate template with ID: %d", certificateContext.TemplateId) + //log.Printf("[INFO] Looking up certificate template with ID: %d", certificateContext.TemplateId) + tflog.Debug( + context.Background(), + fmt.Sprintf("Looking up certificate template with ID: %d", certificateContext.TemplateId), + ) template, err := kfClient.GetTemplate(certificateContext.TemplateId) if err != nil { log.Printf( @@ -417,60 +542,88 @@ func downloadCertificate(id int, collectionId int, kfClient *api.Client, passwor var chainPem []byte if recoverable && !csrEnrollment { - log.Printf("[INFO] Recovering certificate with ID: %d", id) + //log.Printf("[INFO] Recovering certificate with ID: %d", id) + tflog.Info(context.Background(), fmt.Sprintf("Recovering certificate with ID: %d", id)) //priv, leaf, chain, rErr := kfClient.RecoverCertificate(id, "", "", "", password) priv, leaf, chain, rErr := kfClient.RecoverCertificate(id, "", "", "", password, collectionId) if rErr != nil { - log.Printf("[ERROR] Error recovering certificate: %s", rErr) return "", "", "", rErr } // Encode DER to PEM - log.Printf("[DEBUG] Encoding certificate to PEM") + tflog.Debug(context.Background(), fmt.Sprintf("Encoding certificate to PEM for certificate ID: %d", id)) leafPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leaf.Raw}) - log.Printf("[DEBUG] Encoding chain to PEM") + tflog.Debug(context.Background(), fmt.Sprintf("Encoding chain to PEM for certificate ID: %d", id)) for _, i := range chain { chainPem = append(chainPem, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: i.Raw})...) } - log.Printf("[DEBUG] Chain PEM: %s", chainPem) - - log.Printf("[DEBUG] Encoding private key to PEM") - // Figure out the format of the private key, then encode it to PEM + //log.Printf("[DEBUG] Chain PEM: %s", chainPem) + tflog.Debug(context.Background(), fmt.Sprintf("Encoding private key to PEM for certificate ID: %d", id)) - log.Printf("[DEBUG] Private Key Type: %T", priv) + //log.Printf("[DEBUG] Private Key Type: %T", priv) + tflog.Debug(context.Background(), fmt.Sprintf("Private Key Type: %T", priv)) rsa, ok := priv.(*rsa2.PrivateKey) if ok { - log.Printf("[INFO] Private Key is RSA for certificate ID: %d", id) + //log.Printf("[INFO] Private Key is RSA for certificate ID: %d", id) + tflog.Debug(context.Background(), fmt.Sprintf("Attempting to parse RSA key for certificate ID: %d", id)) buf := x509.MarshalPKCS1PrivateKey(rsa) if len(buf) > 0 { + tflog.Debug( + context.Background(), + fmt.Sprintf("RSA Private Key successfully parsed for certificate ID: %d", id), + ) + tflog.Debug( + context.Background(), + fmt.Sprintf("Attempting to encode RSA private key to PEM for certificate ID: %d", id), + ) privPem = pem.EncodeToMemory(&pem.Block{Bytes: buf, Type: "RSA PRIVATE KEY"}) } } if privPem == nil { - log.Printf("[INFO] Private Key is not RSA for certificate ID: %d attempting to parse ECC key", id) + //log.Printf("[INFO] Private Key is not RSA for certificate ID: %d attempting to parse ECC key", id) + tflog.Debug( + context.Background(), + fmt.Sprintf("Private Key is not RSA for certificate ID: %d attempting to parse ECC key", id), + ) ecc, ok := priv.(*ecdsa.PrivateKey) if ok { - log.Printf("[INFO] Private Key is ECDSA for certificate ID: %d", id) + tflog.Debug( + context.Background(), + fmt.Sprintf("Attempting to parse ECDSA key for certificate ID: %d", id), + ) buf, _ := x509.MarshalECPrivateKey(ecc) if len(buf) > 0 { + tflog.Debug( + context.Background(), + fmt.Sprintf("ECDSA Private Key successfully parsed for certificate ID: %d", id), + ) + tflog.Debug( + context.Background(), + fmt.Sprintf("Attempting to encode ECDSA private key to PEM for certificate ID: %d", id), + ) privPem = pem.EncodeToMemory(&pem.Block{Bytes: buf, Type: "EC PRIVATE KEY"}) } } } } else { - log.Printf("[INFO] Downloading certificate with ID: %d", id) + //log.Printf("[INFO] Downloading certificate with ID: %d", id) + tflog.Debug(context.Background(), fmt.Sprintf("Calling DownloadCertificate for certificate ID: %d", id)) leaf, chain, dlErr := kfClient.DownloadCertificate(id, "", "", "") if dlErr != nil { - log.Printf("[ERROR] Error downloading certificate: %s", dlErr) + //log.Printf("[ERROR] Error downloading certificate: %s", dlErr) + tflog.Error(context.Background(), fmt.Sprintf("Error downloading certificate: %s", dlErr)) return "", "", "", err } // Encode DER to PEM - log.Printf("[DEBUG] Encoding certificate to PEM") + //log.Printf("[DEBUG] Encoding certificate to PEM") + tflog.Debug(context.Background(), fmt.Sprintf("Encoding certificate to PEM for certificate ID: %d", id)) leafPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leaf.Raw}) - log.Printf("[DEBUG] Certificate PEM: %s", leafPem) - log.Printf("[DEBUG] Encoding chain to PEM") + tflog.Trace(context.Background(), fmt.Sprintf("Certificate PEM: %s", leafPem)) + //log.Printf("[DEBUG] Certificate PEM: %s", leafPem) + //log.Printf("[DEBUG] Encoding chain to PEM") + tflog.Debug(context.Background(), fmt.Sprintf("Encoding chain to PEM for certificate ID: %d", id)) // iterate through chain in reverse order for i := len(chain) - 1; i >= 0; i-- { // check if current cert is the leaf cert @@ -479,10 +632,11 @@ func downloadCertificate(id int, collectionId int, kfClient *api.Client, passwor } chainPem = append(chainPem, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: chain[i].Raw})...) } - log.Printf("[DEBUG] Chain PEM: %s", chainPem) + //log.Printf("[DEBUG] Chain PEM: %s", chainPem) + tflog.Trace(context.Background(), fmt.Sprintf("Chain PEM: %s", chainPem)) } - log.Printf("[DEBUG] exit downloadCertificate") + tflog.Debug(context.Background(), fmt.Sprintf("Successfully downloaded certificate with ID: %d", id)) return string(leafPem), string(chainPem), string(privPem), nil } @@ -594,3 +748,49 @@ func sortInSameOrder(unsortedList, sortedList []string) []string { } return sorted } + +func parseDuration(duration string) (int, error) { + totalSeconds := 0 + + // check if is an integer + if i, err := strconv.Atoi(duration); err == nil { + return i, nil + } + + // Split the original duration string by digits and non-digits + durationParts := strings.Split(duration, "h") + if len(durationParts) > 1 { + // Process hours + hoursPart := durationParts[0] + hours, err := strconv.Atoi(hoursPart) + if err != nil { + return 0, err + } + totalSeconds += hours * 3600 + duration = durationParts[1] + } else { + durationParts = strings.Split(duration, "m") + if len(durationParts) > 1 { + // Process minutes + minutesPart := durationParts[0] + minutes, err := strconv.Atoi(minutesPart) + if err != nil { + return 0, err + } + totalSeconds += minutes * 60 + duration = durationParts[1] + } + } + + // Process seconds + if strings.Contains(duration, "s") { + secondsPart := strings.TrimSuffix(duration, "s") + seconds, err := strconv.Atoi(secondsPart) + if err != nil { + return 0, err + } + totalSeconds += seconds + } + + return totalSeconds, nil +} diff --git a/keyfactor/models.go b/keyfactor/models.go index d47fc7b8..961e2e1d 100644 --- a/keyfactor/models.go +++ b/keyfactor/models.go @@ -66,12 +66,14 @@ type KeyfactorCertificate struct { PrivateKey types.String `tfsdk:"private_key"` KeyPassword types.String `tfsdk:"key_password"` // Keyfactor Fields - CertificateAuthority types.String `tfsdk:"certificate_authority"` - CertificateTemplate types.String `tfsdk:"certificate_template"` - RequestId types.Int64 `tfsdk:"command_request_id"` - CertificateId types.Int64 `tfsdk:"certificate_id"` - Metadata types.Map `tfsdk:"metadata"` - CollectionId types.Int64 `tfsdk:"collection_id"` + CertificateAuthority types.String `tfsdk:"certificate_authority"` + CertificateTemplate types.String `tfsdk:"certificate_template"` + RequestId types.Int64 `tfsdk:"command_request_id"` + CertificateId types.Int64 `tfsdk:"certificate_id"` + Metadata types.Map `tfsdk:"metadata"` + CollectionId types.Int64 `tfsdk:"collection_id"` + CollectionEnrollmentWait types.String `tfsdk:"collection_enrollment_wait"` + CollectionEnrollmentWaitMaxRetries types.Int64 `tfsdk:"collection_enrollment_wait_max_retries"` } type KeyfactorCertificateDeployment struct { diff --git a/keyfactor/provider.go b/keyfactor/provider.go index e5080c53..2782223e 100644 --- a/keyfactor/provider.go +++ b/keyfactor/provider.go @@ -3,17 +3,19 @@ package keyfactor import ( "context" "fmt" + "os" + "strconv" + "time" + "github.com/Keyfactor/keyfactor-go-client/v2/api" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - "os" - "strconv" - "time" ) var stderr = os.Stderr +var LOG_INSECURE = false //todo: WARNING Do not set to true for a public release func New() tfsdk.Provider { return &provider{} @@ -78,16 +80,40 @@ type providerData struct { RequestTimeout types.Int64 `tfsdk:"request_timeout"` } -func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) { +func loggingInsecure() bool { + if os.Getenv("KEYFACTOR_LOG_INSECURE") == "true" || os.Getenv("TF_LOG_INSECURE") == "true" { + LOG_INSECURE = true + } //TODO: WARNING: THIS IS NOT A GOOD IDEA and should not be included in a public release + return LOG_INSECURE +} + +func (p *provider) Configure( + ctx context.Context, + req tfsdk.ConfigureProviderRequest, + resp *tfsdk.ConfigureProviderResponse, +) { // Retrieve provider data from configuration var config providerData - + loggingInsecure() + tflog.Debug(ctx, "Configuring Keyfactor provider") diags := req.Config.Get(ctx, &config) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { + tflog.Error(ctx, "Error configuring Keyfactor provider") return } + ctx = tflog.SetField(ctx, "hostname", config.Hostname.Value) + ctx = tflog.SetField(ctx, "username", config.Username.Value) + ctx = tflog.SetField(ctx, "domain", config.Domain.Value) + ctx = tflog.SetField(ctx, "appkey", config.ApiKey.Value) + ctx = tflog.SetField(ctx, "password", config.Password.Value) + if config.Password.Value != "" && !LOG_INSECURE { + ctx = tflog.MaskLogStrings(ctx, config.Password.Value) + } + ctx = tflog.SetField(ctx, "request_timeout", config.RequestTimeout.Value) + tflog.Debug(ctx, "Provider configuration complete") + // User must provide a user to the provider var username string if config.Username.Unknown { @@ -100,11 +126,15 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq return } if config.Username.Null { - tflog.Debug(ctx, fmt.Sprintf("Provider username is NULL, attempting to source from %s", EnvCommandUsername)) + tflog.Debug(ctx, fmt.Sprintf("Provider username is NULL, attempting to source from '%s'", EnvCommandUsername)) username = os.Getenv(EnvCommandUsername) config.Username.Value = username + ctx = tflog.SetField(ctx, "username", username) + tflog.Debug(ctx, fmt.Sprintf("Provider username sourced from environmental variable '%s'", EnvCommandUsername)) } else { username = config.Username.Value + ctx = tflog.SetField(ctx, "username", username) + tflog.Debug(ctx, "Provider username sourced provider config 'username'") } if username == "" { // Error vs warning - empty value must stop execution @@ -125,10 +155,15 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq return } if config.Domain.Null { - domain = os.Getenv("KEYFACTOR_DOMAIN") + tflog.Debug(ctx, fmt.Sprintf("Provider 'domain' is NULL, attempting to source from '%s'", EnvCommandDomain)) + domain = os.Getenv(EnvCommandDomain) config.Domain.Value = domain + ctx = tflog.SetField(ctx, "domain", domain) + tflog.Debug(ctx, fmt.Sprintf("Provider 'domain' sourced from environmental variable '%s'", EnvCommandDomain)) } else { domain = config.Domain.Value + ctx = tflog.SetField(ctx, "domain", domain) + tflog.Debug(ctx, "Provider 'domain' sourced provider config 'domain'") } if domain == "" { // Error vs warning - empty value must stop execution @@ -151,10 +186,18 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq } if config.ApiKey.Null { - apiKey = os.Getenv("KEYFACTOR_APPKEY") + tflog.Debug(ctx, fmt.Sprintf("Provider 'appkey' is NULL, attempting to source from '%s'", EnvCommandAppKey)) + apiKey = os.Getenv(EnvCommandAppKey) config.ApiKey.Value = apiKey + ctx = tflog.SetField(ctx, "appkey", apiKey) + if apiKey != "" && !LOG_INSECURE { + ctx = tflog.MaskLogStrings(ctx, apiKey) + } + tflog.Debug(ctx, fmt.Sprintf("Provider 'appkey' sourced from environmental variable '%s'", EnvCommandAppKey)) } else { apiKey = config.ApiKey.Value + ctx = tflog.SetField(ctx, "appkey", apiKey) + tflog.Debug(ctx, "Provider 'appkey' sourced provider config 'appkey'") } // User must provide a password to the provider @@ -169,16 +212,30 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq } if config.Password.Null { - password = os.Getenv("KEYFACTOR_PASSWORD") + tflog.Debug(ctx, fmt.Sprintf("Provider 'password' is NULL, attempting to source from '%s'", EnvCommandPassword)) + password = os.Getenv(EnvCommandPassword) config.Password.Value = password + ctx = tflog.SetField(ctx, "password", password) + if password != "" && !LOG_INSECURE { + ctx = tflog.MaskLogStrings(ctx, password) + } + tflog.Debug( + ctx, + fmt.Sprintf("Provider 'password' sourced from environmental variable '%s'", EnvCommandPassword), + ) } else { password = config.Password.Value + ctx = tflog.SetField(ctx, "password", password) + if password != "" && !LOG_INSECURE { + ctx = tflog.MaskLogStrings(ctx, password) + } + tflog.Debug(ctx, "Provider 'password' sourced provider config 'password'") } if password == "" && apiKey == "" { // Error vs warning - empty value must stop execution resp.Diagnostics.AddError( - "Invlaid provider credentials. ", + "Invalid provider credentials. ", "`password` and `appkey` cannot both be empty string.", ) return @@ -196,10 +253,18 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq } if config.Hostname.Null { - host = os.Getenv("KEYFACTOR_HOSTNAME") + tflog.Debug(ctx, fmt.Sprintf("Provider 'hostname' is NULL, attempting to source from '%s'", EnvCommandHostname)) + host = os.Getenv(EnvCommandHostname) config.Hostname.Value = host + ctx = tflog.SetField(ctx, "hostname", host) + tflog.Debug( + ctx, + fmt.Sprintf("Provider 'hostname' sourced from environmental variable '%s'", EnvCommandHostname), + ) } else { host = config.Hostname.Value + ctx = tflog.SetField(ctx, "hostname", host) + tflog.Debug(ctx, "Provider 'hostname' sourced provider config 'hostname'") } if host == "" { @@ -213,11 +278,21 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq // Set default request timeout if config.RequestTimeout.Null { - timeout := os.Getenv("KEYFACTOR_TIMEOUT") + tflog.Debug(ctx, "Provider 'request_timeout' is NULL, attempting to source from 'KEYFACTOR_TIMEOUT'") + timeout := os.Getenv(EnvCommandTimeout) if timeout == "" { + tflog.Debug( + ctx, + fmt.Sprintf("Provider 'request_timeout' not set, using default value of %d", MAX_WAIT_SECONDS), + ) + ctx = tflog.SetField(ctx, "request_timeout", MAX_WAIT_SECONDS) config.RequestTimeout.Value = MAX_WAIT_SECONDS } else { //convert string to int + tflog.Debug( + ctx, + fmt.Sprintf("Provider 'request_timeout' sourced from environmental variable '%s'", EnvCommandTimeout), + ) timeoutInt, err := strconv.Atoi(timeout) if err != nil { resp.Diagnostics.AddError( @@ -227,11 +302,12 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq return } config.RequestTimeout.Value = int64(timeoutInt) + ctx = tflog.SetField(ctx, "request_timeout", timeoutInt) } - } // Create a new Keyfactor client and set it to the provider client + tflog.Debug(ctx, "Creating Keyfactor Command API client") var clientAuth api.AuthConfig clientAuth.Username = config.Username.Value clientAuth.Password = config.Password.Value @@ -243,7 +319,8 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq connected := false connectionRetries := 0 for !connected && connectionRetries < 5 { - c, err := api.NewKeyfactorClient(&clientAuth) + tflog.Debug(ctx, "Attempting to create client connection to Keyfactor Command") + c, err := api.NewKeyfactorClient(&clientAuth, &ctx) if err != nil { if connectionRetries == 4 { @@ -255,12 +332,14 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq } connectionRetries++ // Sleep for 5 seconds before retrying + tflog.Debug(ctx, "Failed to create client connection to Keyfactor Command. Retrying in 5 seconds.") time.Sleep(5 * time.Second) continue } connected = true p.client = c p.configured = true + tflog.Debug(ctx, "Client connection to Keyfactor Command established successfully") return } } diff --git a/keyfactor/resource_keyfactor_certificate.go b/keyfactor/resource_keyfactor_certificate.go index b9846370..f966fe95 100644 --- a/keyfactor/resource_keyfactor_certificate.go +++ b/keyfactor/resource_keyfactor_certificate.go @@ -201,6 +201,27 @@ func (r resourceKeyfactorCertificateType) GetSchema(_ context.Context) (tfsdk.Sc Sensitive: true, Description: "PEM formatted PKCS#1 private key imported if cert_template has KeyRetention set to a value other than None, and the certificate was not enrolled using a CSR.", }, + "collection_enrollment_wait": { + Type: types.StringType, + Computed: false, + Optional: true, + Description: "The maximum time to wait for a certificate to be added to a collection, " + + "post enrollment. This is useful for certificates that trigger issue handlers and/or workflows" + + " post enrollment and will delay the certificate being added to the expected collection. Format" + + ": 1h, " + + "1m, 1s. Default: 0.", + }, + "collection_enrollment_wait_max_retries": { + Type: types.Int64Type, + Computed: false, + Optional: true, + Description: fmt.Sprintf( + "The maximum number of times to retry checking if a certificate has been"+ + " added to a collection in %d second intervals. "+ + "This takes precedence over `collection_enrollment_wait`. Default: 0.", + MAX_WAIT_SECONDS, + ), + }, }, }, nil } @@ -244,6 +265,8 @@ func (r resourceKeyfactorCertificate) Create( // Generate API request body from plan kfClient := r.p.client + ctx = tflog.SetField(ctx, "command_hostname", r.p.client.Hostname) + ctx = tflog.SetField(ctx, "command_api_path", r.p.client.ApiPath) certificateId := plan.ID.Value collectionId := plan.CollectionId.Value @@ -403,6 +426,14 @@ func (r resourceKeyfactorCertificate) Create( CertificateTemplate: plan.CertificateTemplate, Metadata: plan.Metadata, CollectionId: plan.CollectionId, + CollectionEnrollmentWait: types.String{ + Value: plan.CollectionEnrollmentWait.Value, + Null: isNullString(plan.CollectionEnrollmentWait.Value), + }, + CollectionEnrollmentWaitMaxRetries: types.Int64{ + Value: plan.CollectionEnrollmentWaitMaxRetries.Value, + Null: isNullInt64(plan.CollectionEnrollmentWaitMaxRetries.Value), + }, } diags = response.State.Set(ctx, result) @@ -468,13 +499,13 @@ func (r resourceKeyfactorCertificate) Create( tflog.Debug(ctx, fmt.Sprintf("PFXArgs: %s", string(jsonData))) tflog.Debug(ctx, fmt.Sprintf("Creating PFX certificate %s on Keyfactor.", PFXArgs.Subject.SubjectCommonName)) tflog.Debug(ctx, "Calling EnrollPFXV2.") - enrollResponse, err := r.p.client.EnrollPFXV2(PFXArgs) - if err != nil { - tflog.Error(ctx, "No response from Keyfactor Command after PFX enrollment.") + enrollResponse, eErr := r.p.client.EnrollPFXV2(PFXArgs) + if eErr != nil { + tflog.Error(ctx, "Error response from Keyfactor Command after PFX enrollment.") response.Diagnostics.AddError( ERR_SUMMARY_CERTIFICATE_RESOURCE_CREATE, fmt.Sprintf( - "Could not create certificate %s on Keyfactor: "+err.Error(), + "Could not create certificate %s on Keyfactor Command: "+eErr.Error(), PFXArgs.Subject.SubjectCommonName, ), ) @@ -589,12 +620,19 @@ func (r resourceKeyfactorCertificate) Create( ), ) tflog.Debug(ctx, "Calling downloadCertificate") + maxColletionWait, wErr := parseDuration(plan.CollectionEnrollmentWait.Value) + maxRetries := int(plan.CollectionEnrollmentWaitMaxRetries.Value) + if wErr != nil { + maxColletionWait = 0 + } leaf, chain, pKey, dErr := downloadCertificate( enrolledId, int(collectionId), r.p.client, lookupPassword, csr != "", + maxColletionWait, + maxRetries, ) if dErr != nil { response.Diagnostics.AddError( @@ -644,6 +682,14 @@ func (r resourceKeyfactorCertificate) Create( RequestId: types.Int64{Value: int64(enrollResponse.CertificateInformation.KeyfactorRequestID)}, Metadata: plan.Metadata, CollectionId: plan.CollectionId, + CollectionEnrollmentWait: types.String{ + Value: plan.CollectionEnrollmentWait.Value, + Null: isNullString(plan.CollectionEnrollmentWait.Value), + }, + CollectionEnrollmentWaitMaxRetries: types.Int64{ + Value: plan.CollectionEnrollmentWaitMaxRetries.Value, + Null: isNullInt64(plan.CollectionEnrollmentWaitMaxRetries.Value), + }, } tflog.Debug(ctx, "Setting state") @@ -657,6 +703,14 @@ func (r resourceKeyfactorCertificate) Create( } +func isNullInt64(value int64) bool { + if value == 0 { + return true + } + return false + +} + func (r resourceKeyfactorCertificate) Read( ctx context.Context, request tfsdk.ReadResourceRequest, @@ -752,6 +806,15 @@ func (r resourceKeyfactorCertificate) Read( CertificateTemplate: nullValue, Metadata: types.Map{Null: true, ElemType: types.StringType}, CertificateId: types.Int64{Null: true}, + CollectionId: types.Int64{Null: true}, + CollectionEnrollmentWait: types.String{ + Value: state.CollectionEnrollmentWait.Value, + Null: isNullString(state.CollectionEnrollmentWait.Value), + }, + CollectionEnrollmentWaitMaxRetries: types.Int64{ + Value: state.CollectionEnrollmentWaitMaxRetries.Value, + Null: isNullInt64(state.CollectionEnrollmentWaitMaxRetries.Value), + }, } diags = response.State.Set(ctx, &emptyResult) response.Diagnostics.Append(diags...) @@ -778,7 +841,8 @@ func (r resourceKeyfactorCertificate) Read( ctx, fmt.Sprintf("Downloading certificate '%s'(%d) from Keyfactor Command.", state.ID.Value, certificateIdInt), ) - _, _, _, dErr := downloadCertificate(certificateIdInt, collectionIdInt, r.p.client, lookupPassword, csr != "") + + _, _, _, dErr := downloadCertificate(certificateIdInt, collectionIdInt, r.p.client, lookupPassword, csr != "", 0, 0) if dErr != nil { tflog.Error(ctx, "Error downloading certificate from Keyfactor Command.") response.Diagnostics.AddError( @@ -954,6 +1018,8 @@ func (r resourceKeyfactorCertificate) Read( r.p.client, lookupPassword, csr != "", + 0, + 0, ) //leaf, chain, pKey, dErr := downloadCertificate(enrolledId, int(collectionId), r.p.client, lookupPassword, csr != "") if dChainErr != nil { @@ -1022,7 +1088,15 @@ func (r resourceKeyfactorCertificate) Read( CertificateTemplate: state.CertificateTemplate, Metadata: metadata, CertificateId: types.Int64{Value: int64(cResp.Id), Null: isNullId(cResp.Id)}, - CollectionId: state.CollectionId, + CollectionId: types.Int64{Value: int64(collectionIdInt), Null: isNullId(collectionIdInt)}, + CollectionEnrollmentWait: types.String{ + Value: state.CollectionEnrollmentWait.Value, + Null: isNullString(state.CollectionEnrollmentWait.Value), + }, + CollectionEnrollmentWaitMaxRetries: types.Int64{ + Value: state.CollectionEnrollmentWaitMaxRetries.Value, + Null: isNullInt64(state.CollectionEnrollmentWaitMaxRetries.Value), + }, } } else { tflog.Debug(ctx, "Creating state object for certificate PFX.") @@ -1058,6 +1132,14 @@ func (r resourceKeyfactorCertificate) Read( Metadata: metadata, CertificateId: types.Int64{Value: int64(cResp.Id), Null: isNullId(cResp.Id)}, CollectionId: state.CollectionId, + CollectionEnrollmentWait: types.String{ + Value: state.CollectionEnrollmentWait.Value, + Null: isNullString(state.CollectionEnrollmentWait.Value), + }, + CollectionEnrollmentWaitMaxRetries: types.Int64{ + Value: state.CollectionEnrollmentWaitMaxRetries.Value, + Null: isNullInt64(state.CollectionEnrollmentWaitMaxRetries.Value), + }, } } @@ -1146,12 +1228,15 @@ func (r resourceKeyfactorCertificate) Update( } if !plan.Metadata.Equal(state.Metadata) { tflog.Debug(ctx, "Metadata is updated. Attempting to update metadata on Keyfactor.") - + metadataReq := &api.UpdateMetadataArgs{ + CertID: certificateIdInt, + Metadata: metaInterface, + } + if plan.CollectionId.Value > 0 { + metadataReq.CollectionId = int(plan.CollectionId.Value) + } err := r.p.client.UpdateMetadata( - &api.UpdateMetadataArgs{ - CertID: certificateIdInt, - Metadata: metaInterface, - }, + metadataReq, ) if err != nil { response.Diagnostics.AddError( @@ -1187,6 +1272,15 @@ func (r resourceKeyfactorCertificate) Update( CertificateAuthority: plan.CertificateAuthority, CertificateTemplate: plan.CertificateTemplate, Metadata: plan.Metadata, + CollectionId: plan.CollectionId, + CollectionEnrollmentWait: types.String{ + Value: plan.CollectionEnrollmentWait.Value, + Null: isNullString(plan.CollectionEnrollmentWait.Value), + }, + CollectionEnrollmentWaitMaxRetries: types.Int64{ + Value: plan.CollectionEnrollmentWaitMaxRetries.Value, + Null: isNullId(int(plan.CollectionEnrollmentWaitMaxRetries.Value)), + }, } diags = response.State.Set(ctx, result) @@ -1213,12 +1307,15 @@ func (r resourceKeyfactorCertificate) Update( planMetadataInterface[k] = v } tflog.Info(ctx, fmt.Sprintf("Updating metadata for certificate '%s' on Keyfactor Command.", state.ID.Value)) - err := r.p.client.UpdateMetadata( - &api.UpdateMetadataArgs{ - CertID: int(state.CertificateId.Value), - Metadata: planMetadataInterface, - }, - ) + updateReq := &api.UpdateMetadataArgs{ + CertID: int(state.CertificateId.Value), + Metadata: planMetadataInterface, + } + if plan.CollectionId.Value > 0 { + tflog.Debug(ctx, "Setting collection ID on API request") + updateReq.CollectionId = int(plan.CollectionId.Value) + } + err := r.p.client.UpdateMetadata(updateReq) if err != nil { response.Diagnostics.AddError( "Certificate metadata update error.", @@ -1254,6 +1351,15 @@ func (r resourceKeyfactorCertificate) Update( CertificateAuthority: state.CertificateAuthority, CertificateTemplate: state.CertificateTemplate, Metadata: plan.Metadata, + CollectionId: state.CollectionId, + CollectionEnrollmentWait: types.String{ + Value: plan.CollectionEnrollmentWait.Value, + Null: isNullString(plan.CollectionEnrollmentWait.Value), + }, + CollectionEnrollmentWaitMaxRetries: types.Int64{ + Value: plan.CollectionEnrollmentWaitMaxRetries.Value, + Null: isNullInt64(plan.CollectionEnrollmentWaitMaxRetries.Value), + }, } diags = response.State.Set(ctx, result) @@ -1419,6 +1525,8 @@ func (r resourceKeyfactorCertificate) ImportState( r.p.client, password, csr != "", + 0, + 0, ) // add support for importing with collection ID if dErr != nil { tflog.Error( diff --git a/main.go b/main.go index cc91ac7e..396a747e 100644 --- a/main.go +++ b/main.go @@ -3,9 +3,10 @@ package main import ( "context" "flag" + "log" + "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/keyfactor-pub/terraform-provider-keyfactor/keyfactor" - "log" ) // Run "go generate" to format example terraform files and generate the docs for the registry/website @@ -35,6 +36,7 @@ func main() { ProtocolVersion: 6, } + log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime)) err := providerserver.Serve(context.Background(), keyfactor.New, opts) if err != nil { diff --git a/tag.sh b/tag.sh index 70ae5c89..1f0656c6 100755 --- a/tag.sh +++ b/tag.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -TAG_VERSION=v2.1.11 +TAG_VERSION=v2.2.0-rc.8 git tag -d $TAG_VERSION || true git push origin :$TAG_VERSION || true git tag $TAG_VERSION