diff --git a/.changelog/804.txt b/.changelog/804.txt new file mode 100644 index 000000000..856748e75 --- /dev/null +++ b/.changelog/804.txt @@ -0,0 +1,12 @@ +```release-note:enhancement +`resource/pingone_credential_type`: Added support for the `management_mode` and `metadata.fields.required` properties. +``` + +```release-note:enhancement +`data_source/pingone_credential_type`: Added support for the `management_mode` and `metadata.fields.required` properties. +``` + +```release-note:note +`resource/pingone_credential_issuance_rule`: A `credential_issuance_rule` cannot be assigned to a `credential_type` that has a `management_mode` of `MANAGED`. +``` + diff --git a/docs/data-sources/credential_type.md b/docs/data-sources/credential_type.md index 1e30eaef5..e0b56feb9 100644 --- a/docs/data-sources/credential_type.md +++ b/docs/data-sources/credential_type.md @@ -34,6 +34,7 @@ data "pingone_credential_type" "example_by_id" { - `description` (String) A description of the credential type. - `id` (String) The ID of this resource. - `issuer_id` (String) Identifier (UUID) of the credential issuer. +- `management_mode` (String) Specifies the management mode of the credential type. - `metadata` (Attributes) An object that contains the names, data types, and other metadata related to the credentia (see [below for nested schema](#nestedatt--metadata)) - `revoke_on_delete` (Boolean) Specifies whether a user's issued verifiable credentials are automatically revoked when the credential type is deleted. - `title` (String) Title of the credential. @@ -64,6 +65,7 @@ Read-Only: - `file_support` (String) Specifies how an image is stored in the credential field. - `id` (String) Identifier of the field object. - `is_visible` (Boolean) Specifies whether the field should be visible to viewers of the credential. +- `required` (Boolean) Specifies whether the field is required for the credential. - `title` (String) Descriptive text when showing the field. - `type` (String) Type of data in the field. - `value` (String) The text to appear on the credential for a field.type of Alphanumeric Text. diff --git a/docs/resources/credential_type.md b/docs/resources/credential_type.md index 84c1ad5ad..ff9627c14 100644 --- a/docs/resources/credential_type.md +++ b/docs/resources/credential_type.md @@ -129,6 +129,7 @@ EOT - `card_type` (String) A descriptor of the credential type. Can be non-identity types such as proof of employment or proof of insurance. - `description` (String) A description of the credential type. This value aligns to `${cardSubtitle}` in the `card_design_template`. +- `management_mode` (String) Specifies the management mode of the credential type. Options are `AUTOMATED`, `MANAGED`. Defaults to `AUTOMATED`. - `revoke_on_delete` (Boolean) A boolean that specifies whether a user's issued verifiable credentials are automatically revoked when a `credential_type`, `user`, or `environment` is deleted. Defaults to `true`. ### Read-Only @@ -172,6 +173,7 @@ Optional: - `attribute` (String) Name of the PingOne Directory attribute. Present if `field.type` is `Directory Attribute`. - `file_support` (String) Specifies how an image is stored in the credential field. Options are `BASE64_STRING`, `INCLUDE_FILE`, `REFERENCE_FILE`. - `is_visible` (Boolean) Specifies whether the field should be visible to viewers of the credential. +- `required` (Boolean) Specifies whether the field is required for the credential. - `title` (String) Descriptive text when showing the field. - `value` (String) The text to appear on the credential for a `field.type` of `Alphanumeric Text`. diff --git a/internal/service/credentials/data_source_credential_issuance_rule.go b/internal/service/credentials/data_source_credential_issuance_rule.go index 7788cfa27..0394c5be2 100644 --- a/internal/service/credentials/data_source_credential_issuance_rule.go +++ b/internal/service/credentials/data_source_credential_issuance_rule.go @@ -297,6 +297,10 @@ func (p *CredentialIssuanceRuleDataSourceModel) toState(apiObject *credentials.C func toStateAutomationDataSource(automation *credentials.CredentialIssuanceRuleAutomation, ok bool) (types.Object, diag.Diagnostics) { var diags diag.Diagnostics + if !ok || automation == nil { + return types.ObjectNull(automationDataSourceServiceTFObjectTypes), diags + } + automationMap := map[string]attr.Value{ "issue": framework.EnumOkToTF(automation.GetIssueOk()), "revoke": framework.EnumOkToTF(automation.GetRevokeOk()), @@ -311,6 +315,10 @@ func toStateAutomationDataSource(automation *credentials.CredentialIssuanceRuleA func toStateFilterDataSource(filter *credentials.CredentialIssuanceRuleFilter, ok bool) (types.Object, diag.Diagnostics) { var diags diag.Diagnostics + if !ok || filter == nil { + return types.ObjectNull(filterDataSourceServiceTFObjectTypes), diags + } + filterMap := map[string]attr.Value{ "population_ids": framework.StringSetOkToTF(filter.GetPopulationIdsOk()), "group_ids": framework.StringSetOkToTF(filter.GetGroupIdsOk()), @@ -325,7 +333,7 @@ func toStateFilterDataSource(filter *credentials.CredentialIssuanceRuleFilter, o func toStateNotificationDataSource(notification *credentials.CredentialIssuanceRuleNotification, ok bool) (types.Object, diag.Diagnostics) { var diags diag.Diagnostics - if notification == nil { + if !ok || notification == nil { return types.ObjectNull(notificationDataSourceServiceTFObjectTypes), diags } diff --git a/internal/service/credentials/data_source_credential_type.go b/internal/service/credentials/data_source_credential_type.go index ea7cc42d8..ce8b399d1 100644 --- a/internal/service/credentials/data_source_credential_type.go +++ b/internal/service/credentials/data_source_credential_type.go @@ -23,12 +23,13 @@ type CredentialTypeDataSourceModel struct { EnvironmentId types.String `tfsdk:"environment_id"` IssuerId types.String `tfsdk:"issuer_id"` CredentialTypeId types.String `tfsdk:"credential_type_id"` - Title types.String `tfsdk:"title"` - Description types.String `tfsdk:"description"` CardType types.String `tfsdk:"card_type"` CardDesignTemplate types.String `tfsdk:"card_design_template"` + Description types.String `tfsdk:"description"` + ManagementMode types.String `tfsdk:"management_mode"` Metadata types.Object `tfsdk:"metadata"` RevokeOnDelete types.Bool `tfsdk:"revoke_on_delete"` + Title types.String `tfsdk:"title"` CreatedAt types.String `tfsdk:"created_at"` UpdatedAt types.String `tfsdk:"updated_at"` } @@ -54,6 +55,7 @@ type FieldsDataSourceModel struct { IsVisible types.Bool `tfsdk:"is_visible"` Attribute types.String `tfsdk:"attribute"` Value types.String `tfsdk:"value"` + Required types.Bool `tfsdk:"required"` } var ( @@ -78,6 +80,7 @@ var ( "is_visible": types.BoolType, "attribute": types.StringType, "value": types.StringType, + "required": types.BoolType, } ) @@ -138,6 +141,11 @@ func (r *CredentialTypeDataSource) Schema(ctx context.Context, req datasource.Sc Computed: true, }, + "management_mode": schema.StringAttribute{ + Description: "Specifies the management mode of the credential type.", + Computed: true, + }, + "revoke_on_delete": schema.BoolAttribute{ Description: "Specifies whether a user's issued verifiable credentials are automatically revoked when the credential type is deleted.", Computed: true, @@ -225,6 +233,10 @@ func (r *CredentialTypeDataSource) Schema(ctx context.Context, req datasource.Sc Description: "The text to appear on the credential for a field.type of Alphanumeric Text.", Computed: true, }, + "required": schema.BoolAttribute{ + Description: "Specifies whether the field is required for the credential.", + Computed: true, + }, }, }, }, @@ -332,6 +344,10 @@ func (p *CredentialTypeDataSourceModel) toState(apiObject *credentials.Credentia p.CreatedAt = framework.TimeOkToTF(apiObject.GetCreatedAtOk()) p.UpdatedAt = framework.TimeOkToTF(apiObject.GetUpdatedAtOk()) + if v, ok := apiObject.GetManagementOk(); ok { + p.ManagementMode = framework.EnumOkToTF(v.GetModeOk()) + } + revokeOnDelete := types.BoolNull() if v, ok := apiObject.GetOnDeleteOk(); ok { revokeOnDelete = framework.BoolOkToTF(v.GetRevokeIssuedCredentialsOk()) @@ -349,6 +365,10 @@ func (p *CredentialTypeDataSourceModel) toState(apiObject *credentials.Credentia func toStateMetadataDataSource(metadata *credentials.CredentialTypeMetaData, ok bool) (types.Object, diag.Diagnostics) { var diags diag.Diagnostics + if !ok || metadata == nil { + return types.ObjectNull(metadataDataSourceServiceTFObjectTypes), diags + } + // core metadata object metadataMap := map[string]attr.Value{ "background_image": framework.StringOkToTF(metadata.GetBackgroundImageOk()), @@ -376,6 +396,10 @@ func toStateMetadataDataSource(metadata *credentials.CredentialTypeMetaData, ok func toStateFieldsDataSource(innerFields []credentials.CredentialTypeMetaDataFieldsInner, ok bool) (types.List, diag.Diagnostics) { var diags diag.Diagnostics + if !ok || innerFields == nil { + return types.ListNull(types.ObjectType{AttrTypes: innerFieldsDataSourceServiceTFObjectTypes}), diags + } + tfInnerObjType := types.ObjectType{AttrTypes: innerFieldsDataSourceServiceTFObjectTypes} innerflattenedList := []attr.Value{} for _, v := range innerFields { @@ -388,6 +412,7 @@ func toStateFieldsDataSource(innerFields []credentials.CredentialTypeMetaDataFie "is_visible": framework.BoolOkToTF(v.GetIsVisibleOk()), "attribute": framework.StringOkToTF(v.GetAttributeOk()), "value": framework.StringOkToTF(v.GetValueOk()), + "required": framework.BoolOkToTF(v.GetRequiredOk()), } innerflattenedObj, d := types.ObjectValue(innerFieldsDataSourceServiceTFObjectTypes, fieldsMap) diags.Append(d...) diff --git a/internal/service/credentials/data_source_credential_type_test.go b/internal/service/credentials/data_source_credential_type_test.go index aa5d57a26..0b0468df2 100644 --- a/internal/service/credentials/data_source_credential_type_test.go +++ b/internal/service/credentials/data_source_credential_type_test.go @@ -40,6 +40,7 @@ func TestAccCredentialTypeDataSource_ByIDFull(t *testing.T) { resource.TestCheckResourceAttr(dataSourceFullName, "description", fmt.Sprintf("%s Example Description", name)), resource.TestCheckResourceAttr(dataSourceFullName, "card_type", name), resource.TestCheckResourceAttrPair(dataSourceFullName, "card_design_template", resourceFullName, "card_design_template"), + resource.TestCheckResourceAttr(dataSourceFullName, "management_mode", "AUTOMATED"), resource.TestCheckResourceAttrPair(dataSourceFullName, "metadata.%", resourceFullName, "metadata.%"), resource.TestCheckResourceAttrPair(dataSourceFullName, "metadata.fields.%", resourceFullName, "metadata.fields.%"), resource.TestCheckResourceAttr(dataSourceFullName, "revoke_on_delete", "false"), diff --git a/internal/service/credentials/resource_credential_issuance_rule.go b/internal/service/credentials/resource_credential_issuance_rule.go index c5dce962b..806d41934 100644 --- a/internal/service/credentials/resource_credential_issuance_rule.go +++ b/internal/service/credentials/resource_credential_issuance_rule.go @@ -338,7 +338,7 @@ func (r *CredentialIssuanceRuleResource) Create(ctx context.Context, req resourc } // Build the model for the API - CredentialIssuanceRule, d := plan.expand(ctx) + CredentialIssuanceRule, d := plan.expand(ctx, r) resp.Diagnostics.Append(d...) if resp.Diagnostics.HasError() { return @@ -432,7 +432,7 @@ func (r *CredentialIssuanceRuleResource) Update(ctx context.Context, req resourc } // Build the model for the API - CredentialIssuanceRule, d := plan.expand(ctx) + CredentialIssuanceRule, d := plan.expand(ctx, r) resp.Diagnostics.Append(d...) if resp.Diagnostics.HasError() { return @@ -537,8 +537,13 @@ func (r *CredentialIssuanceRuleResource) ImportState(ctx context.Context, req re } } -func (p *CredentialIssuanceRuleResourceModel) expand(ctx context.Context) (*credentials.CredentialIssuanceRule, diag.Diagnostics) { - var diags diag.Diagnostics +func (p *CredentialIssuanceRuleResourceModel) expand(ctx context.Context, r *CredentialIssuanceRuleResource) (*credentials.CredentialIssuanceRule, diag.Diagnostics) { + // The P1 Credentials service automatically sets the Issuance Rule to disabled in the backend if the Credential Type associated with it has a management.mode of `MANAGED`. + // Perform check to prevent an out of plan / drift condition. + diags := checkCredentialTypeManagementMode(ctx, r, p.EnvironmentId.ValueString(), p.CredentialTypeId.ValueString()) + if diags.HasError() { + return nil, diags + } // expand automation rules credentialIssuanceRuleAutomation := credentials.NewCredentialIssuanceRuleAutomationWithDefaults() @@ -873,3 +878,54 @@ func enumCredentialIssuanceRuleNotificationMethodOkToTF(v []credentials.EnumCred return types.SetValueMust(types.StringType, list) } } + +func checkCredentialTypeManagementMode(ctx context.Context, r *CredentialIssuanceRuleResource, environmentId, credentialTypeId string) diag.Diagnostics { + var diags diag.Diagnostics + + // Run the API call + var respObject *credentials.CredentialType + diags.Append(framework.ParseResponse( + ctx, + + func() (any, *http.Response, error) { + fO, fR, fErr := r.Client.CredentialsAPIClient.CredentialTypesApi.ReadOneCredentialType(ctx, environmentId, credentialTypeId).Execute() + return framework.CheckEnvironmentExistsOnPermissionsError(ctx, r.Client.ManagementAPIClient, environmentId, fO, fR, fErr) + }, + "ReadOneCredentialType", + framework.DefaultCustomError, + sdk.DefaultCreateReadRetryable, + &respObject, + )...) + if diags.HasError() { + return diags + } + + if respObject == nil { + diags.AddError( + "Credential Type Id Invalid or Missing", + "Creantial Type referenced in `credential_type.id` does not exist", + ) + return diags + } + + if v, ok := respObject.GetManagementOk(); ok { + managementMode, managementModeOk := v.GetModeOk() + if !managementModeOk { + diags.AddError( + "Credential Type referenced in `credential_type.id` does not have a management mode defined.", + fmt.Sprintf("Credential Type Id %s does not contain a `management.mode` value, or the value could not be found. Please report this to the provider maintainers.", credentialTypeId), + ) + return diags + } + + if *managementMode == credentials.ENUMCREDENTIALTYPEMANAGEMENTMODE_MANAGED { + diags.AddError( + fmt.Sprintf("A Credential Issuance Rule cannot be assigned to a Credential Type that has a management mode of %s.", string(credentials.ENUMCREDENTIALTYPEMANAGEMENTMODE_MANAGED)), + fmt.Sprintf("The Credential Type Id %s associated with the configured Issuance Rule is set to a `management.mode` of %s. The Issuance Rule must be removed, or the Credential Type updated.", credentialTypeId, string(credentials.ENUMCREDENTIALTYPEMANAGEMENTMODE_MANAGED)), + ) + return diags + } + } + + return diags +} diff --git a/internal/service/credentials/resource_credential_issuance_rule_test.go b/internal/service/credentials/resource_credential_issuance_rule_test.go index 3cf1fa4af..971ce1130 100644 --- a/internal/service/credentials/resource_credential_issuance_rule_test.go +++ b/internal/service/credentials/resource_credential_issuance_rule_test.go @@ -265,6 +265,10 @@ func TestAccCredentialIssuanceRule_InvalidConfigs(t *testing.T) { ExpectError: regexp.MustCompile("Error: Incorrect attribute value type"), Destroy: true, }, + { + Config: testAccCredentialIssuanceRuleConfig_CredentialTypeIsManaged(resourceName, name), + ExpectError: regexp.MustCompile("Error: A Credential Issuance Rule cannot be assigned to a Credential Type that has a management mode of MANAGED."), + }, }, }) } @@ -538,10 +542,11 @@ func testAccCredentialIssuanceRuleConfig_Disabled(resourceName, name string) str %[1]s resource "pingone_credential_type" "%[2]s" { - environment_id = data.pingone_environment.general_test.id - title = "%[3]s" - description = "%[3]s Example Description" - card_type = "%[3]s" + environment_id = data.pingone_environment.general_test.id + title = "%[3]s" + description = "%[3]s Example Description" + card_type = "%[3]s" + card_design_template = "$${cardTitle}$${cardSubtitle}" metadata = { @@ -1219,3 +1224,86 @@ resource "pingone_credential_issuance_rule" "%[2]s" { } }`, acctest.GenericSandboxEnvironment(), resourceName, name) } + +func testAccCredentialIssuanceRuleConfig_CredentialTypeIsManaged(resourceName, name string) string { + return fmt.Sprintf(` + %[1]s + +resource "pingone_population" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + name = "%[3]s" +} + +resource "pingone_credential_type" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + title = "%[3]s" + description = "%[3]s Example Description" + card_type = "%[3]s" + card_design_template = "$${cardTitle}$${cardSubtitle}" + + management_mode = "MANAGED" + + metadata = { + name = "%[3]s" + description = "%[3]s Example Description" + bg_opacity_percent = 100 + card_color = "#000000" + text_color = "#eff0f1" + + fields = [ + { + type = "Alphanumeric Text" + title = "Example Field" + value = "Demo" + is_visible = false + }, + ] + } +} + +resource "pingone_application" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + name = "%[3]s" + enabled = true + + oidc_options { + type = "NATIVE_APP" + grant_types = ["CLIENT_CREDENTIALS"] + token_endpoint_authn_method = "CLIENT_SECRET_BASIC" + + mobile_app { + bundle_id = "com.pingidentity.ios_%[3]s" + package_name = "com.pingidentity.android_%[3]s" + passcode_refresh_seconds = 30 + } + } +} + +resource "pingone_digital_wallet_application" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + application_id = resource.pingone_application.%[2]s.id + name = "%[3]s" + app_open_url = "https://www.example.com" + + depends_on = [resource.pingone_application.%[2]s] +} + +resource "pingone_credential_issuance_rule" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + credential_type_id = resource.pingone_credential_type.%[2]s.id + digital_wallet_application_id = resource.pingone_digital_wallet_application.%[2]s.id + status = "ACTIVE" + + filter = { + scim = "address.countryCode eq \"NG\"" + } + + automation = { + issue = "PERIODIC" + revoke = "PERIODIC" + update = "PERIODIC" + } + + +}`, acctest.GenericSandboxEnvironment(), resourceName, name) +} diff --git a/internal/service/credentials/resource_credential_type.go b/internal/service/credentials/resource_credential_type.go index 9538e530a..b5dd17893 100644 --- a/internal/service/credentials/resource_credential_type.go +++ b/internal/service/credentials/resource_credential_type.go @@ -41,6 +41,7 @@ type CredentialTypeResourceModel struct { CardType types.String `tfsdk:"card_type"` CardDesignTemplate types.String `tfsdk:"card_design_template"` Description types.String `tfsdk:"description"` + ManagementMode types.String `tfsdk:"management_mode"` Metadata types.Object `tfsdk:"metadata"` RevokeOnDelete types.Bool `tfsdk:"revoke_on_delete"` Title types.String `tfsdk:"title"` @@ -69,6 +70,7 @@ type FieldsModel struct { IsVisible types.Bool `tfsdk:"is_visible"` Attribute types.String `tfsdk:"attribute"` Value types.String `tfsdk:"value"` + Required types.Bool `tfsdk:"required"` } var ( @@ -93,14 +95,16 @@ var ( "is_visible": types.BoolType, "attribute": types.StringType, "value": types.StringType, + "required": types.BoolType, } ) // Framework interfaces var ( - _ resource.Resource = &CredentialTypeResource{} - _ resource.ResourceWithConfigure = &CredentialTypeResource{} - _ resource.ResourceWithImportState = &CredentialTypeResource{} + _ resource.Resource = &CredentialTypeResource{} + _ resource.ResourceWithConfigure = &CredentialTypeResource{} + _ resource.ResourceWithValidateConfig = &CredentialTypeResource{} + _ resource.ResourceWithImportState = &CredentialTypeResource{} ) // New Object @@ -134,6 +138,10 @@ func (r *CredentialTypeResource) Schema(ctx context.Context, req resource.Schema "The identifier (UUID) of the issuer of the credential, which is the `id` of the `credential_issuer_profile` defined in the `environment`.", ) + managementModeDescription := framework.SchemaAttributeDescriptionFromMarkdown( + "Specifies the management mode of the credential type.", + ).AllowedValuesEnum(credentials.AllowedEnumCredentialTypeManagementModeEnumValues).DefaultValue(string(credentials.ENUMCREDENTIALTYPEMANAGEMENTMODE_AUTOMATED)) + revokeOnDeleteDescription := framework.SchemaAttributeDescriptionFromMarkdown( "A boolean that specifies whether a user's issued verifiable credentials are automatically revoked when a `credential_type`, `user`, or `environment` is deleted.", ).DefaultValue("true") @@ -234,6 +242,16 @@ func (r *CredentialTypeResource) Schema(ctx context.Context, req resource.Schema }, }, + "management_mode": schema.StringAttribute{ + Description: managementModeDescription.Description, + MarkdownDescription: managementModeDescription.MarkdownDescription, + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf(utils.EnumSliceToStringSlice(credentials.AllowedEnumCredentialTypeManagementModeEnumValues)...), + }, + }, + "revoke_on_delete": schema.BoolAttribute{ Description: revokeOnDeleteDescription.Description, MarkdownDescription: revokeOnDeleteDescription.MarkdownDescription, @@ -426,9 +444,13 @@ func (r *CredentialTypeResource) Schema(ctx context.Context, req resource.Schema Validators: []validator.String{ stringvalidator.LengthAtLeast(attrMinLength), stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("attribute")), - customstringvalidator.IsRequiredIfMatchesPathValue(basetypes.NewStringValue(string(credentials.ENUMCREDENTIALTYPEMETADATAFIELDSTYPE_ALPHANUMERIC_TEXT)), path.MatchRelative().AtParent().AtName("type")), }, }, + "required": schema.BoolAttribute{ + Description: "Specifies whether the field is required for the credential.", + Optional: true, + Computed: true, + }, }, }, }, @@ -452,6 +474,43 @@ func (r *CredentialTypeResource) Schema(ctx context.Context, req resource.Schema } } +func (r *CredentialTypeResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data CredentialTypeResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + var metaData MetadataModel + resp.Diagnostics.Append(data.Metadata.As(ctx, &metaData, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: false, + UnhandledUnknownAsEmpty: false, + })...) + if resp.Diagnostics.HasError() { + return + } + + var metadataFields []FieldsModel + resp.Diagnostics.Append(metaData.Fields.ElementsAs(ctx, &metadataFields, false)...) + if resp.Diagnostics.HasError() { + return + } + + if data.ManagementMode != types.StringValue(string(credentials.ENUMCREDENTIALTYPEMANAGEMENTMODE_MANAGED)) { + for _, v := range metadataFields { + if v.Type == types.StringValue(string(credentials.ENUMCREDENTIALTYPEMETADATAFIELDSTYPE_ALPHANUMERIC_TEXT)) && (v.Value.IsNull() || v.Value.IsUnknown()) { + + resp.Diagnostics.AddAttributeError( + path.Root("metadata"), + "Invalid credential type configuration", + fmt.Sprintf("The configuration for `%s` is invalid. The `fields.value` property is required when the `fields.type` property is `%s` and the credential `management_mode` property is undefined or `%s`.", v.Title.ValueString(), v.Type.ValueString(), string(credentials.ENUMCREDENTIALTYPEMANAGEMENTMODE_AUTOMATED)), + ) + } + } + } +} + func (r *CredentialTypeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { @@ -726,6 +785,13 @@ func (p *CredentialTypeResourceModel) expand(ctx context.Context) (*credentials. data.SetCardType(p.CardType.ValueString()) } + if !p.ManagementMode.IsNull() && !p.ManagementMode.IsUnknown() { + managementModeObject := credentials.NewCredentialTypeManagement() + managementModeObject.SetMode(credentials.EnumCredentialTypeManagementMode(p.ManagementMode.ValueString())) + + data.SetManagement(*managementModeObject) + } + if !p.RevokeOnDelete.IsNull() && !p.RevokeOnDelete.IsUnknown() { onDeleteObject := credentials.NewCredentialTypeOnDelete() onDeleteObject.SetRevokeIssuedCredentials(p.RevokeOnDelete.ValueBool()) @@ -812,11 +878,11 @@ func (p *FieldsModel) expandFields() (*credentials.CredentialTypeMetaDataFieldsI attrId := p.Type.ValueString() + " -> " + p.Title.ValueString() // construct id per P1Creds API recommendations innerFields.SetId(attrId) - if attrType == credentials.ENUMCREDENTIALTYPEMETADATAFIELDSTYPE_ALPHANUMERIC_TEXT { + if attrType == credentials.ENUMCREDENTIALTYPEMETADATAFIELDSTYPE_ALPHANUMERIC_TEXT && !p.Value.IsNull() && !p.Value.IsUnknown() { innerFields.SetValue(p.Value.ValueString()) } - if attrType == credentials.ENUMCREDENTIALTYPEMETADATAFIELDSTYPE_DIRECTORY_ATTRIBUTE { + if attrType == credentials.ENUMCREDENTIALTYPEMETADATAFIELDSTYPE_DIRECTORY_ATTRIBUTE && !p.Attribute.IsNull() && !p.Attribute.IsUnknown() { innerFields.SetAttribute(p.Attribute.ValueString()) if !p.FileSupport.IsNull() && !p.FileSupport.IsUnknown() { @@ -833,6 +899,10 @@ func (p *FieldsModel) expandFields() (*credentials.CredentialTypeMetaDataFieldsI innerFields.SetIsVisible(p.IsVisible.ValueBool()) } + if !p.Required.IsNull() && !p.Required.IsUnknown() { + innerFields.SetRequired(p.Required.ValueBool()) + } + if innerFields == nil { diags.AddWarning( "Unexpected Value", @@ -865,6 +935,10 @@ func (p *CredentialTypeResourceModel) toState(apiObject *credentials.CredentialT p.CreatedAt = framework.TimeOkToTF(apiObject.GetCreatedAtOk()) p.UpdatedAt = framework.TimeOkToTF(apiObject.GetUpdatedAtOk()) + if v, ok := apiObject.GetManagementOk(); ok { + p.ManagementMode = framework.EnumOkToTF(v.GetModeOk()) + } + revokeOnDelete := types.BoolNull() if v, ok := apiObject.GetOnDeleteOk(); ok { revokeOnDelete = framework.BoolOkToTF(v.GetRevokeIssuedCredentialsOk()) @@ -882,6 +956,10 @@ func (p *CredentialTypeResourceModel) toState(apiObject *credentials.CredentialT func toStateMetadata(metadata *credentials.CredentialTypeMetaData, ok bool) (types.Object, diag.Diagnostics) { var diags diag.Diagnostics + if !ok || metadata == nil { + return types.ObjectNull(metadataServiceTFObjectTypes), diags + } + // core metadata object metadataMap := map[string]attr.Value{ "background_image": framework.StringOkToTF(metadata.GetBackgroundImageOk()), @@ -909,6 +987,10 @@ func toStateMetadata(metadata *credentials.CredentialTypeMetaData, ok bool) (typ func toStateFields(innerFields []credentials.CredentialTypeMetaDataFieldsInner, ok bool) (types.List, diag.Diagnostics) { var diags diag.Diagnostics + if !ok || innerFields == nil { + return types.ListNull(types.ObjectType{AttrTypes: innerFieldsServiceTFObjectTypes}), diags + } + tfInnerObjType := types.ObjectType{AttrTypes: innerFieldsServiceTFObjectTypes} innerflattenedList := []attr.Value{} for _, v := range innerFields { @@ -921,6 +1003,7 @@ func toStateFields(innerFields []credentials.CredentialTypeMetaDataFieldsInner, "is_visible": framework.BoolOkToTF(v.GetIsVisibleOk()), "attribute": framework.StringOkToTF(v.GetAttributeOk()), "value": framework.StringOkToTF(v.GetValueOk()), + "required": framework.BoolOkToTF(v.GetRequiredOk()), } innerflattenedObj, d := types.ObjectValue(innerFieldsServiceTFObjectTypes, fieldsMap) diags.Append(d...) diff --git a/internal/service/credentials/resource_credential_type_test.go b/internal/service/credentials/resource_credential_type_test.go index 2ed918b3f..6f038c838 100644 --- a/internal/service/credentials/resource_credential_type_test.go +++ b/internal/service/credentials/resource_credential_type_test.go @@ -141,6 +141,7 @@ func TestAccCredentialType_Full(t *testing.T) { resource.TestCheckResourceAttr(resourceFullName, "description", fmt.Sprintf("%s Example Description", name)), resource.TestCheckResourceAttr(resourceFullName, "card_type", "VerifiedEmployee"), resource.TestCheckResourceAttr(resourceFullName, "card_design_template", cardDesignTemplate), + resource.TestCheckResourceAttr(resourceFullName, "management_mode", "AUTOMATED"), resource.TestCheckResourceAttr(resourceFullName, "metadata.name", name), resource.TestCheckResourceAttr(resourceFullName, "metadata.description", fmt.Sprintf("%s Example Description", name)), resource.TestCheckResourceAttr(resourceFullName, "metadata.version", "5"), // ensures calculated default is 5 @@ -156,6 +157,13 @@ func TestAccCredentialType_Full(t *testing.T) { resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.3.title", "displayName"), resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.3.attribute", "name.formatted"), resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.3.is_visible", "false"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.3.required", "false"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.6.id", "Directory Attribute -> id"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.6.type", "Directory Attribute"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.6.title", "id"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.6.attribute", "id"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.6.is_visible", "false"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.6.required", "true"), resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.7.file_support", "REFERENCE_FILE"), resource.TestCheckResourceAttr(resourceFullName, "revoke_on_delete", "true"), resource.TestMatchResourceAttr(resourceFullName, "created_at", verify.RFC3339Regexp), @@ -183,6 +191,7 @@ func TestAccCredentialType_Full(t *testing.T) { resource.TestCheckResourceAttr(resourceFullName, "description", fmt.Sprintf("%s Example Description", updatedName)), resource.TestCheckResourceAttr(resourceFullName, "card_type", "DemonstrationCard"), resource.TestCheckResourceAttr(resourceFullName, "card_design_template", updatedCardDesignTemplate), + resource.TestCheckResourceAttr(resourceFullName, "management_mode", "AUTOMATED"), resource.TestCheckResourceAttr(resourceFullName, "metadata.name", updatedName), resource.TestCheckResourceAttr(resourceFullName, "metadata.version", "5"), // ensures calculated default is 5 resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.#", "1"), @@ -200,6 +209,44 @@ func TestAccCredentialType_Full(t *testing.T) { ), } + updateManagementModeStep := resource.TestStep{ + Config: testAccCredentialTypeConfig_ManagedCredential(resourceName, updatedName), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr(resourceFullName, "id", verify.P1ResourceIDRegexpFullString), + resource.TestMatchResourceAttr(resourceFullName, "environment_id", verify.P1ResourceIDRegexpFullString), + resource.TestMatchResourceAttr(resourceFullName, "issuer_id", verify.P1ResourceIDRegexpFullString), + resource.TestCheckResourceAttr(resourceFullName, "title", updatedName), + resource.TestCheckResourceAttr(resourceFullName, "description", fmt.Sprintf("%s Example Description", updatedName)), + resource.TestCheckResourceAttr(resourceFullName, "card_type", "DemonstrationCard"), + resource.TestCheckResourceAttr(resourceFullName, "card_design_template", updatedCardDesignTemplate), + resource.TestCheckResourceAttr(resourceFullName, "management_mode", "MANAGED"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.name", updatedName), + resource.TestCheckResourceAttr(resourceFullName, "metadata.version", "5"), // ensures calculated default is 5 + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.#", "3"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.0.id", "Issued Timestamp -> timestamp"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.0.type", "Issued Timestamp"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.0.title", "timestamp"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.0.is_visible", "false"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.1.id", "Alphanumeric Text -> selfie"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.1.type", "Alphanumeric Text"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.1.title", "selfie"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.1.is_visible", "false"), + resource.TestCheckNoResourceAttr(resourceFullName, "metadata.fields.1.value"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.2.id", "Alphanumeric Text -> other"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.2.type", "Alphanumeric Text"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.2.title", "other"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.2.is_visible", "false"), + resource.TestCheckResourceAttr(resourceFullName, "metadata.fields.2.value", "sample"), + resource.TestCheckNoResourceAttr(resourceFullName, "metadata.columns"), + resource.TestCheckNoResourceAttr(resourceFullName, "metadata.bg_opacity_percent"), + resource.TestCheckNoResourceAttr(resourceFullName, "metadata.card_color"), + resource.TestCheckNoResourceAttr(resourceFullName, "metadata.text_color"), + resource.TestCheckResourceAttr(resourceFullName, "revoke_on_delete", "false"), + resource.TestMatchResourceAttr(resourceFullName, "created_at", verify.RFC3339Regexp), + resource.TestMatchResourceAttr(resourceFullName, "updated_at", verify.RFC3339Regexp), + ), + } + resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheckClient(t) @@ -223,7 +270,7 @@ func TestAccCredentialType_Full(t *testing.T) { }, // update fullStep, - minimalStep, + updateManagementModeStep, fullStep, // Test importing the resource { @@ -242,6 +289,10 @@ func TestAccCredentialType_Full(t *testing.T) { ImportStateVerify: true, }, // clear + { + Config: testAccCredentialTypeConfig_ManagedCredential(resourceName, updatedName), + Destroy: true, + }, { Config: testAccCredentialTypeConfig_Minimal(resourceName, updatedName), Destroy: true, @@ -291,7 +342,7 @@ func TestAccCredentialType_MetaData(t *testing.T) { }, { Config: testAccCredentialTypeConfig_EmptyFieldsArray(resourceName, name), - ExpectError: regexp.MustCompile("ttribute metadata.fields list must contain at least 1 elements"), + ExpectError: regexp.MustCompile("Attribute metadata.fields list must contain at least 1 elements"), Destroy: true, }, { @@ -299,6 +350,11 @@ func TestAccCredentialType_MetaData(t *testing.T) { ExpectError: regexp.MustCompile("Error: Invalid Attribute Value Match"), Destroy: true, }, + { + Config: testAccCredentialTypeConfig_InvalidManagementModeValueCombination(resourceName, name), + ExpectError: regexp.MustCompile("Error: Invalid credential type configuration"), + Destroy: true, + }, }, }) } @@ -526,6 +582,7 @@ EOT title = "id" attribute = "id" is_visible = false + required = true }, { type = "Directory Attribute" @@ -798,6 +855,41 @@ EOT }`, acctest.GenericSandboxEnvironment(), resourceName, name) } +func testAccCredentialTypeConfig_InvalidManagementModeValueCombination(resourceName, name string) string { + return fmt.Sprintf(` + %[1]s + +resource "pingone_credential_type" "%[3]s" { + environment_id = data.pingone_environment.general_test.id + title = "%[3]s" + description = "%[3]s Example Description" + card_type = "DemonstrationCard" + revoke_on_delete = true + + card_design_template = <<-EOT + + + + +$${cardTitle} +$${cardSubtitle} + +EOT + + metadata = { + name = "%[3]s" + + fields = [ + { + type = "Alphanumeric Text" + title = "selfie" + is_visible = false + } + ] + } +}`, acctest.GenericSandboxEnvironment(), resourceName, name) +} + func testAccCredentialTypeConfig_CardDesignTemplate_NoSVG(resourceName, name string) string { return fmt.Sprintf(` %[1]s @@ -1032,3 +1124,50 @@ EOT } }`, acctest.GenericSandboxEnvironment(), resourceName, name) } + +func testAccCredentialTypeConfig_ManagedCredential(resourceName, name string) string { + return fmt.Sprintf(` + %[1]s + +resource "pingone_credential_type" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + title = "%[3]s" + description = "%[3]s Example Description" + card_type = "DemonstrationCard" + management_mode = "MANAGED" + revoke_on_delete = false + + card_design_template = <<-EOT + + + + +$${cardTitle} +$${cardSubtitle} + +EOT + + metadata = { + name = "%[3]s" + + fields = [ + { + type = "Issued Timestamp" + title = "timestamp" + is_visible = false + }, + { + type = "Alphanumeric Text" + title = "selfie" + is_visible = false + }, + { + type = "Alphanumeric Text" + title = "other" + is_visible = false + value = "sample" + } + ] + } +}`, acctest.GenericSandboxEnvironment(), resourceName, name) +}