Skip to content

Commit

Permalink
Support management.mode add metadata.fields.required properties i…
Browse files Browse the repository at this point in the history
…n `credential_type`. (#804)

* Added support for management.mode and required properties in credential type.  Minor fixes to behavior.  Code is incomplete due to need for new validator to handle a specific API validation requirement.

* Implemented resource validation to support specialized credential type schema check requirements. Implemented credential type management mode handling to acocunt for issuance rule API behavior when the credential management mode is set to MANAGED.

* Updated data source with new available credential type properties. Test case expansion.  Minor cleanup.

* Fixed a typo in an error message. Added changelog for PR.

---------

Co-authored-by: Patrick Cowland <[email protected]>
  • Loading branch information
mjspi and patrickcping authored May 10, 2024
1 parent 5ab7788 commit 5103598
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 19 deletions.
12 changes: 12 additions & 0 deletions .changelog/804.txt
Original file line number Diff line number Diff line change
@@ -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`.
```

2 changes: 2 additions & 0 deletions docs/data-sources/credential_type.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
2 changes: 2 additions & 0 deletions docs/resources/credential_type.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand All @@ -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()),
Expand All @@ -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
}

Expand Down
29 changes: 27 additions & 2 deletions internal/service/credentials/data_source_credential_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand All @@ -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 (
Expand All @@ -78,6 +80,7 @@ var (
"is_visible": types.BoolType,
"attribute": types.StringType,
"value": types.StringType,
"required": types.BoolType,
}
)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
},
},
},
Expand Down Expand Up @@ -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())
Expand All @@ -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()),
Expand Down Expand Up @@ -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 {
Expand All @@ -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...)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
64 changes: 60 additions & 4 deletions internal/service/credentials/resource_credential_issuance_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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."),
},
},
})
}
Expand Down Expand Up @@ -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 = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 740 480\"><rect fill=\"none\" width=\"736\" height=\"476\" stroke=\"#CACED3\" stroke-width=\"3\" rx=\"10\" ry=\"10\" x=\"2\" y=\"2\"></rect><rect fill=\"$${cardColor}\" height=\"476\" rx=\"10\" ry=\"10\" width=\"736\" x=\"2\" y=\"2\" opacity=\"$${bgOpacityPercent}\"></rect><line y2=\"160\" x2=\"695\" y1=\"160\" x1=\"42.5\" stroke=\"$${textColor}\"></line><text fill=\"$${textColor}\" font-weight=\"450\" font-size=\"30\" x=\"160\" y=\"90\">$${cardTitle}</text><text fill=\"$${textColor}\" font-size=\"25\" font-weight=\"300\" x=\"160\" y=\"130\">$${cardSubtitle}</text></svg>"
metadata = {
Expand Down Expand Up @@ -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 = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 740 480\"><rect fill=\"none\" width=\"736\" height=\"476\" stroke=\"#CACED3\" stroke-width=\"3\" rx=\"10\" ry=\"10\" x=\"2\" y=\"2\"></rect><rect fill=\"$${cardColor}\" height=\"476\" rx=\"10\" ry=\"10\" width=\"736\" x=\"2\" y=\"2\" opacity=\"$${bgOpacityPercent}\"></rect><line y2=\"160\" x2=\"695\" y1=\"160\" x1=\"42.5\" stroke=\"$${textColor}\"></line><text fill=\"$${textColor}\" font-weight=\"450\" font-size=\"30\" x=\"160\" y=\"90\">$${cardTitle}</text><text fill=\"$${textColor}\" font-size=\"25\" font-weight=\"300\" x=\"160\" y=\"130\">$${cardSubtitle}</text></svg>"
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)
}
Loading

0 comments on commit 5103598

Please sign in to comment.