diff --git a/CHANGELOG.md b/CHANGELOG.md index 559ed02aa..d7fe27e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # v0.17.0 (Unreleased) ### Resources +* **New Resource:** `pingfederate_keypairs_signing_csr_export` ([#307](https://github.com/pingidentity/terraform-provider-pingfederate/pull/307)) * **New Resource:** `pingfederate_idp_token_processor` ([#277]([https](https://github.com/pingidentity/terraform-provider-pingfederate/pull/277))) * **New Resource:** `pingfederate_protocol_metadata_signing_settings` ([#290](https://github.com/pingidentity/terraform-provider-pingfederate/pull/290)) * **New Resource:** `pingfederate_service_authentication` ([#295](https://github.com/pingidentity/terraform-provider-pingfederate/pull/295)) diff --git a/docs/resources/keypairs_signing_csr_export.md b/docs/resources/keypairs_signing_csr_export.md new file mode 100644 index 000000000..aa6edb72c --- /dev/null +++ b/docs/resources/keypairs_signing_csr_export.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "pingfederate_keypairs_signing_csr_export Resource - terraform-provider-pingfederate" +subcategory: "" +description: |- + Datasource to generate a new certificate signing request (CSR) for a key pair. +--- + +# pingfederate_keypairs_signing_csr_export (Resource) + +Datasource to generate a new certificate signing request (CSR) for a key pair. + +## Example Usage + +```terraform +// Example of using the time provider to control regular export of CSR +resource "time_rotating" "csr_export" { + rotation_days = 30 +} + +resource "pingfederate_keypairs_signing_csr_export" "signingCsr" { + keypair_id = "mysigningkeypair" + export_trigger_values = { + "export_rfc3339" : time_rotating.csr_export.rotation_rfc3339, + } +} +``` + + +## Schema + +### Required + +- `keypair_id` (String) The ID of the keypair. + +### Optional + +- `export_trigger_values` (Map of String) A meta-argument map of values that, if any values are changed, will force export of a new CSR. Adding values to and removing values from the map will not trigger an export. This parameter can be used to control time-based exports using Terraform. + +### Read-Only + +- `exported_csr` (String) The exported PEM-encoded certificate signing request. +- `id` (String) The ID of this resource. diff --git a/examples/resources/pingfederate_keypairs_signing_csr_export/resource.tf b/examples/resources/pingfederate_keypairs_signing_csr_export/resource.tf new file mode 100644 index 000000000..ca85250be --- /dev/null +++ b/examples/resources/pingfederate_keypairs_signing_csr_export/resource.tf @@ -0,0 +1,11 @@ +// Example of using the time provider to control regular export of CSR +resource "time_rotating" "csr_export" { + rotation_days = 30 +} + +resource "pingfederate_keypairs_signing_csr_export" "signingCsr" { + keypair_id = "mysigningkeypair" + export_trigger_values = { + "export_rfc3339" : time_rotating.csr_export.rotation_rfc3339, + } +} \ No newline at end of file diff --git a/internal/acctest/config/keypairs/signing/csr/keypairs_signing_csr_export_resource_test.go b/internal/acctest/config/keypairs/signing/csr/keypairs_signing_csr_export_resource_test.go new file mode 100644 index 000000000..933125e14 --- /dev/null +++ b/internal/acctest/config/keypairs/signing/csr/keypairs_signing_csr_export_resource_test.go @@ -0,0 +1,97 @@ +package keypairssigningcsr_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/pingidentity/terraform-provider-pingfederate/internal/acctest" + "github.com/pingidentity/terraform-provider-pingfederate/internal/provider" +) + +func TestAccKeypairsSigningCsrExportResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.ConfigurationPreCheck(t) }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "pingfederate": providerserver.NewProtocol6WithError(provider.NewTestProvider()), + }, + Steps: []resource.TestStep{ + { + // Run the export and validate the results + Config: keypairsSigningCsrExportResource_MinimalHCL(), + Check: keypairsSigningCsrExportResource_CheckComputedValues(), + }, + { + // Expect no additional rotation + Config: keypairsSigningCsrExportResource_NoExportHCL(), + Check: keypairsSigningCsrExportResource_CheckComputedValues(), + }, + { + // Expect rotation + Config: keypairsSigningCsrExportResource_SecondExportHCL(), + Check: keypairsSigningCsrExportResource_CheckComputedValues(), + }, + { + // Expect no additional rotation + Config: keypairsSigningCsrExportResource_SecondNoExportHCL(), + Check: keypairsSigningCsrExportResource_CheckComputedValues(), + }, + { + // Back to the original with no trigger values + Config: keypairsSigningCsrExportResource_MinimalHCL(), + Check: keypairsSigningCsrExportResource_CheckComputedValues(), + }, + }, + }) +} + +func keypairsSigningCsrExportResource_MinimalHCL() string { + return ` +resource "pingfederate_keypairs_signing_csr_export" "example" { + keypair_id = "419x9yg43rlawqwq9v6az997k" +} +` +} + +func keypairsSigningCsrExportResource_NoExportHCL() string { + return ` +resource "pingfederate_keypairs_signing_csr_export" "example" { + keypair_id = "419x9yg43rlawqwq9v6az997k" + export_trigger_values = { + "trigger" = "false" + } +} +` +} + +func keypairsSigningCsrExportResource_SecondExportHCL() string { + return ` +resource "pingfederate_keypairs_signing_csr_export" "example" { + keypair_id = "419x9yg43rlawqwq9v6az997k" + export_trigger_values = { + "trigger" = "updated" + "newtrigger" = "new" + } +} +` +} + +func keypairsSigningCsrExportResource_SecondNoExportHCL() string { + return ` +resource "pingfederate_keypairs_signing_csr_export" "example" { + keypair_id = "419x9yg43rlawqwq9v6az997k" + export_trigger_values = { + "trigger" = "updated" + } +} +` +} + +// Validate any computed values when applying HCL +func keypairsSigningCsrExportResource_CheckComputedValues() resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("pingfederate_keypairs_signing_csr_export.example", "exported_csr"), + resource.TestCheckResourceAttr("pingfederate_keypairs_signing_csr_export.example", "id", "419x9yg43rlawqwq9v6az997k"), + ) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index e2cac76d4..02a0cb90b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -56,6 +56,7 @@ import ( keypairsoauthopenidconnectadditionalkeysets "github.com/pingidentity/terraform-provider-pingfederate/internal/resource/config/keypairs/oauthopenidconnect/additionalkeysets" keypairsigning "github.com/pingidentity/terraform-provider-pingfederate/internal/resource/config/keypairs/signing" keypairssigningcertificate "github.com/pingidentity/terraform-provider-pingfederate/internal/resource/config/keypairs/signing/certificate" + keypairssigningcsr "github.com/pingidentity/terraform-provider-pingfederate/internal/resource/config/keypairs/signing/csr" keypairsigningimport "github.com/pingidentity/terraform-provider-pingfederate/internal/resource/config/keypairs/signing/import" keypairssigningrotationsettings "github.com/pingidentity/terraform-provider-pingfederate/internal/resource/config/keypairs/signing/rotationsettings" keypairssslserver "github.com/pingidentity/terraform-provider-pingfederate/internal/resource/config/keypairs/sslserver" @@ -773,6 +774,7 @@ func (p *pingfederateProvider) Resources(_ context.Context) []func() resource.Re keypairsoauthopenidconnectadditionalkeysets.KeypairsOauthOpenidConnectAdditionalKeySetResource, keypairsigning.KeypairsSigningKeyResource, keypairsigningimport.KeyPairsSigningImportResource, + keypairssigningcsr.KeypairsSigningCsrExportResource, keypairssigningrotationsettings.KeypairsSigningKeyRotationSettingsResource, keypairssslserver.KeypairsSslServerKeyResource, keypairsslserverimport.KeyPairsSslServerImportResource, diff --git a/internal/resource/config/keypairs/signing/csr/keypairs_signing_csr_export_resource.go b/internal/resource/config/keypairs/signing/csr/keypairs_signing_csr_export_resource.go new file mode 100644 index 000000000..3315dcd72 --- /dev/null +++ b/internal/resource/config/keypairs/signing/csr/keypairs_signing_csr_export_resource.go @@ -0,0 +1,167 @@ +package keypairssigningcsr + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + client "github.com/pingidentity/pingfederate-go-client/v1210/configurationapi" + "github.com/pingidentity/terraform-provider-pingfederate/internal/resource/common/id" + "github.com/pingidentity/terraform-provider-pingfederate/internal/resource/config" + "github.com/pingidentity/terraform-provider-pingfederate/internal/resource/providererror" + internaltypes "github.com/pingidentity/terraform-provider-pingfederate/internal/types" +) + +var ( + _ resource.Resource = &keypairsSigningCsrExportResource{} + _ resource.ResourceWithConfigure = &keypairsSigningCsrExportResource{} + + customId = "keypair_id" +) + +func KeypairsSigningCsrExportResource() resource.Resource { + return &keypairsSigningCsrExportResource{} +} + +type keypairsSigningCsrExportResource struct { + providerConfig internaltypes.ProviderConfiguration + apiClient *client.APIClient +} + +func (r *keypairsSigningCsrExportResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_keypairs_signing_csr_export" +} + +func (r *keypairsSigningCsrExportResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + providerCfg := req.ProviderData.(internaltypes.ResourceConfiguration) + r.providerConfig = providerCfg.ProviderConfig + r.apiClient = providerCfg.ApiClient +} + +type keypairsSigningCsrExportResourceModel struct { + Id types.String `tfsdk:"id"` + KeypairId types.String `tfsdk:"keypair_id"` + ExportedCsr types.String `tfsdk:"exported_csr"` + ExportTriggerValues types.Map `tfsdk:"export_trigger_values"` +} + +func (r *keypairsSigningCsrExportResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Datasource to generate a new certificate signing request (CSR) for a key pair.", + Attributes: map[string]schema.Attribute{ + "keypair_id": schema.StringAttribute{ + Description: "The ID of the keypair.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "exported_csr": schema.StringAttribute{ + Description: "The exported PEM-encoded certificate signing request.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "export_trigger_values": schema.MapAttribute{ + Description: "A meta-argument map of values that, if any values are changed, will force export of a new CSR. Adding values to and removing values from the map will not trigger an export. This parameter can be used to control time-based exports using Terraform.", + Optional: true, + ElementType: types.StringType, + }, + }, + } + id.ToSchema(&resp.Schema) +} + +// Export a new CSR via RequiresReplace when the trigger values change +func (r *keypairsSigningCsrExportResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + // Destruction plan + if req.Plan.Raw.IsNull() { + return + } + + var plan, state types.Map + var planValues, stateValues map[string]attr.Value + + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("export_trigger_values"), &plan)...) + if resp.Diagnostics.HasError() { + return + } + + planValues = plan.Elements() + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("export_trigger_values"), &state)...) + if resp.Diagnostics.HasError() { + return + } + + stateValues = state.Elements() + + for k, v := range planValues { + if stateValue, ok := stateValues[k]; ok && (v == types.StringUnknown() || !stateValue.Equal(v)) { + resp.RequiresReplace = path.Paths{path.Root("export_trigger_values")} + break + } + } +} + +func (r *keypairsSigningCsrExportResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data keypairsSigningCsrExportResourceModel + + // Read Terraform config data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Read API call logic + exportRequest := r.apiClient.KeyPairsSigningAPI.ExportCsr(config.AuthContext(ctx, r.providerConfig), data.KeypairId.ValueString()) + responseData, httpResp, err := exportRequest.Execute() + if err != nil { + config.ReportHttpErrorCustomId(ctx, &resp.Diagnostics, "An error occurred while generating the certificate signing request.", err, httpResp, &customId) + return + } + + // Set the exported metadata + data.Id = types.StringValue(data.KeypairId.ValueString()) + data.ExportedCsr = types.StringValue(responseData) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *keypairsSigningCsrExportResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // PingFederate provides no read endpoint for this resource, so we'll just maintain whatever is in state + resp.State.Raw = req.State.Raw +} + +func (r *keypairsSigningCsrExportResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // This will only happen when adding or removing export trigger values. + // Just copy the existing state and export_trigger_values into state. + var plan, state keypairsSigningCsrExportResourceModel + + // Read Terraform config data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + state.ExportTriggerValues = plan.ExportTriggerValues + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *keypairsSigningCsrExportResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // There is no way to delete an exported CSR + providererror.WarnConfigurationCannotBeReset("pingfederate_keypairs_signing_csr_export", &resp.Diagnostics) +} diff --git a/internal/resource/config/utils.go b/internal/resource/config/utils.go index 2c8ec97ad..7cdd0f815 100644 --- a/internal/resource/config/utils.go +++ b/internal/resource/config/utils.go @@ -132,13 +132,26 @@ func ReportHttpErrorCustomId(ctx context.Context, diagnostics *diag.Diagnostics, var pfError pingFederateErrorResponse internalError = json.Unmarshal(body, &pfError) if internalError == nil { + if len(pfError.ValidationErrors) == 0 { + var errorDetail strings.Builder + errorDetail.WriteString("Error summary: ") + errorDetail.WriteString(errorSummary) + errorDetail.WriteString("\nMessage: ") + errorDetail.WriteString(pfError.Message) + errorDetail.WriteString("\nHTTP status: ") + errorDetail.WriteString(httpResp.Status) + errorDetail.WriteString("\nResult ID: ") + errorDetail.WriteString(pfError.ResultId) + diagnostics.AddError(providererror.PingFederateAPIError, errorDetail.String()) + } for _, validationError := range pfError.ValidationErrors { var errorDetail strings.Builder errorDetail.WriteString("Error summary: ") errorDetail.WriteString(errorSummary) errorDetail.WriteString("\nMessage: ") errorDetail.WriteString(validationError.Message) - errorDetail.WriteString("\nHTTP status: " + httpResp.Status) + errorDetail.WriteString("\nHTTP status: ") + errorDetail.WriteString(httpResp.Status) if validationError.FieldPath != "" { errorDetail.WriteString("\nPingFederate field path: ") errorDetail.WriteString(validationError.FieldPath) diff --git a/scripts/verifyContent.py b/scripts/verifyContent.py index 752174b07..1ec689d70 100644 --- a/scripts/verifyContent.py +++ b/scripts/verifyContent.py @@ -9,6 +9,7 @@ noImportResources = [ "pingfederate_connection_metadata_export", + "pingfederate_keypairs_signing_csr_export", "pingfederate_keypairs_signing_key", "pingfederate_keypairs_ssl_server_key", "pingfederate_license"