From 5a1b159f7ace13c3cc0e68f38f5502fc85cb5b69 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Mon, 13 Jan 2025 13:16:37 -0800 Subject: [PATCH 01/13] Updated provider_pluginframework.go to include new resources for SaaS app and Okta Universal Directory checkout settings. Enhance HTTP mock functionality in Oktapam provider - Added SetupDefaultMockResponders function to configure mock responses for groups, resource groups, and projects. - Implemented UUID generation for consistent mock data. - Registered HTTP methods (POST, GET, DELETE) with regex patterns for various endpoints to simulate API behavior. --- ...a_universal_directory_checkout_settings.md | 51 ++++ docs/resources/saas_app_checkout_settings.md | 51 ++++ docs/resources/security_policy_v2.md | 209 +++++++++++++ .../service_account_checkout_settings.go | 167 ++++++++++ oktapam/provider_httpmock.go | 129 ++++++++ oktapam/provider_pluginframework.go | 2 + ...a_universal_directory_checkout_settings.go | 213 +++++++++++++ ...versal_directory_checkout_settings_test.go | 288 ++++++++++++++++++ .../resource_saas_app_checkout_settings.go | 213 +++++++++++++ ...esource_saas_app_checkout_settings_test.go | 280 +++++++++++++++++ 10 files changed, 1603 insertions(+) create mode 100644 docs/resources/okta_universal_directory_checkout_settings.md create mode 100644 docs/resources/saas_app_checkout_settings.md create mode 100644 docs/resources/security_policy_v2.md create mode 100644 oktapam/convert/service_account_checkout_settings.go create mode 100644 oktapam/resource_okta_universal_directory_checkout_settings.go create mode 100644 oktapam/resource_okta_universal_directory_checkout_settings_test.go create mode 100644 oktapam/resource_saas_app_checkout_settings.go create mode 100644 oktapam/resource_saas_app_checkout_settings_test.go diff --git a/docs/resources/okta_universal_directory_checkout_settings.md b/docs/resources/okta_universal_directory_checkout_settings.md new file mode 100644 index 0000000000..2f7638195a --- /dev/null +++ b/docs/resources/okta_universal_directory_checkout_settings.md @@ -0,0 +1,51 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "oktapam_okta_universal_directory_checkout_settings Resource - terraform-provider-oktapam" +subcategory: "" +description: |- + Manages checkout settings for Okta Universal Directory resources in a project +--- + +# oktapam_okta_universal_directory_checkout_settings (Resource) + +Manages checkout settings for Okta Universal Directory resources in a project + + + + +## Schema + +### Required + +- `checkout_duration_in_seconds` (Number) The duration in seconds for the checkout. If the checkout is enabled, the duration is the maximum time a user can access the resource before the checkout expires. +- `checkout_required` (Boolean) Indicates whether a checkout is mandatory for accessing resources within the project. If `true`, checkout is enforced for all applicable resources by default. If `false`, checkout is not required, and resources are accessible without it. +- `project` (String) The UUID of a Project. +- `resource_group` (String) The UUID of a OktaPA Resource Group. + +### Optional + +- `exclude_list` (List of Object) If provided, only the account identifiers listed are excluded from the checkout requirement. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. (see [below for nested schema](#nestedatt--exclude_list)) +- `include_list` (List of Object) If provided, only the account identifiers listed are required to perform a checkout to access the resource. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. (see [below for nested schema](#nestedatt--include_list)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `exclude_list` + +Optional: + +- `id` (String) +- `saas_app_instance_name` (String) +- `service_account_user_name` (String) + + + +### Nested Schema for `include_list` + +Optional: + +- `id` (String) +- `saas_app_instance_name` (String) +- `service_account_user_name` (String) diff --git a/docs/resources/saas_app_checkout_settings.md b/docs/resources/saas_app_checkout_settings.md new file mode 100644 index 0000000000..7ad291ea7f --- /dev/null +++ b/docs/resources/saas_app_checkout_settings.md @@ -0,0 +1,51 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "oktapam_saas_app_checkout_settings Resource - terraform-provider-oktapam" +subcategory: "" +description: |- + Manages checkout settings for SaaS Application resources in a project +--- + +# oktapam_saas_app_checkout_settings (Resource) + +Manages checkout settings for SaaS Application resources in a project + + + + +## Schema + +### Required + +- `checkout_duration_in_seconds` (Number) The duration in seconds for the checkout. If the checkout is enabled, the duration is the maximum time a user can access the resource before the checkout expires. +- `checkout_required` (Boolean) Indicates whether a checkout is mandatory for accessing resources within the project. If `true`, checkout is enforced for all applicable resources by default. If `false`, checkout is not required, and resources are accessible without it. +- `project` (String) The UUID of a Project. +- `resource_group` (String) The UUID of a OktaPA Resource Group. + +### Optional + +- `exclude_list` (List of Object) If provided, only the account identifiers listed are excluded from the checkout requirement. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. (see [below for nested schema](#nestedatt--exclude_list)) +- `include_list` (List of Object) If provided, only the account identifiers listed are required to perform a checkout to access the resource. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. (see [below for nested schema](#nestedatt--include_list)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `exclude_list` + +Optional: + +- `id` (String) +- `saas_app_instance_name` (String) +- `service_account_user_name` (String) + + + +### Nested Schema for `include_list` + +Optional: + +- `id` (String) +- `saas_app_instance_name` (String) +- `service_account_user_name` (String) diff --git a/docs/resources/security_policy_v2.md b/docs/resources/security_policy_v2.md new file mode 100644 index 0000000000..fd18963dde --- /dev/null +++ b/docs/resources/security_policy_v2.md @@ -0,0 +1,209 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "oktapam_security_policy_v2 Resource - terraform-provider-oktapam" +subcategory: "" +description: |- + A policy which defines how users can gain access to resources. For details, see Security policy https://help.okta.com/okta_help.htm?type=oie&id=ext-pam-policy. +--- + +# oktapam_security_policy_v2 (Resource) + +A policy which defines how users can gain access to resources. For details, see [Security policy](https://help.okta.com/okta_help.htm?type=oie&id=ext-pam-policy). + + + + +## Schema + +### Required + +- `active` (Boolean) +- `name` (String) +- `principals` (Attributes) (see [below for nested schema](#nestedatt--principals)) +- `rules` (Attributes List) (see [below for nested schema](#nestedatt--rules)) + +### Optional + +- `description` (String) +- `type` (String) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `principals` + +Optional: + +- `user_groups` (List of String) + + + +### Nested Schema for `rules` + +Required: + +- `name` (String) +- `privileges` (Attributes List) (see [below for nested schema](#nestedatt--rules--privileges)) +- `resource_selector` (Attributes) (see [below for nested schema](#nestedatt--rules--resource_selector)) +- `resource_type` (String) + +Optional: + +- `conditions` (Attributes List) (see [below for nested schema](#nestedatt--rules--conditions)) +- `override_checkout_duration_in_seconds` (Number) + + +### Nested Schema for `rules.privileges` + +Optional: + +- `password_checkout_database` (Attributes) (see [below for nested schema](#nestedatt--rules--privileges--password_checkout_database)) +- `password_checkout_ssh` (Attributes) (see [below for nested schema](#nestedatt--rules--privileges--password_checkout_ssh)) +- `principal_account_ssh` (Attributes) (see [below for nested schema](#nestedatt--rules--privileges--principal_account_ssh)) + + +### Nested Schema for `rules.privileges.password_checkout_database` + +Required: + +- `password_checkout_database` (Boolean) + + + +### Nested Schema for `rules.privileges.password_checkout_ssh` + +Required: + +- `password_checkout_ssh` (Boolean) + + + +### Nested Schema for `rules.privileges.principal_account_ssh` + +Required: + +- `principal_account_ssh` (Boolean) Defines the privilege to make SSH connections to a server with the user's principal account. + +Optional: + +- `admin_level_permissions` (Boolean) Provides coarse grain (full admin) access to the user. +- `sudo_command_bundles` (List of String) UUIDs of the existing sudo command bundles. These commands have been created by the resource administrator +- `sudo_display_name` (String) The name for sudo commands that will be visible to end users + + + + +### Nested Schema for `rules.resource_selector` + +Required: + +- `server_based_resource` (Attributes) (see [below for nested schema](#nestedatt--rules--resource_selector--server_based_resource)) + + +### Nested Schema for `rules.resource_selector.server_based_resource` + +Required: + +- `selectors` (Attributes List) (see [below for nested schema](#nestedatt--rules--resource_selector--server_based_resource--selectors)) + + +### Nested Schema for `rules.resource_selector.server_based_resource.selectors` + +Optional: + +- `individual_server` (Attributes) (see [below for nested schema](#nestedatt--rules--resource_selector--server_based_resource--selectors--individual_server)) +- `individual_server_account` (Attributes) (see [below for nested schema](#nestedatt--rules--resource_selector--server_based_resource--selectors--individual_server_account)) +- `server_label` (Attributes) (see [below for nested schema](#nestedatt--rules--resource_selector--server_based_resource--selectors--server_label)) + + +### Nested Schema for `rules.resource_selector.server_based_resource.selectors.individual_server` + +Required: + +- `server` (String) + + + +### Nested Schema for `rules.resource_selector.server_based_resource.selectors.individual_server_account` + +Required: + +- `server` (String) +- `username` (String) + + + +### Nested Schema for `rules.resource_selector.server_based_resource.selectors.server_label` + +Required: + +- `account_selector` (Attributes) (see [below for nested schema](#nestedatt--rules--resource_selector--server_based_resource--selectors--server_label--account_selector)) +- `account_selector_type` (String) + +Optional: + +- `server_selector` (Attributes) (see [below for nested schema](#nestedatt--rules--resource_selector--server_based_resource--selectors--server_label--server_selector)) + + +### Nested Schema for `rules.resource_selector.server_based_resource.selectors.server_label.account_selector` + +Optional: + +- `usernames` (List of String) + + + +### Nested Schema for `rules.resource_selector.server_based_resource.selectors.server_label.server_selector` + +Required: + +- `labels` (Map of String) + + + + + + + +### Nested Schema for `rules.conditions` + +Optional: + +- `access_request` (Attributes) (see [below for nested schema](#nestedatt--rules--conditions--access_request)) +- `gateway` (Attributes) (see [below for nested schema](#nestedatt--rules--conditions--gateway)) +- `mfa` (Attributes) (see [below for nested schema](#nestedatt--rules--conditions--mfa)) + + +### Nested Schema for `rules.conditions.access_request` + +Required: + +- `request_type_name` (String) + +Optional: + +- `expires_after_seconds` (Number) +- `request_type_id` (String) + + + +### Nested Schema for `rules.conditions.gateway` + +Required: + +- `session_recording` (Boolean) +- `traffic_forwarding` (Boolean) + + + +### Nested Schema for `rules.conditions.mfa` + +Required: + +- `re_auth_frequency_in_seconds` (Number) + +Optional: + +- `acr_values` (String) diff --git a/oktapam/convert/service_account_checkout_settings.go b/oktapam/convert/service_account_checkout_settings.go new file mode 100644 index 0000000000..f46fd0a3f9 --- /dev/null +++ b/oktapam/convert/service_account_checkout_settings.go @@ -0,0 +1,167 @@ +package convert + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/okta/terraform-provider-oktapam/oktapam/constants/descriptions" + + "github.com/atko-pam/pam-sdk-go/client/pam" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// ServiceAccountCheckoutSettingsModel represents the Terraform model for service account checkout settings +type ServiceAccountCheckoutSettingsModel struct { + CheckoutRequired types.Bool `tfsdk:"checkout_required"` + CheckoutDurationInSeconds types.Int32 `tfsdk:"checkout_duration_in_seconds"` + IncludeList types.List `tfsdk:"include_list"` + ExcludeList types.List `tfsdk:"exclude_list"` +} + +// ServiceAccountSettingNameObjectModel represents the Terraform model for service account setting name object +type ServiceAccountSettingNameObjectModel struct { + Id string `tfsdk:"id"` + ServiceAccountUserName string `tfsdk:"service_account_user_name"` + SaasAppInstanceName string `tfsdk:"saas_app_instance_name"` +} + +// ServiceAccountCheckoutSettingsSchemaAttributes returns the schema attributes for service account checkout settings +func ServiceAccountCheckoutSettingsSchemaAttributes(mergeIntoMap map[string]schema.Attribute) map[string]schema.Attribute { + myMap := map[string]schema.Attribute{ + "checkout_duration_in_seconds": schema.Int32Attribute{ + Required: true, + Description: descriptions.CheckoutDurationInSeconds, + Validators: []validator.Int32{ + int32validator.Between(900, 86400), + }, + }, + "checkout_required": schema.BoolAttribute{ + Required: true, + Description: descriptions.CheckoutRequired, + }, + "exclude_list": schema.ListAttribute{ + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "service_account_user_name": types.StringType, + "saas_app_instance_name": types.StringType, + }, + }, + Optional: true, + Description: descriptions.ExcludeList, + }, + "include_list": schema.ListAttribute{ + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "service_account_user_name": types.StringType, + "saas_app_instance_name": types.StringType, + }, + }, + Optional: true, + Description: descriptions.IncludeList, + }, + } + + for key, value := range myMap { + mergeIntoMap[key] = value + } + return mergeIntoMap +} + +// ServiceAccountCheckoutSettingsFromModelToSDK converts from the Terraform model to the SDK type +func ServiceAccountCheckoutSettingsFromModelToSDK(ctx context.Context, in *ServiceAccountCheckoutSettingsModel) (*pam.APIServiceAccountCheckoutSettings, diag.Diagnostics) { + var out pam.APIServiceAccountCheckoutSettings + var diags diag.Diagnostics + + if !in.CheckoutRequired.IsNull() && !in.CheckoutRequired.IsUnknown() { + out.CheckoutRequired = in.CheckoutRequired.ValueBool() + } + if !in.CheckoutDurationInSeconds.IsNull() && !in.CheckoutDurationInSeconds.IsUnknown() { + out.CheckoutDurationInSeconds = in.CheckoutDurationInSeconds.ValueInt32() + } + + if !in.IncludeList.IsNull() && !in.IncludeList.IsUnknown() { + var modelList []ServiceAccountSettingNameObjectModel + diags.Append(in.IncludeList.ElementsAs(ctx, &modelList, false)...) + if diags.HasError() { + return nil, diags + } + + includeList := make([]pam.ServiceAccountSettingNameObject, len(modelList)) + for i, item := range modelList { + includeList[i] = pam.ServiceAccountSettingNameObject{ + Id: item.Id, + ServiceAccountUserName: &item.ServiceAccountUserName, + SaasAppInstanceName: &item.SaasAppInstanceName, + } + } + out.IncludeList = includeList + } + + if !in.ExcludeList.IsNull() && !in.ExcludeList.IsUnknown() { + var modelList []ServiceAccountSettingNameObjectModel + diags.Append(in.ExcludeList.ElementsAs(ctx, &modelList, false)...) + if diags.HasError() { + return nil, diags + } + + excludeList := make([]pam.ServiceAccountSettingNameObject, len(modelList)) + for i, item := range modelList { + excludeList[i] = pam.ServiceAccountSettingNameObject{ + Id: item.Id, + ServiceAccountUserName: &item.ServiceAccountUserName, + SaasAppInstanceName: &item.SaasAppInstanceName, + } + } + out.ExcludeList = excludeList + } + + return &out, diags +} + +// ServiceAccountCheckoutSettingsFromSDKToModel converts from the SDK type to the Terraform model +func ServiceAccountCheckoutSettingsFromSDKToModel(ctx context.Context, in *pam.APIServiceAccountCheckoutSettings) (*ServiceAccountCheckoutSettingsModel, diag.Diagnostics) { + var out ServiceAccountCheckoutSettingsModel + var diags diag.Diagnostics + + if val, ok := in.GetCheckoutRequiredOk(); ok { + out.CheckoutRequired = types.BoolValue(*val) + } + + if val, ok := in.GetCheckoutDurationInSecondsOk(); ok { + out.CheckoutDurationInSeconds = types.Int32Value(*val) + } + + includeList, d := types.ListValueFrom(ctx, types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "service_account_user_name": types.StringType, + "saas_app_instance_name": types.StringType, + }, + }, in.IncludeList) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + out.IncludeList = includeList + + excludeList, d := types.ListValueFrom(ctx, types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "service_account_user_name": types.StringType, + "saas_app_instance_name": types.StringType, + }, + }, in.ExcludeList) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + out.ExcludeList = excludeList + + return &out, diags +} diff --git a/oktapam/provider_httpmock.go b/oktapam/provider_httpmock.go index 6b6d28ae7c..5baac1929a 100644 --- a/oktapam/provider_httpmock.go +++ b/oktapam/provider_httpmock.go @@ -2,10 +2,14 @@ package oktapam import ( "context" + "encoding/json" "log" + "net/http" + "regexp" "github.com/atko-pam/pam-sdk-go/client/pam" "github.com/go-resty/resty/v2" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-go/tfprotov6" diag2 "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -77,3 +81,128 @@ func httpMockClients() *client.APIClients { LocalClient: localClient, } } + +// SetupDefaultMockResponders configures common mock responses for standard resources +func SetupDefaultMockResponders(groupName string, resourceGroupName string, projectName string) { + // Pre-generate UUIDs for consistency + groupID := generateUUID(groupName) + resourceGroupID := generateUUID(resourceGroupName) + projectID := generateUUID(projectName) + + // Group endpoints + httpmock.RegisterRegexpResponder("POST", + regexp.MustCompile(`/v1/teams/httpmock-test-team/groups`), + func(req *http.Request) (*http.Response, error) { + var requestBody map[string]interface{} + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + return httpmock.NewStringResponse(400, ""), nil + } + return httpmock.NewJsonResponse(201, map[string]interface{}{ + "id": groupID, + "name": requestBody["name"].(string), + }) + }, + ) + + httpmock.RegisterRegexpResponder("GET", + regexp.MustCompile(`/v1/teams/httpmock-test-team/groups/.*`), + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, map[string]interface{}{ + "id": groupID, + "name": groupName, + }) + }, + ) + + // Resource Group endpoints + httpmock.RegisterRegexpResponder("POST", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups`), + func(req *http.Request) (*http.Response, error) { + var requestBody map[string]interface{} + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + return httpmock.NewStringResponse(400, ""), nil + } + return httpmock.NewJsonResponse(201, map[string]interface{}{ + "id": resourceGroupID, + "name": requestBody["name"].(string), + "description": requestBody["description"].(string), + "delegated_resource_admin_groups": []map[string]interface{}{ + { + "id": groupID, + "type": "group", + }, + }, + }) + }, + ) + + httpmock.RegisterRegexpResponder("GET", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*`), + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, map[string]interface{}{ + "id": resourceGroupID, + "name": resourceGroupName, + "description": "test resource group", + "delegated_resource_admin_groups": []map[string]interface{}{ + { + "id": groupID, + "type": "user_group", + }, + }, + }) + }, + ) + + httpmock.RegisterRegexpResponder("POST", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects`), + func(req *http.Request) (*http.Response, error) { + var requestBody map[string]interface{} + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + return httpmock.NewStringResponse(400, ""), nil + } + return httpmock.NewJsonResponse(201, map[string]interface{}{ + "id": projectID, + "name": projectName, + "resource_group": resourceGroupID, + "team": "httpmock-test-team", + "ssh_certificate_type": "CERT_TYPE_ED25519_01", + "account_discovery": true, + }) + }, + ) + + httpmock.RegisterRegexpResponder("GET", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*`), + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, map[string]interface{}{ + "id": projectID, + "name": projectName, + "resource_group": resourceGroupID, + "team": "httpmock-test-team", + "ssh_certificate_type": "CERT_TYPE_ED25519_01", + "account_discovery": true, + }) + }, + ) + + // Add DELETE responders + httpmock.RegisterRegexpResponder("DELETE", + regexp.MustCompile(`/v1/teams/httpmock-test-team/groups/.*`), + httpmock.NewStringResponder(204, ""), + ) + + httpmock.RegisterRegexpResponder("DELETE", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*`), + httpmock.NewStringResponder(204, ""), + ) + + httpmock.RegisterRegexpResponder("DELETE", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*`), + httpmock.NewStringResponder(204, ""), + ) +} + +func generateUUID(name string) string { + id := uuid.NewSHA1(uuid.NameSpaceOID, []byte(name)) + return id.String() +} diff --git a/oktapam/provider_pluginframework.go b/oktapam/provider_pluginframework.go index 130ec830df..12756d1216 100644 --- a/oktapam/provider_pluginframework.go +++ b/oktapam/provider_pluginframework.go @@ -122,6 +122,8 @@ func (p *FrameworkProvider) Resources(_ context.Context) []func() resource.Resou //Add New Resources here NewServerCheckoutSettingsResource, NewSecurityPolicyResource, + NewSaasAppCheckoutSettingsResource, + NewoktaUniversalDirectoryCheckoutSettingsResource, } } diff --git a/oktapam/resource_okta_universal_directory_checkout_settings.go b/oktapam/resource_okta_universal_directory_checkout_settings.go new file mode 100644 index 0000000000..bfad7e58a8 --- /dev/null +++ b/oktapam/resource_okta_universal_directory_checkout_settings.go @@ -0,0 +1,213 @@ +package oktapam + +import ( + "context" + "fmt" + + "github.com/okta/terraform-provider-oktapam/oktapam/convert" + + "github.com/atko-pam/pam-sdk-go/client/pam" + "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" + "github.com/okta/terraform-provider-oktapam/oktapam/constants/descriptions" +) + +var ( + _ resource.Resource = &oktaUniversalDirectoryCheckoutSettingsResource{} + _ resource.ResourceWithConfigure = &oktaUniversalDirectoryCheckoutSettingsResource{} +) + +func NewoktaUniversalDirectoryCheckoutSettingsResource() resource.Resource { + return &oktaUniversalDirectoryCheckoutSettingsResource{} +} + +type oktaUniversalDirectoryCheckoutSettingsResource struct { + api *pam.ProjectsAPIService + teamName string +} + +type oktaUniversalDirectoryCheckoutSettingsResourceModel struct { + Id types.String `tfsdk:"id"` + ResourceGroup string `tfsdk:"resource_group"` + Project string `tfsdk:"project"` + convert.ServiceAccountCheckoutSettingsModel +} + +func (r *oktaUniversalDirectoryCheckoutSettingsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_okta_universal_directory_checkout_settings" +} + +func (r *oktaUniversalDirectoryCheckoutSettingsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages checkout settings for Okta Universal Directory resources in a project", + Attributes: convert.ServiceAccountCheckoutSettingsSchemaAttributes(map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project": schema.StringAttribute{ + Required: true, + Description: descriptions.ProjectID, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "resource_group": schema.StringAttribute{ + Required: true, + Description: descriptions.ResourceGroupID, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }), + } +} + +func (r *oktaUniversalDirectoryCheckoutSettingsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan oktaUniversalDirectoryCheckoutSettingsResourceModel + if diags := req.Plan.Get(ctx, &plan); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + var checkoutSettings pam.APIServiceAccountCheckoutSettings + if settings, diags := convert.ServiceAccountCheckoutSettingsFromModelToSDK(ctx, &plan.ServiceAccountCheckoutSettingsModel); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } else { + checkoutSettings = *settings + } + + if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { + resp.Diagnostics.AddError("Error creating Okta UD checkout settings", err.Error()) + return + } + + plan.Id = types.StringValue(formatOktaUDCheckoutSettingsID(plan.ResourceGroup, plan.Project)) + + if diags := resp.State.Set(ctx, plan); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } +} + +func (r *oktaUniversalDirectoryCheckoutSettingsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state oktaUniversalDirectoryCheckoutSettingsResourceModel + if diags := req.State.Get(ctx, &state); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + if checkoutSettings, _, err := r.api.FetchResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, state.ResourceGroup, state.Project).Execute(); err != nil { + resp.Diagnostics.AddError( + "Error reading Okta UD checkout settings", + fmt.Sprintf("Could not read Okta UD checkout settings for team: %q resource_group: %q project_id: %q: Error: %s", + r.teamName, + state.ResourceGroup, + state.Project, + err.Error())) + return + } else { + if settingsModel, diags := convert.ServiceAccountCheckoutSettingsFromSDKToModel(ctx, checkoutSettings); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } else { + state.ServiceAccountCheckoutSettingsModel = *settingsModel + } + } + + if diags := resp.State.Set(ctx, state); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } +} + +func (r *oktaUniversalDirectoryCheckoutSettingsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan oktaUniversalDirectoryCheckoutSettingsResourceModel + if diags := req.Plan.Get(ctx, &plan); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + var checkoutSettings pam.APIServiceAccountCheckoutSettings + if settings, diags := convert.ServiceAccountCheckoutSettingsFromModelToSDK(ctx, &plan.ServiceAccountCheckoutSettingsModel); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } else { + checkoutSettings = *settings + } + + if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { + resp.Diagnostics.AddError("Error updating Okta UD checkout settings", err.Error()) + return + } + + if updatedSettings, _, err := r.api.FetchResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).Execute(); err != nil { + resp.Diagnostics.AddError( + "Error reading Okta UD checkout settings", + fmt.Sprintf("Could not read Okta UD checkout settings for team: %q resource_group: %q project_id: %q: Error: %s", + r.teamName, + plan.ResourceGroup, + plan.Project, + err.Error())) + return + } else { + if settingsModel, diags := convert.ServiceAccountCheckoutSettingsFromSDKToModel(ctx, updatedSettings); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } else { + plan.ServiceAccountCheckoutSettingsModel = *settingsModel + } + } + + if diags := resp.State.Set(ctx, plan); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } +} + +func (r *oktaUniversalDirectoryCheckoutSettingsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state oktaUniversalDirectoryCheckoutSettingsResourceModel + if diags := req.State.Get(ctx, &state); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + defaultCheckoutDurationInSeconds := int32(900) + checkoutSettings := &pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: false, + CheckoutDurationInSeconds: defaultCheckoutDurationInSeconds, + IncludeList: []pam.ServiceAccountSettingNameObject{}, + ExcludeList: []pam.ServiceAccountSettingNameObject{}, + } + + if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, state.ResourceGroup, state.Project).APIServiceAccountCheckoutSettings(*checkoutSettings).Execute(); err != nil { + resp.Diagnostics.AddError("Error resetting Okta UD checkout settings", err.Error()) + return + } + + state.Id = types.StringValue("") + if diags := resp.State.Set(ctx, state); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } +} + +func (r *oktaUniversalDirectoryCheckoutSettingsResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + sdkClient := getSDKClientFromMetadata(req.ProviderData) + r.api = sdkClient.SDKClient.ProjectsAPI + r.teamName = sdkClient.Team +} + +func formatOktaUDCheckoutSettingsID(resourceGroupID string, projectID string) string { + return fmt.Sprintf("%s/%s", resourceGroupID, projectID) +} diff --git a/oktapam/resource_okta_universal_directory_checkout_settings_test.go b/oktapam/resource_okta_universal_directory_checkout_settings_test.go new file mode 100644 index 0000000000..7e4dabfc6e --- /dev/null +++ b/oktapam/resource_okta_universal_directory_checkout_settings_test.go @@ -0,0 +1,288 @@ +package oktapam + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "regexp" + "testing" + + "github.com/atko-pam/pam-sdk-go/client/pam" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/jarcoal/httpmock" + "github.com/okta/terraform-provider-oktapam/oktapam/constants/attributes" +) + +const testAccOktaUDCheckoutSettingsBaseConfigFormat = ` +resource "oktapam_group" "test_resource_group_dga_group" { + name = "%s" +} +resource "oktapam_resource_group" "test_acc_resource_group" { + name = "%s" + description = "test resource group" + delegated_resource_admin_groups = [oktapam_group.test_resource_group_dga_group.id] +} +resource "oktapam_resource_group_project" "test_acc_resource_group_project" { + name = "%s" + resource_group = oktapam_resource_group.test_acc_resource_group.id + ssh_certificate_type = "CERT_TYPE_ED25519_01" + account_discovery = true +} +` + +const testAccOktaUDCheckoutSettingsCreateConfigFormat = ` +resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_universal_directory_checkout_settings" { + resource_group = oktapam_resource_group.test_acc_resource_group.id + project = oktapam_resource_group_project.test_acc_resource_group_project.id + checkout_required = true + checkout_duration_in_seconds = 900 +} +` + +const testAccOktaUDCheckoutSettingsUpdateWithIncludeListConfigFormat = ` +resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_universal_directory_checkout_settings" { + resource_group = oktapam_resource_group.test_acc_resource_group.id + project = oktapam_resource_group_project.test_acc_resource_group_project.id + checkout_required = true + checkout_duration_in_seconds = 3600 + include_list = [ + { + id = "account1", + service_account_user_name = "user1", + saas_app_instance_name = "app1" + }, + { + id = "account2", + service_account_user_name = "user2", + saas_app_instance_name = "app2" + } + ] +} +` + +const testAccOktaUDCheckoutSettingsUpdateWithExcludeListConfigFormat = ` +resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_universal_directory_checkout_settings" { + resource_group = oktapam_resource_group.test_acc_resource_group.id + project = oktapam_resource_group_project.test_acc_resource_group_project.id + checkout_required = true + checkout_duration_in_seconds = 3600 + exclude_list = [ + { + id = "account3", + service_account_user_name = "user3", + saas_app_instance_name = "app3" + }, + { + id = "account4", + service_account_user_name = "user4", + saas_app_instance_name = "app4" + } + ] +} +` + +const testAccOktaUDCheckoutSettingsUpdateWithBothListsConfigFormat = ` +resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_universal_directory_checkout_settings" { + resource_group = oktapam_resource_group.test_acc_resource_group.id + project = oktapam_resource_group_project.test_acc_resource_group_project.id + checkout_required = true + checkout_duration_in_seconds = 7200 + include_list = [ + { + id = "account1", + service_account_user_name = "user1", + saas_app_instance_name = "app1" + } + ] + exclude_list = [ + { + id = "account3", + service_account_user_name = "user3", + saas_app_instance_name = "app3" + } + ] +} +` + +func TestAccOktaUDCheckoutSettings(t *testing.T) { + checkTeamApplicable(t, true) + resourceName := "oktapam_okta_universal_directory_checkout_settings.test_acc_okta_universal_directory_checkout_settings" + resourceGroupName := fmt.Sprintf("test_acc_resource_group_%s", randSeq()) + projectName := fmt.Sprintf("test_acc_resource_group_project_%s", randSeq()) + delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_dga_%s", randSeq()) + defaultCheckoutDuration := int32(900) + + initialSettings := &pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: true, + CheckoutDurationInSeconds: defaultCheckoutDuration, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccOktaUDCheckoutSettingsCheckExists(resourceName, initialSettings), + ), + }, + { + Config: createOktaUDCheckoutSettingsUpdateWithBothListsConfig(delegatedAdminGroupName, resourceGroupName, projectName), + ExpectError: regexp.MustCompile(`Only one of 'IncludeList' or 'ExcludeList' can be specified`), + }, + }, + }) +} + +// TestAccOktaUDCheckoutSettingsWithMockHTTPClient is a test that uses httpmock to mock the HTTP client +// and test the Okta UD checkout settings resource marshalling and unmarshalling correctly. +func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { + // Enable debug logging for httpmock + httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { + t.Logf("[DEBUG] No responder found for: %s %s", req.Method, req.URL) + return nil, fmt.Errorf("no responder found for: %s %s", req.Method, req.URL) + }) + + resourceName := "oktapam_okta_universal_directory_checkout_settings.test_acc_okta_universal_directory_checkout_settings" + resourceGroupName := fmt.Sprintf("test_acc_resource_group_%s", randSeq()) + projectName := fmt.Sprintf("test_acc_resource_group_project_%s", randSeq()) + delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_dga_%s", randSeq()) + user1 := "user1" + app1 := "app1" + user3 := "user3" + app3 := "app3" + + // Setup httpmock + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + SetupDefaultMockResponders(delegatedAdminGroupName, resourceGroupName, projectName) + + // Mock the PUT endpoint for update operations + httpmock.RegisterResponder("PUT", + fmt.Sprintf("/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s/okta_universal_directory_checkout_settings", + resourceGroupName, projectName), + func(req *http.Request) (*http.Response, error) { + var requestBody pam.APIServiceAccountCheckoutSettings + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + return httpmock.NewStringResponse(400, ""), nil + } + return httpmock.NewJsonResponse(204, nil) + }, + ) + + // Mock the GET endpoint for read operations + httpmock.RegisterResponder("GET", + fmt.Sprintf("/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s/okta_universal_directory_checkout_settings", + resourceGroupName, projectName), + func(req *http.Request) (*http.Response, error) { + if httpmock.GetCallCountInfo()["GET"]%2 == 0 { + // Return include list settings for the first call + return httpmock.NewJsonResponse(200, pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: true, + CheckoutDurationInSeconds: 3600, + IncludeList: []pam.ServiceAccountSettingNameObject{ + { + Id: "account1", + ServiceAccountUserName: &user1, + SaasAppInstanceName: &app1, + }, + }, + }) + } else { + // Return exclude list settings for the second call + return httpmock.NewJsonResponse(200, pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: true, + CheckoutDurationInSeconds: 3600, + ExcludeList: []pam.ServiceAccountSettingNameObject{ + { + Id: "account3", + ServiceAccountUserName: &user3, + SaasAppInstanceName: &app3, + }, + }, + }) + } + }, + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: httpMockTestV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: createOktaUDCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), + resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), + resource.TestCheckResourceAttr(resourceName, "include_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "include_list.0.id", "account1"), + resource.TestCheckResourceAttr(resourceName, "include_list.0.service_account_user_name", "user1"), + resource.TestCheckResourceAttr(resourceName, "include_list.0.saas_app_instance_name", "app1"), + ), + }, + { + Config: createOktaUDCheckoutSettingsUpdateWithExcludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), + resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0.id", "account3"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0.service_account_user_name", "user3"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0.saas_app_instance_name", "app3"), + ), + }, + }, + }) +} + +func testAccOktaUDCheckoutSettingsCheckExists(resourceName string, expected *pam.APIServiceAccountCheckoutSettings) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Okta UD checkout settings not found: %s", resourceName) + } + + resourceGroupID := rs.Primary.Attributes[attributes.ResourceGroup] + projectID := rs.Primary.Attributes[attributes.Project] + + client := mustTestAccAPIClients().SDKClient + settings, _, err := client.SDKClient.ProjectsAPI.FetchResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(context.Background(), client.Team, resourceGroupID, projectID).Execute() + if err != nil { + return fmt.Errorf("Error fetching Okta UD checkout settings: %s", err) + } + + if settings.CheckoutRequired != expected.CheckoutRequired { + return fmt.Errorf("Okta UD checkout settings checkout required does not match: %t != %t", settings.CheckoutRequired, expected.CheckoutRequired) + } + + if settings.CheckoutDurationInSeconds != expected.CheckoutDurationInSeconds { + return fmt.Errorf("Okta UD checkout settings checkout duration in seconds does not match: %d != %d", settings.CheckoutDurationInSeconds, expected.CheckoutDurationInSeconds) + } + + return nil + } +} + +func createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { + return fmt.Sprintf(testAccOktaUDCheckoutSettingsBaseConfigFormat, delegatedAdminGroupName, resourceGroupName, projectName) +} + +func createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { + return createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccOktaUDCheckoutSettingsCreateConfigFormat +} + +func createOktaUDCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { + return createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccOktaUDCheckoutSettingsUpdateWithIncludeListConfigFormat +} + +func createOktaUDCheckoutSettingsUpdateWithExcludeListConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { + return createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccOktaUDCheckoutSettingsUpdateWithExcludeListConfigFormat +} + +func createOktaUDCheckoutSettingsUpdateWithBothListsConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { + return createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccOktaUDCheckoutSettingsUpdateWithBothListsConfigFormat +} diff --git a/oktapam/resource_saas_app_checkout_settings.go b/oktapam/resource_saas_app_checkout_settings.go new file mode 100644 index 0000000000..2559044d1b --- /dev/null +++ b/oktapam/resource_saas_app_checkout_settings.go @@ -0,0 +1,213 @@ +package oktapam + +import ( + "context" + "fmt" + + "github.com/okta/terraform-provider-oktapam/oktapam/convert" + + "github.com/atko-pam/pam-sdk-go/client/pam" + "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" + "github.com/okta/terraform-provider-oktapam/oktapam/constants/descriptions" +) + +var ( + _ resource.Resource = &saasAppCheckoutSettingsResource{} + _ resource.ResourceWithConfigure = &saasAppCheckoutSettingsResource{} +) + +func NewSaasAppCheckoutSettingsResource() resource.Resource { + return &saasAppCheckoutSettingsResource{} +} + +type saasAppCheckoutSettingsResource struct { + api *pam.ProjectsAPIService + teamName string +} + +type saasAppCheckoutSettingsResourceModel struct { + Id types.String `tfsdk:"id"` + ResourceGroup string `tfsdk:"resource_group"` + Project string `tfsdk:"project"` + convert.ServiceAccountCheckoutSettingsModel +} + +func (r *saasAppCheckoutSettingsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_saas_app_checkout_settings" +} + +func (r *saasAppCheckoutSettingsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages checkout settings for SaaS Application resources in a project", + Attributes: convert.ServiceAccountCheckoutSettingsSchemaAttributes(map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project": schema.StringAttribute{ + Required: true, + Description: descriptions.ProjectID, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "resource_group": schema.StringAttribute{ + Required: true, + Description: descriptions.ResourceGroupID, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }), + } +} + +func (r *saasAppCheckoutSettingsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan saasAppCheckoutSettingsResourceModel + if diags := req.Plan.Get(ctx, &plan); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + var checkoutSettings pam.APIServiceAccountCheckoutSettings + if settings, diags := convert.ServiceAccountCheckoutSettingsFromModelToSDK(ctx, &plan.ServiceAccountCheckoutSettingsModel); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } else { + checkoutSettings = *settings + } + + if _, err := r.api.UpdateResourceGroupSaasAppBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { + resp.Diagnostics.AddError("Error creating SaaS App checkout settings", err.Error()) + return + } + + plan.Id = types.StringValue(formatSaasAppCheckoutSettingsID(plan.ResourceGroup, plan.Project)) + + if diags := resp.State.Set(ctx, plan); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } +} + +func (r *saasAppCheckoutSettingsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state saasAppCheckoutSettingsResourceModel + if diags := req.State.Get(ctx, &state); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + if checkoutSettings, _, err := r.api.FetchResourceGroupSaasAppBasedProjectCheckoutSettings(ctx, r.teamName, state.ResourceGroup, state.Project).Execute(); err != nil { + resp.Diagnostics.AddError( + "Error reading SaaS App checkout settings", + fmt.Sprintf("Could not read SaaS App checkout settings for team: %q resource_group: %q project_id: %q: Error: %s", + r.teamName, + state.ResourceGroup, + state.Project, + err.Error())) + return + } else { + if settingsModel, diags := convert.ServiceAccountCheckoutSettingsFromSDKToModel(ctx, checkoutSettings); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } else { + state.ServiceAccountCheckoutSettingsModel = *settingsModel + } + } + + if diags := resp.State.Set(ctx, state); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } +} + +func (r *saasAppCheckoutSettingsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan saasAppCheckoutSettingsResourceModel + if diags := req.Plan.Get(ctx, &plan); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + var checkoutSettings pam.APIServiceAccountCheckoutSettings + if settings, diags := convert.ServiceAccountCheckoutSettingsFromModelToSDK(ctx, &plan.ServiceAccountCheckoutSettingsModel); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } else { + checkoutSettings = *settings + } + + if _, err := r.api.UpdateResourceGroupSaasAppBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { + resp.Diagnostics.AddError("Error updating SaaS App checkout settings", err.Error()) + return + } + + if updatedSettings, _, err := r.api.FetchResourceGroupSaasAppBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).Execute(); err != nil { + resp.Diagnostics.AddError( + "Error reading SaaS App checkout settings", + fmt.Sprintf("Could not read SaaS App checkout settings for team: %q resource_group: %q project_id: %q: Error: %s", + r.teamName, + plan.ResourceGroup, + plan.Project, + err.Error())) + return + } else { + if settingsModel, diags := convert.ServiceAccountCheckoutSettingsFromSDKToModel(ctx, updatedSettings); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } else { + plan.ServiceAccountCheckoutSettingsModel = *settingsModel + } + } + + if diags := resp.State.Set(ctx, plan); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } +} + +func (r *saasAppCheckoutSettingsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state saasAppCheckoutSettingsResourceModel + if diags := req.State.Get(ctx, &state); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + defaultCheckoutDurationInSeconds := int32(900) + checkoutSettings := &pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: false, + CheckoutDurationInSeconds: defaultCheckoutDurationInSeconds, + IncludeList: []pam.ServiceAccountSettingNameObject{}, + ExcludeList: []pam.ServiceAccountSettingNameObject{}, + } + + if _, err := r.api.UpdateResourceGroupSaasAppBasedProjectCheckoutSettings(ctx, r.teamName, state.ResourceGroup, state.Project).APIServiceAccountCheckoutSettings(*checkoutSettings).Execute(); err != nil { + resp.Diagnostics.AddError("Error resetting SaaS App checkout settings", err.Error()) + return + } + + state.Id = types.StringValue("") + if diags := resp.State.Set(ctx, state); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } +} + +func (r *saasAppCheckoutSettingsResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + sdkClient := getSDKClientFromMetadata(req.ProviderData) + r.api = sdkClient.SDKClient.ProjectsAPI + r.teamName = sdkClient.Team +} + +func formatSaasAppCheckoutSettingsID(resourceGroupID string, projectID string) string { + return fmt.Sprintf("%s/%s", resourceGroupID, projectID) +} diff --git a/oktapam/resource_saas_app_checkout_settings_test.go b/oktapam/resource_saas_app_checkout_settings_test.go new file mode 100644 index 0000000000..7acd553eb3 --- /dev/null +++ b/oktapam/resource_saas_app_checkout_settings_test.go @@ -0,0 +1,280 @@ +package oktapam + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "regexp" + "testing" + + "github.com/atko-pam/pam-sdk-go/client/pam" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/jarcoal/httpmock" + "github.com/okta/terraform-provider-oktapam/oktapam/constants/attributes" +) + +const testAccSaasAppCheckoutSettingsBaseConfigFormat = ` +resource "oktapam_group" "test_resource_group_dga_group" { + name = "%s" +} +resource "oktapam_resource_group" "test_acc_resource_group" { + name = "%s" + description = "test resource group" + delegated_resource_admin_groups = [oktapam_group.test_resource_group_dga_group.id] +} +resource "oktapam_resource_group_project" "test_acc_resource_group_project" { + name = "%s" + resource_group = oktapam_resource_group.test_acc_resource_group.id + ssh_certificate_type = "CERT_TYPE_ED25519_01" + account_discovery = true +} +` + +const testAccSaasAppCheckoutSettingsCreateConfigFormat = ` +resource "oktapam_saas_app_checkout_settings" "test_acc_saas_app_checkout_settings" { + resource_group = oktapam_resource_group.test_acc_resource_group.id + project = oktapam_resource_group_project.test_acc_resource_group_project.id + checkout_required = true + checkout_duration_in_seconds = 900 +} +` + +const testAccSaasAppCheckoutSettingsUpdateWithIncludeListConfigFormat = ` +resource "oktapam_saas_app_checkout_settings" "test_acc_saas_app_checkout_settings" { + resource_group = oktapam_resource_group.test_acc_resource_group.id + project = oktapam_resource_group_project.test_acc_resource_group_project.id + checkout_required = true + checkout_duration_in_seconds = 3600 + include_list = [ + { + id = "account1", + service_account_user_name = "user1", + saas_app_instance_name = "app1" + }, + { + id = "account2", + service_account_user_name = "user2", + saas_app_instance_name = "app2" + } + ] +} +` + +const testAccSaasAppCheckoutSettingsUpdateWithExcludeListConfigFormat = ` +resource "oktapam_saas_app_checkout_settings" "test_acc_saas_app_checkout_settings" { + resource_group = oktapam_resource_group.test_acc_resource_group.id + project = oktapam_resource_group_project.test_acc_resource_group_project.id + checkout_required = true + checkout_duration_in_seconds = 3600 + exclude_list = [ + { + id = "account3", + service_account_user_name = "user3", + saas_app_instance_name = "app3" + }, + { + id = "account4", + service_account_user_name = "user4", + saas_app_instance_name = "app4" + } + ] +} +` + +const testAccSaasAppCheckoutSettingsUpdateWithBothListsConfigFormat = ` +resource "oktapam_saas_app_checkout_settings" "test_acc_saas_app_checkout_settings" { + resource_group = oktapam_resource_group.test_acc_resource_group.id + project = oktapam_resource_group_project.test_acc_resource_group_project.id + checkout_required = true + checkout_duration_in_seconds = 7200 + include_list = [ + { + id = "account1", + service_account_user_name = "user1", + saas_app_instance_name = "app1" + } + ] + exclude_list = [ + { + id = "account3", + service_account_user_name = "user3", + saas_app_instance_name = "app3" + } + ] +} +` + +func TestAccSaasAppCheckoutSettings(t *testing.T) { + checkTeamApplicable(t, true) + resourceName := "oktapam_saas_app_checkout_settings.test_acc_saas_app_checkout_settings" + resourceGroupName := fmt.Sprintf("test_acc_resource_group_%s", randSeq()) + projectName := fmt.Sprintf("test_acc_resource_group_project_%s", randSeq()) + delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_dga_%s", randSeq()) + defaultCheckoutDuration := int32(900) + + initialSettings := &pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: true, + CheckoutDurationInSeconds: defaultCheckoutDuration, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: createSaasAppCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccSaasAppCheckoutSettingsCheckExists(resourceName, initialSettings), + ), + }, + { + Config: createSaasAppCheckoutSettingsUpdateWithBothListsConfig(delegatedAdminGroupName, resourceGroupName, projectName), + ExpectError: regexp.MustCompile(`Only one of 'IncludeList' or 'ExcludeList' can be specified`), + }, + }, + }) +} + +// TestAccSaasAppCheckoutSettingsWithMockHTTPClient is a test that uses httpmock to mock the HTTP client +// and test the SaaS App checkout settings resource marshalling and unmarshalling correctly. +func TestAccSaasAppCheckoutSettingsWithMockHTTPClient(t *testing.T) { + resourceName := "oktapam_saas_app_checkout_settings.test_acc_saas_app_checkout_settings" + resourceGroupName := fmt.Sprintf("test_acc_resource_group_%s", randSeq()) + projectName := fmt.Sprintf("test_acc_resource_group_project_%s", randSeq()) + delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_dga_%s", randSeq()) + user1 := "user1" + app1 := "app1" + user3 := "user3" + app3 := "app3" + + // Setup httpmock + httpmock.Activate() + defer httpmock.DeactivateAndReset() + SetupDefaultMockResponders(delegatedAdminGroupName, resourceGroupName, projectName) + + // Mock the PUT endpoint for update operations + httpmock.RegisterResponder("PUT", + fmt.Sprintf("/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s/saas_app_checkout_settings", + resourceGroupName, projectName), + func(req *http.Request) (*http.Response, error) { + var requestBody pam.APIServiceAccountCheckoutSettings + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + return httpmock.NewStringResponse(400, ""), nil + } + return httpmock.NewJsonResponse(204, nil) + }, + ) + + // Mock the GET endpoint for read operations + httpmock.RegisterResponder("GET", + fmt.Sprintf("/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s/saas_app_checkout_settings", + resourceGroupName, projectName), + func(req *http.Request) (*http.Response, error) { + if httpmock.GetCallCountInfo()["GET"]%2 == 0 { + // Return include list settings for the first call + return httpmock.NewJsonResponse(200, pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: true, + CheckoutDurationInSeconds: 3600, + IncludeList: []pam.ServiceAccountSettingNameObject{ + { + Id: "account1", + ServiceAccountUserName: &user1, + SaasAppInstanceName: &app1, + }, + }, + }) + } else { + // Return exclude list settings for the second call + return httpmock.NewJsonResponse(200, pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: true, + CheckoutDurationInSeconds: 3600, + ExcludeList: []pam.ServiceAccountSettingNameObject{ + { + Id: "account3", + ServiceAccountUserName: &user3, + SaasAppInstanceName: &app3, + }, + }, + }) + } + }, + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: httpMockTestV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: createSaasAppCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), + resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), + resource.TestCheckResourceAttr(resourceName, "include_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "include_list.0.id", "account1"), + resource.TestCheckResourceAttr(resourceName, "include_list.0.service_account_user_name", "user1"), + resource.TestCheckResourceAttr(resourceName, "include_list.0.saas_app_instance_name", "app1"), + ), + }, + { + Config: createSaasAppCheckoutSettingsUpdateWithExcludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), + resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0.id", "account3"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0.service_account_user_name", "user3"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0.saas_app_instance_name", "app3"), + ), + }, + }, + }) +} + +func testAccSaasAppCheckoutSettingsCheckExists(resourceName string, expected *pam.APIServiceAccountCheckoutSettings) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("SaaS App checkout settings not found: %s", resourceName) + } + + resourceGroupID := rs.Primary.Attributes[attributes.ResourceGroup] + projectID := rs.Primary.Attributes[attributes.Project] + + client := mustTestAccAPIClients().SDKClient + settings, _, err := client.SDKClient.ProjectsAPI.FetchResourceGroupSaasAppBasedProjectCheckoutSettings(context.Background(), client.Team, resourceGroupID, projectID).Execute() + if err != nil { + return fmt.Errorf("Error fetching SaaS App checkout settings: %s", err) + } + + if settings.CheckoutRequired != expected.CheckoutRequired { + return fmt.Errorf("SaaS App checkout settings checkout required does not match: %t != %t", settings.CheckoutRequired, expected.CheckoutRequired) + } + + if settings.CheckoutDurationInSeconds != expected.CheckoutDurationInSeconds { + return fmt.Errorf("SaaS App checkout settings checkout duration in seconds does not match: %d != %d", settings.CheckoutDurationInSeconds, expected.CheckoutDurationInSeconds) + } + return nil + } +} + +func createSaasAppCheckoutSettingsBaseConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { + return fmt.Sprintf(testAccSaasAppCheckoutSettingsBaseConfigFormat, delegatedAdminGroupName, resourceGroupName, projectName) +} + +func createSaasAppCheckoutSettingsCreateConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { + return createSaasAppCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccSaasAppCheckoutSettingsCreateConfigFormat +} + +func createSaasAppCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { + return createSaasAppCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccSaasAppCheckoutSettingsUpdateWithIncludeListConfigFormat +} + +func createSaasAppCheckoutSettingsUpdateWithExcludeListConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { + return createSaasAppCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccSaasAppCheckoutSettingsUpdateWithExcludeListConfigFormat +} + +func createSaasAppCheckoutSettingsUpdateWithBothListsConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { + return createSaasAppCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccSaasAppCheckoutSettingsUpdateWithBothListsConfigFormat +} From 3d9115d09b0806297e9a830ccdf9322057fbb761 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Tue, 14 Jan 2025 00:32:51 -0800 Subject: [PATCH 02/13] debugging mock test failure --- Makefile | 3 +- oktapam/provider_httpmock.go | 106 ++--- ...a_universal_directory_checkout_settings.go | 10 + ...versal_directory_checkout_settings_test.go | 394 +++++++++++++----- ...esource_saas_app_checkout_settings_test.go | 6 +- 5 files changed, 363 insertions(+), 156 deletions(-) diff --git a/Makefile b/Makefile index f5d7192154..099bbbf64d 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,8 @@ testacc: testaccpam: # TESTARGS here can be used to pass arbitrary flags to go test, e.g. '-run TestMyTest' - TF_ACC=1 TF_ACC_PAM=1 go test ./... -v $(TESTARGS) -timeout 120m + TF_LOG=DEBUG TF_ACC=1 TF_ACC_PAM=1 go test ./... -v $(TESTARGS) -timeout 120m + # TF_LOG=DEBUG go test -v ./oktapam -run TestAccOktaUDCheckoutSettingsWithMockHTTPClient generate: diff --git a/oktapam/provider_httpmock.go b/oktapam/provider_httpmock.go index 5baac1929a..22a0e2396c 100644 --- a/oktapam/provider_httpmock.go +++ b/oktapam/provider_httpmock.go @@ -1,15 +1,17 @@ package oktapam import ( + "bytes" "context" "encoding/json" + "fmt" + "io" "log" "net/http" "regexp" "github.com/atko-pam/pam-sdk-go/client/pam" "github.com/go-resty/resty/v2" - "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-go/tfprotov6" diag2 "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -83,19 +85,14 @@ func httpMockClients() *client.APIClients { } // SetupDefaultMockResponders configures common mock responses for standard resources -func SetupDefaultMockResponders(groupName string, resourceGroupName string, projectName string) { - // Pre-generate UUIDs for consistency - groupID := generateUUID(groupName) - resourceGroupID := generateUUID(resourceGroupName) - projectID := generateUUID(projectName) - +func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectID string, groupName string, resourceGroupName string, projectName string) { // Group endpoints httpmock.RegisterRegexpResponder("POST", regexp.MustCompile(`/v1/teams/httpmock-test-team/groups`), func(req *http.Request) (*http.Response, error) { var requestBody map[string]interface{} if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { - return httpmock.NewStringResponse(400, ""), nil + return httpmock.NewStringResponse(400, fmt.Sprintf("Failed to decode: %v", err)), nil } return httpmock.NewJsonResponse(201, map[string]interface{}{ "id": groupID, @@ -118,14 +115,21 @@ func SetupDefaultMockResponders(groupName string, resourceGroupName string, proj httpmock.RegisterRegexpResponder("POST", regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups`), func(req *http.Request) (*http.Response, error) { - var requestBody map[string]interface{} + + bodyBytes, _ := io.ReadAll(req.Body) + req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + var requestBody struct { + Name string `json:"name"` + Description string `json:"description"` + DelegatedResourceAdminGroups []interface{} `json:"delegated_resource_admin_groups"` + } if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { - return httpmock.NewStringResponse(400, ""), nil + return httpmock.NewStringResponse(400, fmt.Sprintf("Failed to decode: %v", err)), nil } return httpmock.NewJsonResponse(201, map[string]interface{}{ "id": resourceGroupID, - "name": requestBody["name"].(string), - "description": requestBody["description"].(string), + "name": requestBody.Name, + "description": requestBody.Description, "delegated_resource_admin_groups": []map[string]interface{}{ { "id": groupID, @@ -153,37 +157,45 @@ func SetupDefaultMockResponders(groupName string, resourceGroupName string, proj }, ) - httpmock.RegisterRegexpResponder("POST", - regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects`), - func(req *http.Request) (*http.Response, error) { - var requestBody map[string]interface{} - if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { - return httpmock.NewStringResponse(400, ""), nil - } - return httpmock.NewJsonResponse(201, map[string]interface{}{ - "id": projectID, - "name": projectName, - "resource_group": resourceGroupID, - "team": "httpmock-test-team", - "ssh_certificate_type": "CERT_TYPE_ED25519_01", - "account_discovery": true, - }) - }, - ) - - httpmock.RegisterRegexpResponder("GET", - regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*`), - func(req *http.Request) (*http.Response, error) { - return httpmock.NewJsonResponse(200, map[string]interface{}{ - "id": projectID, - "name": projectName, - "resource_group": resourceGroupID, - "team": "httpmock-test-team", - "ssh_certificate_type": "CERT_TYPE_ED25519_01", - "account_discovery": true, - }) - }, - ) + // // Project endpoints + // httpmock.RegisterRegexpResponder("POST", + // regexp.MustCompile(fmt.Sprintf(`/v1/teams/httpmock-test-team/resource_groups/%s/projects`, resourceGroupID)), + // func(req *http.Request) (*http.Response, error) { + // bodyBytes, _ := io.ReadAll(req.Body) + // req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + // var requestBody struct { + // Name string `json:"name"` + // ResourceGroup string `json:"resource_group"` + // SSHCertificateType string `json:"ssh_certificate_type"` + // AccountDiscovery bool `json:"account_discovery"` + // } + // json.NewDecoder(req.Body).Decode(&requestBody) + + // resp := map[string]interface{}{ + // "id": projectID, + // "name": "sdajklshrfikljaewhtruiahtoigfe", + // "resource_group": resourceGroupID, + // "team": "httpmock-test-team", + // "ssh_certificate_type": requestBody.SSHCertificateType, + // "account_discovery": requestBody.AccountDiscovery, + // } + // return httpmock.NewJsonResponse(201, resp) + // }, + // ) + + // httpmock.RegisterRegexpResponder("GET", + // regexp.MustCompile(fmt.Sprintf(`/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s`, resourceGroupID, projectID)), + // func(req *http.Request) (*http.Response, error) { + // return httpmock.NewJsonResponse(200, map[string]interface{}{ + // "id": projectID, + // "name": projectName, + // "resource_group": resourceGroupID, + // "team": "httpmock-test-team", + // "ssh_certificate_type": "CERT_TYPE_ED25519_01", + // }) + // }, + // ) // Add DELETE responders httpmock.RegisterRegexpResponder("DELETE", @@ -202,7 +214,7 @@ func SetupDefaultMockResponders(groupName string, resourceGroupName string, proj ) } -func generateUUID(name string) string { - id := uuid.NewSHA1(uuid.NameSpaceOID, []byte(name)) - return id.String() -} +// func generateUUID(name string) string { +// id := uuid.NewSHA1(uuid.NameSpaceOID, []byte(name)) +// return id.String() +// } diff --git a/oktapam/resource_okta_universal_directory_checkout_settings.go b/oktapam/resource_okta_universal_directory_checkout_settings.go index bfad7e58a8..f7cba1d7f5 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings.go @@ -3,6 +3,7 @@ package oktapam import ( "context" "fmt" + "log" "github.com/okta/terraform-provider-oktapam/oktapam/convert" @@ -83,6 +84,7 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Create(ctx context.Cont checkoutSettings = *settings } + log.Printf("[DEBUG] Create Updating Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { resp.Diagnostics.AddError("Error creating Okta UD checkout settings", err.Error()) return @@ -103,6 +105,7 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Read(ctx context.Contex return } + log.Printf("[DEBUG] Read Reading Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, state.ResourceGroup, state.Project) if checkoutSettings, _, err := r.api.FetchResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, state.ResourceGroup, state.Project).Execute(); err != nil { resp.Diagnostics.AddError( "Error reading Okta UD checkout settings", @@ -121,6 +124,8 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Read(ctx context.Contex } } + log.Printf("[DEBUG] Read Setting state for Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, state.ResourceGroup, state.Project) + log.Printf("settingsModel: %+v", state) if diags := resp.State.Set(ctx, state); diags.HasError() { resp.Diagnostics.Append(diags...) return @@ -142,11 +147,13 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Update(ctx context.Cont checkoutSettings = *settings } + log.Printf("[DEBUG] Update Updating Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { resp.Diagnostics.AddError("Error updating Okta UD checkout settings", err.Error()) return } + log.Printf("[DEBUG] Update Reading Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) if updatedSettings, _, err := r.api.FetchResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).Execute(); err != nil { resp.Diagnostics.AddError( "Error reading Okta UD checkout settings", @@ -157,10 +164,13 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Update(ctx context.Cont err.Error())) return } else { + if settingsModel, diags := convert.ServiceAccountCheckoutSettingsFromSDKToModel(ctx, updatedSettings); diags.HasError() { resp.Diagnostics.Append(diags...) return } else { + log.Printf("[DEBUG]Update Setting state for Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) + log.Printf("settingsModel: %+v", settingsModel) plan.ServiceAccountCheckoutSettingsModel = *settingsModel } } diff --git a/oktapam/resource_okta_universal_directory_checkout_settings_test.go b/oktapam/resource_okta_universal_directory_checkout_settings_test.go index 7e4dabfc6e..6f899d27f0 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings_test.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings_test.go @@ -4,11 +4,14 @@ import ( "context" "encoding/json" "fmt" + "log" "net/http" "regexp" + "sync" "testing" "github.com/atko-pam/pam-sdk-go/client/pam" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/jarcoal/httpmock" @@ -53,11 +56,6 @@ resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_uni service_account_user_name = "user1", saas_app_instance_name = "app1" }, - { - id = "account2", - service_account_user_name = "user2", - saas_app_instance_name = "app2" - } ] } ` @@ -73,11 +71,6 @@ resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_uni id = "account3", service_account_user_name = "user3", saas_app_instance_name = "app3" - }, - { - id = "account4", - service_account_user_name = "user4", - saas_app_instance_name = "app4" } ] } @@ -106,135 +99,312 @@ resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_uni } ` -func TestAccOktaUDCheckoutSettings(t *testing.T) { - checkTeamApplicable(t, true) - resourceName := "oktapam_okta_universal_directory_checkout_settings.test_acc_okta_universal_directory_checkout_settings" - resourceGroupName := fmt.Sprintf("test_acc_resource_group_%s", randSeq()) - projectName := fmt.Sprintf("test_acc_resource_group_project_%s", randSeq()) - delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_dga_%s", randSeq()) - defaultCheckoutDuration := int32(900) +// func TestAccOktaUDCheckoutSettings(t *testing.T) { +// checkTeamApplicable(t, true) +// resourceName := "oktapam_okta_universal_directory_checkout_settings.test_acc_okta_universal_directory_checkout_settings" +// resourceGroupName := fmt.Sprintf("test_acc_resource_group_%s", randSeq()) +// projectName := fmt.Sprintf("test_acc_resource_group_project_%s", randSeq()) +// delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_dga_%s", randSeq()) +// defaultCheckoutDuration := int32(900) - initialSettings := &pam.APIServiceAccountCheckoutSettings{ - CheckoutRequired: true, - CheckoutDurationInSeconds: defaultCheckoutDuration, - } +// initialSettings := &pam.APIServiceAccountCheckoutSettings{ +// CheckoutRequired: true, +// CheckoutDurationInSeconds: defaultCheckoutDuration, +// } - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV6ProviderFactories: testAccV6ProviderFactories(), - Steps: []resource.TestStep{ - { - Config: createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), - Check: resource.ComposeAggregateTestCheckFunc( - testAccOktaUDCheckoutSettingsCheckExists(resourceName, initialSettings), - ), - }, - { - Config: createOktaUDCheckoutSettingsUpdateWithBothListsConfig(delegatedAdminGroupName, resourceGroupName, projectName), - ExpectError: regexp.MustCompile(`Only one of 'IncludeList' or 'ExcludeList' can be specified`), - }, - }, - }) -} +// resource.Test(t, resource.TestCase{ +// PreCheck: func() { testAccPreCheck(t) }, +// ProtoV6ProviderFactories: testAccV6ProviderFactories(), +// Steps: []resource.TestStep{ +// { +// Config: createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), +// Check: resource.ComposeAggregateTestCheckFunc( +// testAccOktaUDCheckoutSettingsCheckExists(resourceName, initialSettings), +// ), +// }, +// { +// Config: createOktaUDCheckoutSettingsUpdateWithBothListsConfig(delegatedAdminGroupName, resourceGroupName, projectName), +// ExpectError: regexp.MustCompile(`Only one of 'IncludeList' or 'ExcludeList' can be specified`), +// }, +// // Delete Okta UD checkout settings resource +// { +// Config: createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName), +// Check: testAccOktaUDCheckoutSettingsCheckDeleted(resourceName), +// }, +// // Destroy all resources +// { +// Config: `{}`, +// Check: testAccOktaUDCheckoutSettingsCheckDeleted(resourceName), +// }, +// }, +// }) +// } // TestAccOktaUDCheckoutSettingsWithMockHTTPClient is a test that uses httpmock to mock the HTTP client // and test the Okta UD checkout settings resource marshalling and unmarshalling correctly. func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { - // Enable debug logging for httpmock - httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { - t.Logf("[DEBUG] No responder found for: %s %s", req.Method, req.URL) - return nil, fmt.Errorf("no responder found for: %s %s", req.Method, req.URL) - }) - + // Use fixed names and IDs for consistency resourceName := "oktapam_okta_universal_directory_checkout_settings.test_acc_okta_universal_directory_checkout_settings" - resourceGroupName := fmt.Sprintf("test_acc_resource_group_%s", randSeq()) - projectName := fmt.Sprintf("test_acc_resource_group_project_%s", randSeq()) - delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_dga_%s", randSeq()) - user1 := "user1" - app1 := "app1" - user3 := "user3" - app3 := "app3" + resourceGroupName := fmt.Sprintf("test_acc_resource_mock_group_%s", randSeq()) + projectName := fmt.Sprintf("test_acc_resource_group_mock_project_%s", randSeq()) + delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_mock_dga_%s", randSeq()) + + // deleteSettings := &pam.APIServiceAccountCheckoutSettings{ + // CheckoutRequired: false, + // CheckoutDurationInSeconds: int32(900), + // } + + // Fixed test data + // user1 := "user1" + // app1 := "app1" + // user3 := "user3" + // app3 := "app3" // Setup httpmock httpmock.Activate() defer httpmock.DeactivateAndReset() - SetupDefaultMockResponders(delegatedAdminGroupName, resourceGroupName, projectName) + // Setup mock responders with fixed IDs + groupID := uuid.New().String() + resourceGroupID := uuid.New().String() + projectID := uuid.New().String() - // Mock the PUT endpoint for update operations - httpmock.RegisterResponder("PUT", - fmt.Sprintf("/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s/okta_universal_directory_checkout_settings", - resourceGroupName, projectName), + if resourceGroupID == projectID { + log.Printf("[DEBUG] Group ID: %s, Resource Group ID: %s, Project ID: %s", groupID, resourceGroupID, projectID) + panic("Resource Group ID and Project ID are the same") + } + + // Setup mock responders with consistent IDs and names + httpmock.RegisterRegexpResponder("POST", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects`), func(req *http.Request) (*http.Response, error) { - var requestBody pam.APIServiceAccountCheckoutSettings - if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { - return httpmock.NewStringResponse(400, ""), nil + var requestBody struct { + Name string `json:"name"` } - return httpmock.NewJsonResponse(204, nil) + json.NewDecoder(req.Body).Decode(&requestBody) + + // Return consistent project ID and name + return httpmock.NewJsonResponse(201, map[string]interface{}{ + "id": projectID, + "name": projectName, // Use the same name throughout + "resource_group": resourceGroupID, + "team": "httpmock-test-team", + "ssh_certificate_type": "CERT_TYPE_ED25519_01", + "account_discovery": true, + }) + }, + ) + + // Mock GET for project + httpmock.RegisterRegexpResponder("GET", + regexp.MustCompile(fmt.Sprintf(`/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s`, resourceGroupID, projectID)), + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, map[string]interface{}{ + "id": projectID, + "name": projectName, + "resource_group": resourceGroupID, + "team": "httpmock-test-team", + "ssh_certificate_type": "CERT_TYPE_ED25519_01", + "account_discovery": true, + }) }, ) + // Create a map to store entities with mutex for thread safety + var entitiesLock sync.RWMutex + entities := make(map[string]*pam.APIServiceAccountCheckoutSettings) + + // Initialize with default settings + initialSettings := &pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: true, + CheckoutDurationInSeconds: int32(900), + IncludeList: []pam.ServiceAccountSettingNameObject{}, + ExcludeList: []pam.ServiceAccountSettingNameObject{}, + } + + // Store initial settings + entityKey := fmt.Sprintf("%s/%s", resourceGroupID, projectID) + entities[entityKey] = initialSettings + // Mock the GET endpoint for read operations - httpmock.RegisterResponder("GET", - fmt.Sprintf("/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s/okta_universal_directory_checkout_settings", - resourceGroupName, projectName), + httpmock.RegisterRegexpResponder("GET", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*/okta_universal_directory_checkout_settings`), func(req *http.Request) (*http.Response, error) { - if httpmock.GetCallCountInfo()["GET"]%2 == 0 { - // Return include list settings for the first call - return httpmock.NewJsonResponse(200, pam.APIServiceAccountCheckoutSettings{ - CheckoutRequired: true, - CheckoutDurationInSeconds: 3600, - IncludeList: []pam.ServiceAccountSettingNameObject{ - { - Id: "account1", - ServiceAccountUserName: &user1, - SaasAppInstanceName: &app1, - }, - }, - }) - } else { - // Return exclude list settings for the second call - return httpmock.NewJsonResponse(200, pam.APIServiceAccountCheckoutSettings{ - CheckoutRequired: true, - CheckoutDurationInSeconds: 3600, - ExcludeList: []pam.ServiceAccountSettingNameObject{ - { - Id: "account3", - ServiceAccountUserName: &user3, - SaasAppInstanceName: &app3, - }, - }, - }) + log.Printf("[DEBUG] Mock GET request received: %s", req.URL.String()) + + // Extract IDs from URL path + matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects/([^/]+)/`).FindStringSubmatch(req.URL.Path) + if len(matches) != 3 { + log.Printf("[ERROR] Invalid URL format in GET request: %s", req.URL.Path) + return httpmock.NewStringResponse(400, "Invalid URL"), nil } + resourceGroupID := matches[1] + projectID := matches[2] + + entityKey := fmt.Sprintf("%s/%s", resourceGroupID, projectID) + log.Printf("[DEBUG] Looking up settings for key: %s", entityKey) + + // Return the stored settings if they exist + entitiesLock.RLock() + settings, exists := entities[entityKey] + entitiesLock.RUnlock() + + if exists { + // Create response with all required fields + response := map[string]interface{}{ + "id": entityKey, + "resource_group": resourceGroupID, + "project": projectID, + "checkout_required": settings.CheckoutRequired, + "checkout_duration_in_seconds": settings.CheckoutDurationInSeconds, + "include_list": settings.IncludeList, + "exclude_list": settings.ExcludeList, + } + log.Printf("[DEBUG] Found stored settings for key %s: %+v", entityKey, response) + return httpmock.NewJsonResponse(200, response) + } + + // If no settings exist yet, return default settings with all fields + defaultResponse := map[string]interface{}{ + "id": entityKey, + "resource_group": resourceGroupID, + "project": projectID, + "checkout_required": true, + "checkout_duration_in_seconds": int32(900), + "include_list": []pam.ServiceAccountSettingNameObject{}, + "exclude_list": []pam.ServiceAccountSettingNameObject{}, + } + log.Printf("[DEBUG] No stored settings found, returning default: %+v", defaultResponse) + return httpmock.NewJsonResponse(200, defaultResponse) + }, + ) + + // Mock the PUT endpoint for update operations + httpmock.RegisterRegexpResponder("PUT", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*/okta_universal_directory_checkout_settings`), + func(req *http.Request) (*http.Response, error) { + log.Printf("[DEBUG] Mock PUT request received: %s", req.URL.String()) + + // Extract IDs from URL path + matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects/([^/]+)/`).FindStringSubmatch(req.URL.Path) + if len(matches) != 3 { + return httpmock.NewStringResponse(400, "Invalid URL"), nil + } + resourceGroupID := matches[1] + projectID := matches[2] + + var settings pam.APIServiceAccountCheckoutSettings + if err := json.NewDecoder(req.Body).Decode(&settings); err != nil { + log.Printf("[ERROR] Failed to decode request body: %v", err) + return httpmock.NewStringResponse(400, ""), nil + } + + entityKey := fmt.Sprintf("%s/%s", resourceGroupID, projectID) + + // Store the settings in our entities map + entitiesLock.Lock() + entities[entityKey] = &settings + entitiesLock.Unlock() + + // Create response with all required fields + response := map[string]interface{}{ + "id": entityKey, + "resource_group": resourceGroupID, + "project": projectID, + "checkout_required": settings.CheckoutRequired, + "checkout_duration_in_seconds": settings.CheckoutDurationInSeconds, + "include_list": settings.IncludeList, + "exclude_list": settings.ExcludeList, + } + + log.Printf("[DEBUG] Stored settings for key %s: %+v", entityKey, response) + return httpmock.NewJsonResponse(200, response) }, ) + // // Mock the POST endpoint for create okta_universal_directory_checkout_settings operations + // httpmock.RegisterRegexpResponder("POST", + // regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*/okta_universal_directory_checkout_settings`), + // func(req *http.Request) (*http.Response, error) { + // return httpmock.NewJsonResponse(201, map[string]interface{}{ + // "id": projectID, + // "name": projectName, + // "resource_group": resourceGroupID, + // "team": "httpmock-test-team", + // }) + // }, + // ) + + // Register these responders before SetupDefaultMockResponders + SetupDefaultMockResponders(groupID, resourceGroupID, projectID, delegatedAdminGroupName, resourceGroupName, projectName) + + // Add request counter + httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { + log.Printf("[ERROR] No responder found for %s %s", req.Method, req.URL.String()) + return httpmock.NewStringResponse(404, ""), nil + }) + + // Add cleanup step to print statistics + defer func() { + info := httpmock.GetCallCountInfo() + log.Printf("[DEBUG] Mock HTTP call count info: %v", info) + }() + resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { + testAccPreCheck(t) + log.Printf("[DEBUG] Starting test with empty entities map") + }, ProtoV6ProviderFactories: httpMockTestV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: createOktaUDCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), - resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), - resource.TestCheckResourceAttr(resourceName, "include_list.#", "1"), - resource.TestCheckResourceAttr(resourceName, "include_list.0.id", "account1"), - resource.TestCheckResourceAttr(resourceName, "include_list.0.service_account_user_name", "user1"), - resource.TestCheckResourceAttr(resourceName, "include_list.0.saas_app_instance_name", "app1"), - ), - }, - { - Config: createOktaUDCheckoutSettingsUpdateWithExcludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Config: createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), - resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.#", "1"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.0.id", "account3"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.0.service_account_user_name", "user3"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.0.saas_app_instance_name", "app3"), + resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "900"), ), }, + // { + // PreConfig: func() { + // log.Printf("[DEBUG] Starting first test step") + // // Clear the entities map + // entitiesLock.Lock() + // for k := range entities { + // delete(entities, k) + // } + // entitiesLock.Unlock() + // }, + // Config: createOktaUDCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), + // Check: resource.ComposeAggregateTestCheckFunc( + // func(s *terraform.State) error { + // log.Printf("[DEBUG] Running Check function") + // return nil + // }, + // resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), + // resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), + // resource.TestCheckResourceAttr(resourceName, "include_list.#", "1"), + // resource.TestCheckResourceAttr(resourceName, "include_list.0.id", "account1"), + // resource.TestCheckResourceAttr(resourceName, "include_list.0.service_account_user_name", "user1"), + // resource.TestCheckResourceAttr(resourceName, "include_list.0.saas_app_instance_name", "app1"), + // ), + // }, + // { + // PreConfig: func() { currentStep = 1 }, + // Config: createOktaUDCheckoutSettingsUpdateWithExcludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), + // Check: resource.ComposeAggregateTestCheckFunc( + // resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), + // resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), + // resource.TestCheckResourceAttr(resourceName, "exclude_list.#", "1"), + // resource.TestCheckResourceAttr(resourceName, "exclude_list.0.id", "account3"), + // resource.TestCheckResourceAttr(resourceName, "exclude_list.0.service_account_user_name", "user3"), + // resource.TestCheckResourceAttr(resourceName, "exclude_list.0.saas_app_instance_name", "app3"), + // ), + // }, + // { + // PreConfig: func() { currentStep = 2 }, + // Config: createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName), + // Check: testAccOktaUDCheckoutSettingsCheckExists(resourceName, deleteSettings), + // }, }, }) } @@ -267,6 +437,16 @@ func testAccOktaUDCheckoutSettingsCheckExists(resourceName string, expected *pam } } +func testAccOktaUDCheckoutSettingsCheckDeleted(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[resourceName] + if ok { + return fmt.Errorf("Okta Universal Directory checkout settings still exists: %s", resourceName) + } + return nil + } +} + func createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { return fmt.Sprintf(testAccOktaUDCheckoutSettingsBaseConfigFormat, delegatedAdminGroupName, resourceGroupName, projectName) } diff --git a/oktapam/resource_saas_app_checkout_settings_test.go b/oktapam/resource_saas_app_checkout_settings_test.go index 7acd553eb3..97166e01fd 100644 --- a/oktapam/resource_saas_app_checkout_settings_test.go +++ b/oktapam/resource_saas_app_checkout_settings_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/atko-pam/pam-sdk-go/client/pam" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/jarcoal/httpmock" @@ -152,7 +153,10 @@ func TestAccSaasAppCheckoutSettingsWithMockHTTPClient(t *testing.T) { // Setup httpmock httpmock.Activate() defer httpmock.DeactivateAndReset() - SetupDefaultMockResponders(delegatedAdminGroupName, resourceGroupName, projectName) + groupID, _ := uuid.NewUUID() + resourceGroupID, _ := uuid.NewUUID() + projectID, _ := uuid.NewUUID() + SetupDefaultMockResponders(groupID.String(), resourceGroupID.String(), projectID.String(), delegatedAdminGroupName, resourceGroupName, projectName) // Mock the PUT endpoint for update operations httpmock.RegisterResponder("PUT", From 70bc0abf5e2dcfd341f974b20dab79947679ece9 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Tue, 14 Jan 2025 11:58:55 -0800 Subject: [PATCH 03/13] some debug cleanup --- Makefile | 1 - .../service_account_checkout_settings.go | 4 +- oktapam/provider_httpmock.go | 110 +++++++----- ...a_universal_directory_checkout_settings.go | 25 ++- ...versal_directory_checkout_settings_test.go | 163 ++++++------------ 5 files changed, 127 insertions(+), 176 deletions(-) diff --git a/Makefile b/Makefile index 099bbbf64d..4801ad986f 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,6 @@ testacc: testaccpam: # TESTARGS here can be used to pass arbitrary flags to go test, e.g. '-run TestMyTest' TF_LOG=DEBUG TF_ACC=1 TF_ACC_PAM=1 go test ./... -v $(TESTARGS) -timeout 120m - # TF_LOG=DEBUG go test -v ./oktapam -run TestAccOktaUDCheckoutSettingsWithMockHTTPClient generate: diff --git a/oktapam/convert/service_account_checkout_settings.go b/oktapam/convert/service_account_checkout_settings.go index f46fd0a3f9..06954ac26b 100644 --- a/oktapam/convert/service_account_checkout_settings.go +++ b/oktapam/convert/service_account_checkout_settings.go @@ -130,11 +130,11 @@ func ServiceAccountCheckoutSettingsFromSDKToModel(ctx context.Context, in *pam.A var diags diag.Diagnostics if val, ok := in.GetCheckoutRequiredOk(); ok { - out.CheckoutRequired = types.BoolValue(*val) + out.CheckoutRequired = types.BoolPointerValue(val) } if val, ok := in.GetCheckoutDurationInSecondsOk(); ok { - out.CheckoutDurationInSeconds = types.Int32Value(*val) + out.CheckoutDurationInSeconds = types.Int32PointerValue(val) } includeList, d := types.ListValueFrom(ctx, types.ObjectType{ diff --git a/oktapam/provider_httpmock.go b/oktapam/provider_httpmock.go index 22a0e2396c..de43aa3df6 100644 --- a/oktapam/provider_httpmock.go +++ b/oktapam/provider_httpmock.go @@ -126,6 +126,7 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { return httpmock.NewStringResponse(400, fmt.Sprintf("Failed to decode: %v", err)), nil } + log.Printf("Create resource group for %s", resourceGroupID) return httpmock.NewJsonResponse(201, map[string]interface{}{ "id": resourceGroupID, "name": requestBody.Name, @@ -157,47 +158,69 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI }, ) - // // Project endpoints - // httpmock.RegisterRegexpResponder("POST", - // regexp.MustCompile(fmt.Sprintf(`/v1/teams/httpmock-test-team/resource_groups/%s/projects`, resourceGroupID)), - // func(req *http.Request) (*http.Response, error) { - // bodyBytes, _ := io.ReadAll(req.Body) - // req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) - - // var requestBody struct { - // Name string `json:"name"` - // ResourceGroup string `json:"resource_group"` - // SSHCertificateType string `json:"ssh_certificate_type"` - // AccountDiscovery bool `json:"account_discovery"` - // } - // json.NewDecoder(req.Body).Decode(&requestBody) - - // resp := map[string]interface{}{ - // "id": projectID, - // "name": "sdajklshrfikljaewhtruiahtoigfe", - // "resource_group": resourceGroupID, - // "team": "httpmock-test-team", - // "ssh_certificate_type": requestBody.SSHCertificateType, - // "account_discovery": requestBody.AccountDiscovery, - // } - // return httpmock.NewJsonResponse(201, resp) - // }, - // ) - - // httpmock.RegisterRegexpResponder("GET", - // regexp.MustCompile(fmt.Sprintf(`/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s`, resourceGroupID, projectID)), - // func(req *http.Request) (*http.Response, error) { - // return httpmock.NewJsonResponse(200, map[string]interface{}{ - // "id": projectID, - // "name": projectName, - // "resource_group": resourceGroupID, - // "team": "httpmock-test-team", - // "ssh_certificate_type": "CERT_TYPE_ED25519_01", - // }) - // }, - // ) - - // Add DELETE responders + // Project endpoints + httpmock.RegisterRegexpResponder("POST", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects`), + func(req *http.Request) (*http.Response, error) { + // Extract resource group ID from URL + matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects`).FindStringSubmatch(req.URL.Path) + if len(matches) != 2 { + return httpmock.NewStringResponse(400, "Invalid URL format"), nil + } + actualResourceGroupID := matches[1] + + // Parse request body + var requestBody struct { + Name string `json:"name"` + SSHCertificateType string `json:"ssh_certificate_type"` + AccountDiscovery bool `json:"account_discovery"` + GatewaySelector string `json:"gateway_selector,omitempty"` + } + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + return httpmock.NewStringResponse(400, fmt.Sprintf("Failed to decode: %v", err)), nil + } + log.Printf("Create project for %s", projectID) + return httpmock.NewJsonResponse(201, map[string]interface{}{ + "id": projectID, + "name": requestBody.Name, + "resource_group": actualResourceGroupID, + "team": "httpmock-test-team", + "ssh_certificate_type": requestBody.SSHCertificateType, + "account_discovery": requestBody.AccountDiscovery, + "gateway_selector": requestBody.GatewaySelector, + }) + }, + ) + + httpmock.RegisterRegexpResponder("GET", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*`), + func(req *http.Request) (*http.Response, error) { + // Extract both IDs from URL + matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects/([^/]+)`).FindStringSubmatch(req.URL.Path) + if len(matches) != 3 { + return httpmock.NewStringResponse(404, "Project not found"), nil + } + actualResourceGroupID := matches[1] + actualProjectID := matches[2] + + // Verify IDs match expected values + if actualResourceGroupID != resourceGroupID || actualProjectID != projectID { + return httpmock.NewStringResponse(404, "Project not found"), nil + } + log.Printf("Get project for %s", actualProjectID) + return httpmock.NewJsonResponse(200, map[string]interface{}{ + "id": actualProjectID, + "name": projectName, + "resource_group": actualResourceGroupID, + "team": "httpmock-test-team", + "ssh_certificate_type": "CERT_TYPE_ED25519_01", + "account_discovery": true, + "deleted_at": nil, + }) + }, + ) + + // DELETE responders httpmock.RegisterRegexpResponder("DELETE", regexp.MustCompile(`/v1/teams/httpmock-test-team/groups/.*`), httpmock.NewStringResponder(204, ""), @@ -213,8 +236,3 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI httpmock.NewStringResponder(204, ""), ) } - -// func generateUUID(name string) string { -// id := uuid.NewSHA1(uuid.NameSpaceOID, []byte(name)) -// return id.String() -// } diff --git a/oktapam/resource_okta_universal_directory_checkout_settings.go b/oktapam/resource_okta_universal_directory_checkout_settings.go index f7cba1d7f5..623548a5c5 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings.go @@ -84,9 +84,9 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Create(ctx context.Cont checkoutSettings = *settings } - log.Printf("[DEBUG] Create Updating Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) + log.Printf("[DEBUG] Create Updating Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { - resp.Diagnostics.AddError("Error creating Okta UD checkout settings", err.Error()) + resp.Diagnostics.AddError("Error creating Okta Universal Directory checkout settings", err.Error()) return } @@ -108,8 +108,8 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Read(ctx context.Contex log.Printf("[DEBUG] Read Reading Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, state.ResourceGroup, state.Project) if checkoutSettings, _, err := r.api.FetchResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, state.ResourceGroup, state.Project).Execute(); err != nil { resp.Diagnostics.AddError( - "Error reading Okta UD checkout settings", - fmt.Sprintf("Could not read Okta UD checkout settings for team: %q resource_group: %q project_id: %q: Error: %s", + "Error reading Okta Universal Directory checkout settings", + fmt.Sprintf("Could not read Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q: Error: %s", r.teamName, state.ResourceGroup, state.Project, @@ -124,7 +124,7 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Read(ctx context.Contex } } - log.Printf("[DEBUG] Read Setting state for Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, state.ResourceGroup, state.Project) + log.Printf("[DEBUG] Read Setting state for Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, state.ResourceGroup, state.Project) log.Printf("settingsModel: %+v", state) if diags := resp.State.Set(ctx, state); diags.HasError() { resp.Diagnostics.Append(diags...) @@ -147,17 +147,17 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Update(ctx context.Cont checkoutSettings = *settings } - log.Printf("[DEBUG] Update Updating Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) + log.Printf("[DEBUG] Update Updating Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { - resp.Diagnostics.AddError("Error updating Okta UD checkout settings", err.Error()) + resp.Diagnostics.AddError("Error updating Okta Universal Directory checkout settings", err.Error()) return } - log.Printf("[DEBUG] Update Reading Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) + log.Printf("[DEBUG] Update Reading Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) if updatedSettings, _, err := r.api.FetchResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).Execute(); err != nil { resp.Diagnostics.AddError( - "Error reading Okta UD checkout settings", - fmt.Sprintf("Could not read Okta UD checkout settings for team: %q resource_group: %q project_id: %q: Error: %s", + "Error reading Okta Universal Directory checkout settings", + fmt.Sprintf("Could not read Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q: Error: %s", r.teamName, plan.ResourceGroup, plan.Project, @@ -169,8 +169,7 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Update(ctx context.Cont resp.Diagnostics.Append(diags...) return } else { - log.Printf("[DEBUG]Update Setting state for Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) - log.Printf("settingsModel: %+v", settingsModel) + log.Printf("[DEBUG]Update Setting state for Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) plan.ServiceAccountCheckoutSettingsModel = *settingsModel } } @@ -197,7 +196,7 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Delete(ctx context.Cont } if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, state.ResourceGroup, state.Project).APIServiceAccountCheckoutSettings(*checkoutSettings).Execute(); err != nil { - resp.Diagnostics.AddError("Error resetting Okta UD checkout settings", err.Error()) + resp.Diagnostics.AddError("Error resetting Okta Universal Directory checkout settings", err.Error()) return } diff --git a/oktapam/resource_okta_universal_directory_checkout_settings_test.go b/oktapam/resource_okta_universal_directory_checkout_settings_test.go index 6f899d27f0..8f121855f0 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings_test.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings_test.go @@ -13,11 +13,28 @@ import ( "github.com/atko-pam/pam-sdk-go/client/pam" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/jarcoal/httpmock" "github.com/okta/terraform-provider-oktapam/oktapam/constants/attributes" ) +var _ plancheck.PlanCheck = debugPlan{} + +type debugPlan struct{} + +func (e debugPlan) CheckPlan(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { + rd, err := json.Marshal(req.Plan) + if err != nil { + fmt.Println("error marshalling machine-readable plan output:", err) + } + fmt.Printf("req.Plan - %s\n", string(rd)) +} + +func DebugPlan() plancheck.PlanCheck { + return debugPlan{} +} + const testAccOktaUDCheckoutSettingsBaseConfigFormat = ` resource "oktapam_group" "test_resource_group_dga_group" { name = "%s" @@ -145,20 +162,9 @@ resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_uni func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { // Use fixed names and IDs for consistency resourceName := "oktapam_okta_universal_directory_checkout_settings.test_acc_okta_universal_directory_checkout_settings" - resourceGroupName := fmt.Sprintf("test_acc_resource_mock_group_%s", randSeq()) - projectName := fmt.Sprintf("test_acc_resource_group_mock_project_%s", randSeq()) - delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_mock_dga_%s", randSeq()) - - // deleteSettings := &pam.APIServiceAccountCheckoutSettings{ - // CheckoutRequired: false, - // CheckoutDurationInSeconds: int32(900), - // } - - // Fixed test data - // user1 := "user1" - // app1 := "app1" - // user3 := "user3" - // app3 := "app3" + resourceGroupName := fmt.Sprintf("test_acc_mock_resource_group_%s", randSeq()) + projectName := fmt.Sprintf("test_acc_mock_project_%s", randSeq()) + delegatedAdminGroupName := fmt.Sprintf("test_acc_mock_dga_%s", randSeq()) // Setup httpmock httpmock.Activate() @@ -166,49 +172,11 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { // Setup mock responders with fixed IDs groupID := uuid.New().String() - resourceGroupID := uuid.New().String() - projectID := uuid.New().String() - - if resourceGroupID == projectID { - log.Printf("[DEBUG] Group ID: %s, Resource Group ID: %s, Project ID: %s", groupID, resourceGroupID, projectID) - panic("Resource Group ID and Project ID are the same") - } - - // Setup mock responders with consistent IDs and names - httpmock.RegisterRegexpResponder("POST", - regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects`), - func(req *http.Request) (*http.Response, error) { - var requestBody struct { - Name string `json:"name"` - } - json.NewDecoder(req.Body).Decode(&requestBody) - - // Return consistent project ID and name - return httpmock.NewJsonResponse(201, map[string]interface{}{ - "id": projectID, - "name": projectName, // Use the same name throughout - "resource_group": resourceGroupID, - "team": "httpmock-test-team", - "ssh_certificate_type": "CERT_TYPE_ED25519_01", - "account_discovery": true, - }) - }, - ) - - // Mock GET for project - httpmock.RegisterRegexpResponder("GET", - regexp.MustCompile(fmt.Sprintf(`/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s`, resourceGroupID, projectID)), - func(req *http.Request) (*http.Response, error) { - return httpmock.NewJsonResponse(200, map[string]interface{}{ - "id": projectID, - "name": projectName, - "resource_group": resourceGroupID, - "team": "httpmock-test-team", - "ssh_certificate_type": "CERT_TYPE_ED25519_01", - "account_discovery": true, - }) - }, - ) + // resourceGroupID := uuid.New().String() + // projectID := uuid.New().String() + // Use fixed IDs for consistency + resourceGroupID := "0c6194ea-a60f-4a0e-b98b-aabdbae3db8c" + projectID := "92a42895-7da4-42f2-b045-2fd531c04d0c" // Different ID for project // Create a map to store entities with mutex for thread safety var entitiesLock sync.RWMutex @@ -249,33 +217,18 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { settings, exists := entities[entityKey] entitiesLock.RUnlock() - if exists { - // Create response with all required fields - response := map[string]interface{}{ - "id": entityKey, - "resource_group": resourceGroupID, - "project": projectID, - "checkout_required": settings.CheckoutRequired, - "checkout_duration_in_seconds": settings.CheckoutDurationInSeconds, - "include_list": settings.IncludeList, - "exclude_list": settings.ExcludeList, + if !exists { + // Return default settings if none exist + defaultSettings := &pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: false, + CheckoutDurationInSeconds: int32(0), + IncludeList: []pam.ServiceAccountSettingNameObject{}, + ExcludeList: []pam.ServiceAccountSettingNameObject{}, } - log.Printf("[DEBUG] Found stored settings for key %s: %+v", entityKey, response) - return httpmock.NewJsonResponse(200, response) + return httpmock.NewJsonResponse(200, defaultSettings) } - // If no settings exist yet, return default settings with all fields - defaultResponse := map[string]interface{}{ - "id": entityKey, - "resource_group": resourceGroupID, - "project": projectID, - "checkout_required": true, - "checkout_duration_in_seconds": int32(900), - "include_list": []pam.ServiceAccountSettingNameObject{}, - "exclude_list": []pam.ServiceAccountSettingNameObject{}, - } - log.Printf("[DEBUG] No stored settings found, returning default: %+v", defaultResponse) - return httpmock.NewJsonResponse(200, defaultResponse) + return httpmock.NewJsonResponse(200, settings) }, ) @@ -306,44 +259,14 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { entities[entityKey] = &settings entitiesLock.Unlock() - // Create response with all required fields - response := map[string]interface{}{ - "id": entityKey, - "resource_group": resourceGroupID, - "project": projectID, - "checkout_required": settings.CheckoutRequired, - "checkout_duration_in_seconds": settings.CheckoutDurationInSeconds, - "include_list": settings.IncludeList, - "exclude_list": settings.ExcludeList, - } - - log.Printf("[DEBUG] Stored settings for key %s: %+v", entityKey, response) - return httpmock.NewJsonResponse(200, response) + log.Printf("[DEBUG] Stored settings for key %s: %+v", entityKey, settings) + return httpmock.NewJsonResponse(200, settings) }, ) - // // Mock the POST endpoint for create okta_universal_directory_checkout_settings operations - // httpmock.RegisterRegexpResponder("POST", - // regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*/okta_universal_directory_checkout_settings`), - // func(req *http.Request) (*http.Response, error) { - // return httpmock.NewJsonResponse(201, map[string]interface{}{ - // "id": projectID, - // "name": projectName, - // "resource_group": resourceGroupID, - // "team": "httpmock-test-team", - // }) - // }, - // ) - // Register these responders before SetupDefaultMockResponders SetupDefaultMockResponders(groupID, resourceGroupID, projectID, delegatedAdminGroupName, resourceGroupName, projectName) - // Add request counter - httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { - log.Printf("[ERROR] No responder found for %s %s", req.Method, req.URL.String()) - return httpmock.NewStringResponse(404, ""), nil - }) - // Add cleanup step to print statistics defer func() { info := httpmock.GetCallCountInfo() @@ -359,6 +282,13 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { Steps: []resource.TestStep{ { Config: createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), + // PlanOnly: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPreRefresh: []plancheck.PlanCheck{ + DebugPlan(), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "900"), @@ -452,7 +382,12 @@ func createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName string, reso } func createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { - return createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccOktaUDCheckoutSettingsCreateConfigFormat + combinedConfig := createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccOktaUDCheckoutSettingsCreateConfigFormat + log.Printf("[DEBUG] Combined config: %s", func() string { + pretty, _ := json.MarshalIndent(combinedConfig, "", " ") + return string(pretty) + }()) + return combinedConfig } func createOktaUDCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { From 9c056613d6302b17f0f24d2911d1efaa0273d684 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Tue, 14 Jan 2025 13:12:47 -0800 Subject: [PATCH 04/13] Refactor HTTP mock setup and enhance test coverage for Okta PAM resources - Cleaned up the HTTP mock responder setup in `SetupDefaultMockResponders` to improve readability and maintainability. - Updated regex patterns for various endpoints to ensure accurate request handling. - Enhanced test cases for `SaaS App` and `Okta Universal Directory` checkout settings, including improved mock data handling and validation. - Removed unnecessary debug logs and commented-out code to streamline the test files. - Ensured thread safety in mock data management by implementing mutex locks.. --- Makefile | 2 +- .../service_account_checkout_settings.go | 40 +++- oktapam/provider_httpmock.go | 46 ++--- ...versal_directory_checkout_settings_test.go | 195 ++++++------------ ...esource_saas_app_checkout_settings_test.go | 140 ++++++++----- 5 files changed, 203 insertions(+), 220 deletions(-) diff --git a/Makefile b/Makefile index 4801ad986f..f5d7192154 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ testacc: testaccpam: # TESTARGS here can be used to pass arbitrary flags to go test, e.g. '-run TestMyTest' - TF_LOG=DEBUG TF_ACC=1 TF_ACC_PAM=1 go test ./... -v $(TESTARGS) -timeout 120m + TF_ACC=1 TF_ACC_PAM=1 go test ./... -v $(TESTARGS) -timeout 120m generate: diff --git a/oktapam/convert/service_account_checkout_settings.go b/oktapam/convert/service_account_checkout_settings.go index 06954ac26b..c6350d7a97 100644 --- a/oktapam/convert/service_account_checkout_settings.go +++ b/oktapam/convert/service_account_checkout_settings.go @@ -137,31 +137,59 @@ func ServiceAccountCheckoutSettingsFromSDKToModel(ctx context.Context, in *pam.A out.CheckoutDurationInSeconds = types.Int32PointerValue(val) } - includeList, d := types.ListValueFrom(ctx, types.ObjectType{ + var includeList []ServiceAccountSettingNameObjectModel + for _, item := range in.IncludeList { + model := ServiceAccountSettingNameObjectModel{ + Id: item.Id, + } + if item.ServiceAccountUserName != nil { + model.ServiceAccountUserName = *item.ServiceAccountUserName + } + if item.SaasAppInstanceName != nil { + model.SaasAppInstanceName = *item.SaasAppInstanceName + } + includeList = append(includeList, model) + } + + includeListValue, d := types.ListValueFrom(ctx, types.ObjectType{ AttrTypes: map[string]attr.Type{ "id": types.StringType, "service_account_user_name": types.StringType, "saas_app_instance_name": types.StringType, }, - }, in.IncludeList) + }, includeList) diags.Append(d...) if diags.HasError() { return nil, diags } - out.IncludeList = includeList + out.IncludeList = includeListValue + + var excludeList []ServiceAccountSettingNameObjectModel + for _, item := range in.ExcludeList { + model := ServiceAccountSettingNameObjectModel{ + Id: item.Id, + } + if item.ServiceAccountUserName != nil { + model.ServiceAccountUserName = *item.ServiceAccountUserName + } + if item.SaasAppInstanceName != nil { + model.SaasAppInstanceName = *item.SaasAppInstanceName + } + excludeList = append(excludeList, model) + } - excludeList, d := types.ListValueFrom(ctx, types.ObjectType{ + excludeListValue, d := types.ListValueFrom(ctx, types.ObjectType{ AttrTypes: map[string]attr.Type{ "id": types.StringType, "service_account_user_name": types.StringType, "saas_app_instance_name": types.StringType, }, - }, in.ExcludeList) + }, excludeList) diags.Append(d...) if diags.HasError() { return nil, diags } - out.ExcludeList = excludeList + out.ExcludeList = excludeListValue return &out, diags } diff --git a/oktapam/provider_httpmock.go b/oktapam/provider_httpmock.go index de43aa3df6..c2b2b824c8 100644 --- a/oktapam/provider_httpmock.go +++ b/oktapam/provider_httpmock.go @@ -87,8 +87,7 @@ func httpMockClients() *client.APIClients { // SetupDefaultMockResponders configures common mock responses for standard resources func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectID string, groupName string, resourceGroupName string, projectName string) { // Group endpoints - httpmock.RegisterRegexpResponder("POST", - regexp.MustCompile(`/v1/teams/httpmock-test-team/groups`), + httpmock.RegisterRegexpResponder("POST", regexp.MustCompile(`/v1/teams/httpmock-test-team/groups`), func(req *http.Request) (*http.Response, error) { var requestBody map[string]interface{} if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { @@ -101,8 +100,7 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI }, ) - httpmock.RegisterRegexpResponder("GET", - regexp.MustCompile(`/v1/teams/httpmock-test-team/groups/.*`), + httpmock.RegisterRegexpResponder("GET", regexp.MustCompile(`/v1/teams/httpmock-test-team/groups/.*`), func(req *http.Request) (*http.Response, error) { return httpmock.NewJsonResponse(200, map[string]interface{}{ "id": groupID, @@ -112,10 +110,8 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI ) // Resource Group endpoints - httpmock.RegisterRegexpResponder("POST", - regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups`), + httpmock.RegisterRegexpResponder("POST", regexp.MustCompile(`^/v1/teams/httpmock-test-team/resource_groups$`), func(req *http.Request) (*http.Response, error) { - bodyBytes, _ := io.ReadAll(req.Body) req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) var requestBody struct { @@ -126,7 +122,6 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { return httpmock.NewStringResponse(400, fmt.Sprintf("Failed to decode: %v", err)), nil } - log.Printf("Create resource group for %s", resourceGroupID) return httpmock.NewJsonResponse(201, map[string]interface{}{ "id": resourceGroupID, "name": requestBody.Name, @@ -141,8 +136,7 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI }, ) - httpmock.RegisterRegexpResponder("GET", - regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*`), + httpmock.RegisterRegexpResponder("GET", regexp.MustCompile(`^/v1/teams/httpmock-test-team/resource_groups/[^/]+$`), func(req *http.Request) (*http.Response, error) { return httpmock.NewJsonResponse(200, map[string]interface{}{ "id": resourceGroupID, @@ -158,18 +152,15 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI }, ) - // Project endpoints - httpmock.RegisterRegexpResponder("POST", - regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects`), + // Resource Group Project endpoints + httpmock.RegisterRegexpResponder("POST", regexp.MustCompile(`^/v1/teams/httpmock-test-team/resource_groups/[^/]+/projects$`), func(req *http.Request) (*http.Response, error) { - // Extract resource group ID from URL matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects`).FindStringSubmatch(req.URL.Path) if len(matches) != 2 { return httpmock.NewStringResponse(400, "Invalid URL format"), nil } actualResourceGroupID := matches[1] - // Parse request body var requestBody struct { Name string `json:"name"` SSHCertificateType string `json:"ssh_certificate_type"` @@ -179,7 +170,6 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { return httpmock.NewStringResponse(400, fmt.Sprintf("Failed to decode: %v", err)), nil } - log.Printf("Create project for %s", projectID) return httpmock.NewJsonResponse(201, map[string]interface{}{ "id": projectID, "name": requestBody.Name, @@ -192,10 +182,8 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI }, ) - httpmock.RegisterRegexpResponder("GET", - regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*`), + httpmock.RegisterRegexpResponder("GET", regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*`), func(req *http.Request) (*http.Response, error) { - // Extract both IDs from URL matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects/([^/]+)`).FindStringSubmatch(req.URL.Path) if len(matches) != 3 { return httpmock.NewStringResponse(404, "Project not found"), nil @@ -203,11 +191,9 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI actualResourceGroupID := matches[1] actualProjectID := matches[2] - // Verify IDs match expected values if actualResourceGroupID != resourceGroupID || actualProjectID != projectID { return httpmock.NewStringResponse(404, "Project not found"), nil } - log.Printf("Get project for %s", actualProjectID) return httpmock.NewJsonResponse(200, map[string]interface{}{ "id": actualProjectID, "name": projectName, @@ -221,18 +207,12 @@ func SetupDefaultMockResponders(groupID string, resourceGroupID string, projectI ) // DELETE responders - httpmock.RegisterRegexpResponder("DELETE", - regexp.MustCompile(`/v1/teams/httpmock-test-team/groups/.*`), - httpmock.NewStringResponder(204, ""), - ) + httpmock.RegisterRegexpResponder("DELETE", regexp.MustCompile(`/v1/teams/httpmock-test-team/groups/.*`), + httpmock.NewStringResponder(204, "")) - httpmock.RegisterRegexpResponder("DELETE", - regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*`), - httpmock.NewStringResponder(204, ""), - ) + httpmock.RegisterRegexpResponder("DELETE", regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*`), + httpmock.NewStringResponder(204, "")) - httpmock.RegisterRegexpResponder("DELETE", - regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*`), - httpmock.NewStringResponder(204, ""), - ) + httpmock.RegisterRegexpResponder("DELETE", regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*`), + httpmock.NewStringResponder(204, "")) } diff --git a/oktapam/resource_okta_universal_directory_checkout_settings_test.go b/oktapam/resource_okta_universal_directory_checkout_settings_test.go index 8f121855f0..9ff6c555a2 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings_test.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "log" "net/http" "regexp" "sync" @@ -116,46 +115,41 @@ resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_uni } ` -// func TestAccOktaUDCheckoutSettings(t *testing.T) { -// checkTeamApplicable(t, true) -// resourceName := "oktapam_okta_universal_directory_checkout_settings.test_acc_okta_universal_directory_checkout_settings" -// resourceGroupName := fmt.Sprintf("test_acc_resource_group_%s", randSeq()) -// projectName := fmt.Sprintf("test_acc_resource_group_project_%s", randSeq()) -// delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_dga_%s", randSeq()) -// defaultCheckoutDuration := int32(900) - -// initialSettings := &pam.APIServiceAccountCheckoutSettings{ -// CheckoutRequired: true, -// CheckoutDurationInSeconds: defaultCheckoutDuration, -// } - -// resource.Test(t, resource.TestCase{ -// PreCheck: func() { testAccPreCheck(t) }, -// ProtoV6ProviderFactories: testAccV6ProviderFactories(), -// Steps: []resource.TestStep{ -// { -// Config: createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), -// Check: resource.ComposeAggregateTestCheckFunc( -// testAccOktaUDCheckoutSettingsCheckExists(resourceName, initialSettings), -// ), -// }, -// { -// Config: createOktaUDCheckoutSettingsUpdateWithBothListsConfig(delegatedAdminGroupName, resourceGroupName, projectName), -// ExpectError: regexp.MustCompile(`Only one of 'IncludeList' or 'ExcludeList' can be specified`), -// }, -// // Delete Okta UD checkout settings resource -// { -// Config: createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName), -// Check: testAccOktaUDCheckoutSettingsCheckDeleted(resourceName), -// }, -// // Destroy all resources -// { -// Config: `{}`, -// Check: testAccOktaUDCheckoutSettingsCheckDeleted(resourceName), -// }, -// }, -// }) -// } +func TestAccOktaUDCheckoutSettings(t *testing.T) { + checkTeamApplicable(t, true) + resourceName := "oktapam_okta_universal_directory_checkout_settings.test_acc_okta_universal_directory_checkout_settings" + resourceGroupName := fmt.Sprintf("test_acc_resource_group_%s", randSeq()) + projectName := fmt.Sprintf("test_acc_resource_group_project_%s", randSeq()) + delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_dga_%s", randSeq()) + defaultCheckoutDuration := int32(900) + + initialSettings := &pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: true, + CheckoutDurationInSeconds: defaultCheckoutDuration, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccOktaUDCheckoutSettingsCheckExists(resourceName, initialSettings), + ), + }, + { + Config: createOktaUDCheckoutSettingsUpdateWithBothListsConfig(delegatedAdminGroupName, resourceGroupName, projectName), + ExpectError: regexp.MustCompile(`Only one of 'IncludeList' or 'ExcludeList' can be specified`), + }, + // Delete Okta UD checkout settings resource + { + Config: createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Check: testAccOktaUDCheckoutSettingsCheckDeleted(resourceName), + }, + }, + }) +} // TestAccOktaUDCheckoutSettingsWithMockHTTPClient is a test that uses httpmock to mock the HTTP client // and test the Okta UD checkout settings resource marshalling and unmarshalling correctly. @@ -172,56 +166,34 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { // Setup mock responders with fixed IDs groupID := uuid.New().String() - // resourceGroupID := uuid.New().String() - // projectID := uuid.New().String() - // Use fixed IDs for consistency - resourceGroupID := "0c6194ea-a60f-4a0e-b98b-aabdbae3db8c" - projectID := "92a42895-7da4-42f2-b045-2fd531c04d0c" // Different ID for project + resourceGroupID := uuid.New().String() + projectID := uuid.New().String() // Create a map to store entities with mutex for thread safety var entitiesLock sync.RWMutex entities := make(map[string]*pam.APIServiceAccountCheckoutSettings) - // Initialize with default settings - initialSettings := &pam.APIServiceAccountCheckoutSettings{ - CheckoutRequired: true, - CheckoutDurationInSeconds: int32(900), - IncludeList: []pam.ServiceAccountSettingNameObject{}, - ExcludeList: []pam.ServiceAccountSettingNameObject{}, - } - - // Store initial settings - entityKey := fmt.Sprintf("%s/%s", resourceGroupID, projectID) - entities[entityKey] = initialSettings - - // Mock the GET endpoint for read operations httpmock.RegisterRegexpResponder("GET", regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*/okta_universal_directory_checkout_settings`), func(req *http.Request) (*http.Response, error) { - log.Printf("[DEBUG] Mock GET request received: %s", req.URL.String()) - - // Extract IDs from URL path matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects/([^/]+)/`).FindStringSubmatch(req.URL.Path) if len(matches) != 3 { - log.Printf("[ERROR] Invalid URL format in GET request: %s", req.URL.Path) + return httpmock.NewStringResponse(400, "Invalid URL"), nil } resourceGroupID := matches[1] projectID := matches[2] entityKey := fmt.Sprintf("%s/%s", resourceGroupID, projectID) - log.Printf("[DEBUG] Looking up settings for key: %s", entityKey) - // Return the stored settings if they exist entitiesLock.RLock() settings, exists := entities[entityKey] entitiesLock.RUnlock() if !exists { - // Return default settings if none exist defaultSettings := &pam.APIServiceAccountCheckoutSettings{ CheckoutRequired: false, - CheckoutDurationInSeconds: int32(0), + CheckoutDurationInSeconds: int32(900), IncludeList: []pam.ServiceAccountSettingNameObject{}, ExcludeList: []pam.ServiceAccountSettingNameObject{}, } @@ -232,11 +204,9 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { }, ) - // Mock the PUT endpoint for update operations httpmock.RegisterRegexpResponder("PUT", regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*/okta_universal_directory_checkout_settings`), func(req *http.Request) (*http.Response, error) { - log.Printf("[DEBUG] Mock PUT request received: %s", req.URL.String()) // Extract IDs from URL path matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects/([^/]+)/`).FindStringSubmatch(req.URL.Path) @@ -248,7 +218,7 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { var settings pam.APIServiceAccountCheckoutSettings if err := json.NewDecoder(req.Body).Decode(&settings); err != nil { - log.Printf("[ERROR] Failed to decode request body: %v", err) + return httpmock.NewStringResponse(400, ""), nil } @@ -259,30 +229,21 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { entities[entityKey] = &settings entitiesLock.Unlock() - log.Printf("[DEBUG] Stored settings for key %s: %+v", entityKey, settings) return httpmock.NewJsonResponse(200, settings) }, ) - // Register these responders before SetupDefaultMockResponders + // Register default responders for user group, resource group, resource group project SetupDefaultMockResponders(groupID, resourceGroupID, projectID, delegatedAdminGroupName, resourceGroupName, projectName) - // Add cleanup step to print statistics - defer func() { - info := httpmock.GetCallCountInfo() - log.Printf("[DEBUG] Mock HTTP call count info: %v", info) - }() - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) - log.Printf("[DEBUG] Starting test with empty entities map") }, ProtoV6ProviderFactories: httpMockTestV6ProviderFactories(), Steps: []resource.TestStep{ { Config: createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), - // PlanOnly: true, ConfigPlanChecks: resource.ConfigPlanChecks{ PostApplyPreRefresh: []plancheck.PlanCheck{ DebugPlan(), @@ -294,47 +255,32 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "900"), ), }, - // { - // PreConfig: func() { - // log.Printf("[DEBUG] Starting first test step") - // // Clear the entities map - // entitiesLock.Lock() - // for k := range entities { - // delete(entities, k) - // } - // entitiesLock.Unlock() - // }, - // Config: createOktaUDCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), - // Check: resource.ComposeAggregateTestCheckFunc( - // func(s *terraform.State) error { - // log.Printf("[DEBUG] Running Check function") - // return nil - // }, - // resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), - // resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), - // resource.TestCheckResourceAttr(resourceName, "include_list.#", "1"), - // resource.TestCheckResourceAttr(resourceName, "include_list.0.id", "account1"), - // resource.TestCheckResourceAttr(resourceName, "include_list.0.service_account_user_name", "user1"), - // resource.TestCheckResourceAttr(resourceName, "include_list.0.saas_app_instance_name", "app1"), - // ), - // }, - // { - // PreConfig: func() { currentStep = 1 }, - // Config: createOktaUDCheckoutSettingsUpdateWithExcludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), - // Check: resource.ComposeAggregateTestCheckFunc( - // resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), - // resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), - // resource.TestCheckResourceAttr(resourceName, "exclude_list.#", "1"), - // resource.TestCheckResourceAttr(resourceName, "exclude_list.0.id", "account3"), - // resource.TestCheckResourceAttr(resourceName, "exclude_list.0.service_account_user_name", "user3"), - // resource.TestCheckResourceAttr(resourceName, "exclude_list.0.saas_app_instance_name", "app3"), - // ), - // }, - // { - // PreConfig: func() { currentStep = 2 }, - // Config: createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName), - // Check: testAccOktaUDCheckoutSettingsCheckExists(resourceName, deleteSettings), - // }, + { + Config: createOktaUDCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + func(s *terraform.State) error { + return nil + }, + resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), + resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), + resource.TestCheckResourceAttr(resourceName, "include_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "include_list.0.id", "account1"), + resource.TestCheckResourceAttr(resourceName, "include_list.0.service_account_user_name", "user1"), + resource.TestCheckResourceAttr(resourceName, "include_list.0.saas_app_instance_name", "app1"), + ), + }, + { + + Config: createOktaUDCheckoutSettingsUpdateWithExcludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), + resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0.id", "account3"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0.service_account_user_name", "user3"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0.saas_app_instance_name", "app3"), + ), + }, }, }) } @@ -382,12 +328,7 @@ func createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName string, reso } func createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { - combinedConfig := createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccOktaUDCheckoutSettingsCreateConfigFormat - log.Printf("[DEBUG] Combined config: %s", func() string { - pretty, _ := json.MarshalIndent(combinedConfig, "", " ") - return string(pretty) - }()) - return combinedConfig + return createOktaUDCheckoutSettingsBaseConfig(delegatedAdminGroupName, resourceGroupName, projectName) + testAccOktaUDCheckoutSettingsCreateConfigFormat } func createOktaUDCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName string, resourceGroupName string, projectName string) string { diff --git a/oktapam/resource_saas_app_checkout_settings_test.go b/oktapam/resource_saas_app_checkout_settings_test.go index 97166e01fd..80bde62533 100644 --- a/oktapam/resource_saas_app_checkout_settings_test.go +++ b/oktapam/resource_saas_app_checkout_settings_test.go @@ -6,11 +6,13 @@ import ( "fmt" "net/http" "regexp" + "sync" "testing" "github.com/atko-pam/pam-sdk-go/client/pam" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/jarcoal/httpmock" "github.com/okta/terraform-provider-oktapam/oktapam/constants/attributes" @@ -141,84 +143,113 @@ func TestAccSaasAppCheckoutSettings(t *testing.T) { // TestAccSaasAppCheckoutSettingsWithMockHTTPClient is a test that uses httpmock to mock the HTTP client // and test the SaaS App checkout settings resource marshalling and unmarshalling correctly. func TestAccSaasAppCheckoutSettingsWithMockHTTPClient(t *testing.T) { + // Use fixed names and IDs for consistency resourceName := "oktapam_saas_app_checkout_settings.test_acc_saas_app_checkout_settings" - resourceGroupName := fmt.Sprintf("test_acc_resource_group_%s", randSeq()) - projectName := fmt.Sprintf("test_acc_resource_group_project_%s", randSeq()) - delegatedAdminGroupName := fmt.Sprintf("test_acc_resource_group_dga_%s", randSeq()) - user1 := "user1" - app1 := "app1" - user3 := "user3" - app3 := "app3" + resourceGroupName := fmt.Sprintf("test_acc_mock_resource_group_%s", randSeq()) + projectName := fmt.Sprintf("test_acc_mock_project_%s", randSeq()) + delegatedAdminGroupName := fmt.Sprintf("test_acc_mock_dga_%s", randSeq()) // Setup httpmock httpmock.Activate() defer httpmock.DeactivateAndReset() - groupID, _ := uuid.NewUUID() - resourceGroupID, _ := uuid.NewUUID() - projectID, _ := uuid.NewUUID() - SetupDefaultMockResponders(groupID.String(), resourceGroupID.String(), projectID.String(), delegatedAdminGroupName, resourceGroupName, projectName) - - // Mock the PUT endpoint for update operations - httpmock.RegisterResponder("PUT", - fmt.Sprintf("/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s/saas_app_checkout_settings", - resourceGroupName, projectName), + + // Setup mock responders with fixed IDs + groupID := uuid.New().String() + resourceGroupID := uuid.New().String() + projectID := uuid.New().String() + + // Create a map to store entities with mutex for thread safety + var entitiesLock sync.RWMutex + entities := make(map[string]*pam.APIServiceAccountCheckoutSettings) + + httpmock.RegisterRegexpResponder("GET", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*/saas_app_checkout_settings`), func(req *http.Request) (*http.Response, error) { - var requestBody pam.APIServiceAccountCheckoutSettings - if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { - return httpmock.NewStringResponse(400, ""), nil + matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects/([^/]+)/`).FindStringSubmatch(req.URL.Path) + if len(matches) != 3 { + return httpmock.NewStringResponse(400, "Invalid URL"), nil } - return httpmock.NewJsonResponse(204, nil) + resourceGroupID := matches[1] + projectID := matches[2] + + entityKey := fmt.Sprintf("%s/%s", resourceGroupID, projectID) + + entitiesLock.RLock() + settings, exists := entities[entityKey] + entitiesLock.RUnlock() + + if !exists { + defaultSettings := &pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: false, + CheckoutDurationInSeconds: int32(900), + IncludeList: []pam.ServiceAccountSettingNameObject{}, + ExcludeList: []pam.ServiceAccountSettingNameObject{}, + } + return httpmock.NewJsonResponse(200, defaultSettings) + } + + return httpmock.NewJsonResponse(200, settings) }, ) - // Mock the GET endpoint for read operations - httpmock.RegisterResponder("GET", - fmt.Sprintf("/v1/teams/httpmock-test-team/resource_groups/%s/projects/%s/saas_app_checkout_settings", - resourceGroupName, projectName), + httpmock.RegisterRegexpResponder("PUT", + regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*/saas_app_checkout_settings`), func(req *http.Request) (*http.Response, error) { - if httpmock.GetCallCountInfo()["GET"]%2 == 0 { - // Return include list settings for the first call - return httpmock.NewJsonResponse(200, pam.APIServiceAccountCheckoutSettings{ - CheckoutRequired: true, - CheckoutDurationInSeconds: 3600, - IncludeList: []pam.ServiceAccountSettingNameObject{ - { - Id: "account1", - ServiceAccountUserName: &user1, - SaasAppInstanceName: &app1, - }, - }, - }) - } else { - // Return exclude list settings for the second call - return httpmock.NewJsonResponse(200, pam.APIServiceAccountCheckoutSettings{ - CheckoutRequired: true, - CheckoutDurationInSeconds: 3600, - ExcludeList: []pam.ServiceAccountSettingNameObject{ - { - Id: "account3", - ServiceAccountUserName: &user3, - SaasAppInstanceName: &app3, - }, - }, - }) + matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects/([^/]+)/`).FindStringSubmatch(req.URL.Path) + if len(matches) != 3 { + return httpmock.NewStringResponse(400, "Invalid URL"), nil + } + resourceGroupID := matches[1] + projectID := matches[2] + + var settings pam.APIServiceAccountCheckoutSettings + if err := json.NewDecoder(req.Body).Decode(&settings); err != nil { + return httpmock.NewStringResponse(400, ""), nil } + + entityKey := fmt.Sprintf("%s/%s", resourceGroupID, projectID) + + entitiesLock.Lock() + entities[entityKey] = &settings + entitiesLock.Unlock() + + return httpmock.NewJsonResponse(200, settings) }, ) + // Register default responders for user group, resource group, resource group project + SetupDefaultMockResponders(groupID, resourceGroupID, projectID, delegatedAdminGroupName, resourceGroupName, projectName) + resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { + testAccPreCheck(t) + }, ProtoV6ProviderFactories: httpMockTestV6ProviderFactories(), Steps: []resource.TestStep{ + { + Config: createSaasAppCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPreRefresh: []plancheck.PlanCheck{ + DebugPlan(), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), + resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "900"), + ), + }, { Config: createSaasAppCheckoutSettingsUpdateWithIncludeListConfig(delegatedAdminGroupName, resourceGroupName, projectName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), - resource.TestCheckResourceAttr(resourceName, "include_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "include_list.#", "2"), resource.TestCheckResourceAttr(resourceName, "include_list.0.id", "account1"), resource.TestCheckResourceAttr(resourceName, "include_list.0.service_account_user_name", "user1"), resource.TestCheckResourceAttr(resourceName, "include_list.0.saas_app_instance_name", "app1"), + resource.TestCheckResourceAttr(resourceName, "include_list.1.id", "account2"), + resource.TestCheckResourceAttr(resourceName, "include_list.1.service_account_user_name", "user2"), + resource.TestCheckResourceAttr(resourceName, "include_list.1.saas_app_instance_name", "app2"), ), }, { @@ -226,10 +257,13 @@ func TestAccSaasAppCheckoutSettingsWithMockHTTPClient(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.#", "2"), resource.TestCheckResourceAttr(resourceName, "exclude_list.0.id", "account3"), resource.TestCheckResourceAttr(resourceName, "exclude_list.0.service_account_user_name", "user3"), resource.TestCheckResourceAttr(resourceName, "exclude_list.0.saas_app_instance_name", "app3"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.1.id", "account4"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.1.service_account_user_name", "user4"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.1.saas_app_instance_name", "app4"), ), }, }, From 541428f4b00e0a5ccd07a43e28e9b15b66767e48 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Thu, 23 Jan 2025 10:45:14 -0800 Subject: [PATCH 05/13] Fix typo from `OktaPA` to `OktaPAM` references --- docs/data-sources/database.md | 2 +- docs/data-sources/database_password_settings.md | 2 +- docs/data-sources/password_settings.md | 2 +- docs/data-sources/resource_group_project.md | 2 +- docs/data-sources/resource_group_projects.md | 2 +- docs/data-sources/resource_group_server_enrollment_token.md | 2 +- docs/data-sources/resource_group_server_enrollment_tokens.md | 2 +- docs/data-sources/secret.md | 2 +- docs/data-sources/secret_folders.md | 2 +- docs/data-sources/secrets.md | 2 +- docs/resources/database.md | 2 +- docs/resources/database_password_settings.md | 2 +- docs/resources/okta_universal_directory_checkout_settings.md | 2 +- docs/resources/password_settings.md | 2 +- docs/resources/resource_group_project.md | 2 +- docs/resources/resource_group_server_enrollment_token.md | 2 +- docs/resources/saas_app_checkout_settings.md | 2 +- docs/resources/secret.md | 2 +- docs/resources/secret_folder.md | 2 +- docs/resources/server_checkout_settings.md | 2 +- oktapam/constants/descriptions/attributes.go | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/data-sources/database.md b/docs/data-sources/database.md index 3ff341d90c..3328479802 100644 --- a/docs/data-sources/database.md +++ b/docs/data-sources/database.md @@ -18,7 +18,7 @@ Returns an existing Database. ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/docs/data-sources/database_password_settings.md b/docs/data-sources/database_password_settings.md index c112a65554..8361ae9229 100644 --- a/docs/data-sources/database_password_settings.md +++ b/docs/data-sources/database_password_settings.md @@ -18,7 +18,7 @@ Returns an existing Database Password Policy for a PAM Project. ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/docs/data-sources/password_settings.md b/docs/data-sources/password_settings.md index 90dddc33be..b18a29932e 100644 --- a/docs/data-sources/password_settings.md +++ b/docs/data-sources/password_settings.md @@ -18,7 +18,7 @@ Returns an existing Server Password Policy for a PAM Project. For details, see ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Read-Only diff --git a/docs/data-sources/resource_group_project.md b/docs/data-sources/resource_group_project.md index c5d68c39db..90c60ac387 100644 --- a/docs/data-sources/resource_group_project.md +++ b/docs/data-sources/resource_group_project.md @@ -17,7 +17,7 @@ Returns an existing PAM Project associated with a specific PAM Resource Group. F ### Required -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Read-Only diff --git a/docs/data-sources/resource_group_projects.md b/docs/data-sources/resource_group_projects.md index c96b61a7ce..e5249a6299 100644 --- a/docs/data-sources/resource_group_projects.md +++ b/docs/data-sources/resource_group_projects.md @@ -17,7 +17,7 @@ Returns a list of Projects associated with an existing Resource Group. For detai ### Required -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/docs/data-sources/resource_group_server_enrollment_token.md b/docs/data-sources/resource_group_server_enrollment_token.md index cfa886083f..53f920e995 100644 --- a/docs/data-sources/resource_group_server_enrollment_token.md +++ b/docs/data-sources/resource_group_server_enrollment_token.md @@ -18,7 +18,7 @@ Returns an existing PAM Server Enrollment Token associated with a specific PAM p ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Read-Only diff --git a/docs/data-sources/resource_group_server_enrollment_tokens.md b/docs/data-sources/resource_group_server_enrollment_tokens.md index 06c0163041..361ebba330 100644 --- a/docs/data-sources/resource_group_server_enrollment_tokens.md +++ b/docs/data-sources/resource_group_server_enrollment_tokens.md @@ -18,7 +18,7 @@ Returns a list of Server Enrollment Tokens associated with a specific Project. F ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Read-Only diff --git a/docs/data-sources/secret.md b/docs/data-sources/secret.md index 44f41d9f09..37824cb3c3 100644 --- a/docs/data-sources/secret.md +++ b/docs/data-sources/secret.md @@ -18,7 +18,7 @@ Returns an existing PAM Secret. For details, see [Secrets](https://help.okta.com ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Read-Only diff --git a/docs/data-sources/secret_folders.md b/docs/data-sources/secret_folders.md index 6ac4f987f7..422e501f11 100644 --- a/docs/data-sources/secret_folders.md +++ b/docs/data-sources/secret_folders.md @@ -20,7 +20,7 @@ Returns a list of Secret Folders, constrained by the given parameters. For detai - `list_elements_under_path` (Boolean) If `true`, returns a list of any Secret/Secret Folder elements under the path. If `false`, returns the element defined by the path. - `path` (String) The path of the Secret Folder - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Read-Only diff --git a/docs/data-sources/secrets.md b/docs/data-sources/secrets.md index f18f3938b8..5f4e3f9ea8 100644 --- a/docs/data-sources/secrets.md +++ b/docs/data-sources/secrets.md @@ -19,7 +19,7 @@ Returns a list of Secrets, constrained by the given parameters. For details, see - `path` (String) The path of the Secret or parent Secret Folder - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Read-Only diff --git a/docs/resources/database.md b/docs/resources/database.md index a9b1380e6d..6a353a2506 100644 --- a/docs/resources/database.md +++ b/docs/resources/database.md @@ -18,7 +18,7 @@ The password specified in this resource will be stored *unencrypted* in your Ter - `management_connection_details` (Block List, Min: 1, Max: 1) A set of fields defining the database to connect to. (see [below for nested schema](#nestedblock--management_connection_details)) - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/docs/resources/database_password_settings.md b/docs/resources/database_password_settings.md index c40c6622b1..5f6b1af9bb 100644 --- a/docs/resources/database_password_settings.md +++ b/docs/resources/database_password_settings.md @@ -22,7 +22,7 @@ The settings for passwords set on databases within the project. - `max_length` (Number) The maximum length allowed for the password. - `min_length` (Number) The minimum length allowed for the password. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/docs/resources/okta_universal_directory_checkout_settings.md b/docs/resources/okta_universal_directory_checkout_settings.md index 2f7638195a..cfb6f7d6a8 100644 --- a/docs/resources/okta_universal_directory_checkout_settings.md +++ b/docs/resources/okta_universal_directory_checkout_settings.md @@ -20,7 +20,7 @@ Manages checkout settings for Okta Universal Directory resources in a project - `checkout_duration_in_seconds` (Number) The duration in seconds for the checkout. If the checkout is enabled, the duration is the maximum time a user can access the resource before the checkout expires. - `checkout_required` (Boolean) Indicates whether a checkout is mandatory for accessing resources within the project. If `true`, checkout is enforced for all applicable resources by default. If `false`, checkout is not required, and resources are accessible without it. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/docs/resources/password_settings.md b/docs/resources/password_settings.md index 7f21e5a027..064d954d91 100644 --- a/docs/resources/password_settings.md +++ b/docs/resources/password_settings.md @@ -23,7 +23,7 @@ The settings for passwords set on servers within the project. For details, see [ - `max_length` (Number) The maximum length allowed for the password. - `min_length` (Number) The minimum length allowed for the password. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/docs/resources/resource_group_project.md b/docs/resources/resource_group_project.md index 5d2bfb24eb..6e5225625c 100644 --- a/docs/resources/resource_group_project.md +++ b/docs/resources/resource_group_project.md @@ -18,7 +18,7 @@ A PAM construct that contains a collection of resources that share settings. For ### Required - `name` (String) The human-readable name of the resource. Values are case-sensitive. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/docs/resources/resource_group_server_enrollment_token.md b/docs/resources/resource_group_server_enrollment_token.md index 634dc45293..925eb5afd8 100644 --- a/docs/resources/resource_group_server_enrollment_token.md +++ b/docs/resources/resource_group_server_enrollment_token.md @@ -19,7 +19,7 @@ A token used to enroll servers in a PAM Project. For details, see [Server Enroll - `description` (String) The human-readable description of the resource. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Read-Only diff --git a/docs/resources/saas_app_checkout_settings.md b/docs/resources/saas_app_checkout_settings.md index 7ad291ea7f..0a8a4f5770 100644 --- a/docs/resources/saas_app_checkout_settings.md +++ b/docs/resources/saas_app_checkout_settings.md @@ -20,7 +20,7 @@ Manages checkout settings for SaaS Application resources in a project - `checkout_duration_in_seconds` (Number) The duration in seconds for the checkout. If the checkout is enabled, the duration is the maximum time a user can access the resource before the checkout expires. - `checkout_required` (Boolean) Indicates whether a checkout is mandatory for accessing resources within the project. If `true`, checkout is enforced for all applicable resources by default. If `false`, checkout is not required, and resources are accessible without it. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/docs/resources/secret.md b/docs/resources/secret.md index 3282300fc7..340961e451 100644 --- a/docs/resources/secret.md +++ b/docs/resources/secret.md @@ -19,7 +19,7 @@ The secret specified in this resource will be stored *unencrypted* in your Terra - `name` (String) The human-readable name of the resource. Values are case-sensitive. - `parent_folder` (String) The UUID of the directory which contains this Secret/Secret Folder element. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. - `secret` (Map of String, Sensitive) Defines the key value pairs that are used to store sensitive information, like usernames, passwords, API tokens, keys, or any string value. ### Optional diff --git a/docs/resources/secret_folder.md b/docs/resources/secret_folder.md index 29457ca8ed..8f490285a5 100644 --- a/docs/resources/secret_folder.md +++ b/docs/resources/secret_folder.md @@ -19,7 +19,7 @@ A file-system like construct that contains secrets or nested secret folders. For - `name` (String) The human-readable name of the resource. Values are case-sensitive. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/docs/resources/server_checkout_settings.md b/docs/resources/server_checkout_settings.md index f1581d676a..b0cd4bb05e 100644 --- a/docs/resources/server_checkout_settings.md +++ b/docs/resources/server_checkout_settings.md @@ -20,7 +20,7 @@ The settings for limitting access to vaulted (shared) accounts for a single user - `checkout_duration_in_seconds` (Number) The duration in seconds for the checkout. If the checkout is enabled, the duration is the maximum time a user can access the resource before the checkout expires. - `checkout_required` (Boolean) Indicates whether a checkout is mandatory for accessing resources within the project. If `true`, checkout is enforced for all applicable resources by default. If `false`, checkout is not required, and resources are accessible without it. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPA Resource Group. +- `resource_group` (String) The UUID of a OktaPAM Resource Group. ### Optional diff --git a/oktapam/constants/descriptions/attributes.go b/oktapam/constants/descriptions/attributes.go index 10c0087b58..9ac79ee4c2 100644 --- a/oktapam/constants/descriptions/attributes.go +++ b/oktapam/constants/descriptions/attributes.go @@ -149,7 +149,7 @@ var ( RefuseConnections = "If `true`, the Gateway refuses connections." RemovedAt = "The UTC time when the resource was removed from parent resource. Format is '2022-01-01 00:00:00 +0000 UTC'." RequirePreauthForCreds = "If `true`, requires preauthorization before a User can retrieve credentials to sign in." - ResourceGroupID = "The UUID of a OktaPA Resource Group." + ResourceGroupID = "The UUID of a OktaPAM Resource Group." Roles = "A list of roles for the ASA Group. Options are 'access_user', 'access_admin', and 'reporting_user'." SSHCertificateType = fmt.Sprintf("The SSH certificate type used by access requests. Options include: [%s]. '%s' is a deprecated key algorithm type. "+ "This option should only be used to connect to legacy systems that cannot use newer SSH versions. If you do need to use '%s', it is recommended to connect via a gateway with traffic forwarding. "+ From 18ffef2095c56385c7b281e1b44fc189e29db369 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Thu, 23 Jan 2025 10:53:22 -0800 Subject: [PATCH 06/13] self-review cleanup --- ...resource_okta_universal_directory_checkout_settings.go | 8 -------- ...rce_okta_universal_directory_checkout_settings_test.go | 2 -- oktapam/resource_saas_app_checkout_settings_test.go | 1 - 3 files changed, 11 deletions(-) diff --git a/oktapam/resource_okta_universal_directory_checkout_settings.go b/oktapam/resource_okta_universal_directory_checkout_settings.go index 623548a5c5..2ab0bef459 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings.go @@ -3,7 +3,6 @@ package oktapam import ( "context" "fmt" - "log" "github.com/okta/terraform-provider-oktapam/oktapam/convert" @@ -84,7 +83,6 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Create(ctx context.Cont checkoutSettings = *settings } - log.Printf("[DEBUG] Create Updating Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { resp.Diagnostics.AddError("Error creating Okta Universal Directory checkout settings", err.Error()) return @@ -105,7 +103,6 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Read(ctx context.Contex return } - log.Printf("[DEBUG] Read Reading Okta UD checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, state.ResourceGroup, state.Project) if checkoutSettings, _, err := r.api.FetchResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, state.ResourceGroup, state.Project).Execute(); err != nil { resp.Diagnostics.AddError( "Error reading Okta Universal Directory checkout settings", @@ -124,8 +121,6 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Read(ctx context.Contex } } - log.Printf("[DEBUG] Read Setting state for Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, state.ResourceGroup, state.Project) - log.Printf("settingsModel: %+v", state) if diags := resp.State.Set(ctx, state); diags.HasError() { resp.Diagnostics.Append(diags...) return @@ -147,13 +142,11 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Update(ctx context.Cont checkoutSettings = *settings } - log.Printf("[DEBUG] Update Updating Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { resp.Diagnostics.AddError("Error updating Okta Universal Directory checkout settings", err.Error()) return } - log.Printf("[DEBUG] Update Reading Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) if updatedSettings, _, err := r.api.FetchResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).Execute(); err != nil { resp.Diagnostics.AddError( "Error reading Okta Universal Directory checkout settings", @@ -169,7 +162,6 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Update(ctx context.Cont resp.Diagnostics.Append(diags...) return } else { - log.Printf("[DEBUG]Update Setting state for Okta Universal Directory checkout settings for team: %q resource_group: %q project_id: %q", r.teamName, plan.ResourceGroup, plan.Project) plan.ServiceAccountCheckoutSettingsModel = *settingsModel } } diff --git a/oktapam/resource_okta_universal_directory_checkout_settings_test.go b/oktapam/resource_okta_universal_directory_checkout_settings_test.go index 9ff6c555a2..fce1c16457 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings_test.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings_test.go @@ -208,7 +208,6 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { regexp.MustCompile(`/v1/teams/httpmock-test-team/resource_groups/.*/projects/.*/okta_universal_directory_checkout_settings`), func(req *http.Request) (*http.Response, error) { - // Extract IDs from URL path matches := regexp.MustCompile(`/resource_groups/([^/]+)/projects/([^/]+)/`).FindStringSubmatch(req.URL.Path) if len(matches) != 3 { return httpmock.NewStringResponse(400, "Invalid URL"), nil @@ -224,7 +223,6 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { entityKey := fmt.Sprintf("%s/%s", resourceGroupID, projectID) - // Store the settings in our entities map entitiesLock.Lock() entities[entityKey] = &settings entitiesLock.Unlock() diff --git a/oktapam/resource_saas_app_checkout_settings_test.go b/oktapam/resource_saas_app_checkout_settings_test.go index 80bde62533..0738cc1451 100644 --- a/oktapam/resource_saas_app_checkout_settings_test.go +++ b/oktapam/resource_saas_app_checkout_settings_test.go @@ -149,7 +149,6 @@ func TestAccSaasAppCheckoutSettingsWithMockHTTPClient(t *testing.T) { projectName := fmt.Sprintf("test_acc_mock_project_%s", randSeq()) delegatedAdminGroupName := fmt.Sprintf("test_acc_mock_dga_%s", randSeq()) - // Setup httpmock httpmock.Activate() defer httpmock.DeactivateAndReset() From 78ba4e36e16f5c2dd9ace806206855e84fe7d601 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Fri, 24 Jan 2025 17:59:38 -0800 Subject: [PATCH 07/13] Refactor service accounts checkout settings to reuse the existing ResourceCheckoutSettingsModel to provide only ID lists for exclusion. - Modified conversion functions to transform between SDK and Terraform model types - Added default empty list values for include and exclude lists to ensure that empty lists are handled consistently as `[]` rather than `null` in the state - Removed deprecated service account checkout settings conversion file - Updated test cases to reflect new list representation --- oktapam/convert/resource_checkout_settings.go | 46 +++++ .../service_account_checkout_settings.go | 195 ------------------ ...a_universal_directory_checkout_settings.go | 23 ++- ...versal_directory_checkout_settings_test.go | 40 +--- .../resource_saas_app_checkout_settings.go | 22 +- ...esource_saas_app_checkout_settings_test.go | 58 +----- .../resource/schema/listdefault/doc.go | 5 + .../schema/listdefault/static_value.go | 42 ++++ vendor/modules.txt | 1 + 9 files changed, 132 insertions(+), 300 deletions(-) delete mode 100644 oktapam/convert/service_account_checkout_settings.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault/doc.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault/static_value.go diff --git a/oktapam/convert/resource_checkout_settings.go b/oktapam/convert/resource_checkout_settings.go index a51f3127bc..9c86ce85cd 100644 --- a/oktapam/convert/resource_checkout_settings.go +++ b/oktapam/convert/resource_checkout_settings.go @@ -9,7 +9,9 @@ import ( "github.com/okta/terraform-provider-oktapam/oktapam/constants/descriptions" "github.com/atko-pam/pam-sdk-go/client/pam" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -38,11 +40,15 @@ func ResourceCheckoutSettingsSchemaAttributes(mergeIntoMap map[string]schema.Att ElementType: types.StringType, Optional: true, Description: descriptions.ExcludeList, + Computed: true, + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, "include_list": schema.ListAttribute{ ElementType: types.StringType, Optional: true, Description: descriptions.IncludeList, + Computed: true, + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, } @@ -107,3 +113,43 @@ func ResourceCheckoutSettingsFromSDKToModel(ctx context.Context, in *pam.Resourc return &out, diags } + +func PamResourceCheckoutSettingsToPamServiceAccountCheckoutSettings(in *pam.ResourceCheckoutSettings) *pam.APIServiceAccountCheckoutSettings { + includeList := []pam.ServiceAccountSettingNameObject{} + for _, Id := range in.IncludeList { + includeList = append(includeList, pam.ServiceAccountSettingNameObject{ + Id: Id, + }) + } + excludeList := []pam.ServiceAccountSettingNameObject{} + for _, Id := range in.ExcludeList { + excludeList = append(excludeList, pam.ServiceAccountSettingNameObject{ + Id: Id, + }) + } + + return &pam.APIServiceAccountCheckoutSettings{ + CheckoutRequired: in.CheckoutRequired, + CheckoutDurationInSeconds: *in.CheckoutDurationInSeconds, + IncludeList: includeList, + ExcludeList: excludeList, + } +} + +func PamServiceAccountCheckoutSettingsToPamResourceCheckoutSettings(in *pam.APIServiceAccountCheckoutSettings) *pam.ResourceCheckoutSettings { + includeList := []string{} + for _, include := range in.IncludeList { + includeList = append(includeList, include.Id) + } + excludeList := []string{} + for _, exclude := range in.ExcludeList { + excludeList = append(excludeList, exclude.Id) + } + resourceCheckoutSettings := &pam.ResourceCheckoutSettings{ + CheckoutRequired: in.CheckoutRequired, + CheckoutDurationInSeconds: &in.CheckoutDurationInSeconds, + IncludeList: includeList, + ExcludeList: excludeList, + } + return resourceCheckoutSettings +} diff --git a/oktapam/convert/service_account_checkout_settings.go b/oktapam/convert/service_account_checkout_settings.go deleted file mode 100644 index c6350d7a97..0000000000 --- a/oktapam/convert/service_account_checkout_settings.go +++ /dev/null @@ -1,195 +0,0 @@ -package convert - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/okta/terraform-provider-oktapam/oktapam/constants/descriptions" - - "github.com/atko-pam/pam-sdk-go/client/pam" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -// ServiceAccountCheckoutSettingsModel represents the Terraform model for service account checkout settings -type ServiceAccountCheckoutSettingsModel struct { - CheckoutRequired types.Bool `tfsdk:"checkout_required"` - CheckoutDurationInSeconds types.Int32 `tfsdk:"checkout_duration_in_seconds"` - IncludeList types.List `tfsdk:"include_list"` - ExcludeList types.List `tfsdk:"exclude_list"` -} - -// ServiceAccountSettingNameObjectModel represents the Terraform model for service account setting name object -type ServiceAccountSettingNameObjectModel struct { - Id string `tfsdk:"id"` - ServiceAccountUserName string `tfsdk:"service_account_user_name"` - SaasAppInstanceName string `tfsdk:"saas_app_instance_name"` -} - -// ServiceAccountCheckoutSettingsSchemaAttributes returns the schema attributes for service account checkout settings -func ServiceAccountCheckoutSettingsSchemaAttributes(mergeIntoMap map[string]schema.Attribute) map[string]schema.Attribute { - myMap := map[string]schema.Attribute{ - "checkout_duration_in_seconds": schema.Int32Attribute{ - Required: true, - Description: descriptions.CheckoutDurationInSeconds, - Validators: []validator.Int32{ - int32validator.Between(900, 86400), - }, - }, - "checkout_required": schema.BoolAttribute{ - Required: true, - Description: descriptions.CheckoutRequired, - }, - "exclude_list": schema.ListAttribute{ - ElementType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "id": types.StringType, - "service_account_user_name": types.StringType, - "saas_app_instance_name": types.StringType, - }, - }, - Optional: true, - Description: descriptions.ExcludeList, - }, - "include_list": schema.ListAttribute{ - ElementType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "id": types.StringType, - "service_account_user_name": types.StringType, - "saas_app_instance_name": types.StringType, - }, - }, - Optional: true, - Description: descriptions.IncludeList, - }, - } - - for key, value := range myMap { - mergeIntoMap[key] = value - } - return mergeIntoMap -} - -// ServiceAccountCheckoutSettingsFromModelToSDK converts from the Terraform model to the SDK type -func ServiceAccountCheckoutSettingsFromModelToSDK(ctx context.Context, in *ServiceAccountCheckoutSettingsModel) (*pam.APIServiceAccountCheckoutSettings, diag.Diagnostics) { - var out pam.APIServiceAccountCheckoutSettings - var diags diag.Diagnostics - - if !in.CheckoutRequired.IsNull() && !in.CheckoutRequired.IsUnknown() { - out.CheckoutRequired = in.CheckoutRequired.ValueBool() - } - if !in.CheckoutDurationInSeconds.IsNull() && !in.CheckoutDurationInSeconds.IsUnknown() { - out.CheckoutDurationInSeconds = in.CheckoutDurationInSeconds.ValueInt32() - } - - if !in.IncludeList.IsNull() && !in.IncludeList.IsUnknown() { - var modelList []ServiceAccountSettingNameObjectModel - diags.Append(in.IncludeList.ElementsAs(ctx, &modelList, false)...) - if diags.HasError() { - return nil, diags - } - - includeList := make([]pam.ServiceAccountSettingNameObject, len(modelList)) - for i, item := range modelList { - includeList[i] = pam.ServiceAccountSettingNameObject{ - Id: item.Id, - ServiceAccountUserName: &item.ServiceAccountUserName, - SaasAppInstanceName: &item.SaasAppInstanceName, - } - } - out.IncludeList = includeList - } - - if !in.ExcludeList.IsNull() && !in.ExcludeList.IsUnknown() { - var modelList []ServiceAccountSettingNameObjectModel - diags.Append(in.ExcludeList.ElementsAs(ctx, &modelList, false)...) - if diags.HasError() { - return nil, diags - } - - excludeList := make([]pam.ServiceAccountSettingNameObject, len(modelList)) - for i, item := range modelList { - excludeList[i] = pam.ServiceAccountSettingNameObject{ - Id: item.Id, - ServiceAccountUserName: &item.ServiceAccountUserName, - SaasAppInstanceName: &item.SaasAppInstanceName, - } - } - out.ExcludeList = excludeList - } - - return &out, diags -} - -// ServiceAccountCheckoutSettingsFromSDKToModel converts from the SDK type to the Terraform model -func ServiceAccountCheckoutSettingsFromSDKToModel(ctx context.Context, in *pam.APIServiceAccountCheckoutSettings) (*ServiceAccountCheckoutSettingsModel, diag.Diagnostics) { - var out ServiceAccountCheckoutSettingsModel - var diags diag.Diagnostics - - if val, ok := in.GetCheckoutRequiredOk(); ok { - out.CheckoutRequired = types.BoolPointerValue(val) - } - - if val, ok := in.GetCheckoutDurationInSecondsOk(); ok { - out.CheckoutDurationInSeconds = types.Int32PointerValue(val) - } - - var includeList []ServiceAccountSettingNameObjectModel - for _, item := range in.IncludeList { - model := ServiceAccountSettingNameObjectModel{ - Id: item.Id, - } - if item.ServiceAccountUserName != nil { - model.ServiceAccountUserName = *item.ServiceAccountUserName - } - if item.SaasAppInstanceName != nil { - model.SaasAppInstanceName = *item.SaasAppInstanceName - } - includeList = append(includeList, model) - } - - includeListValue, d := types.ListValueFrom(ctx, types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "id": types.StringType, - "service_account_user_name": types.StringType, - "saas_app_instance_name": types.StringType, - }, - }, includeList) - diags.Append(d...) - if diags.HasError() { - return nil, diags - } - out.IncludeList = includeListValue - - var excludeList []ServiceAccountSettingNameObjectModel - for _, item := range in.ExcludeList { - model := ServiceAccountSettingNameObjectModel{ - Id: item.Id, - } - if item.ServiceAccountUserName != nil { - model.ServiceAccountUserName = *item.ServiceAccountUserName - } - if item.SaasAppInstanceName != nil { - model.SaasAppInstanceName = *item.SaasAppInstanceName - } - excludeList = append(excludeList, model) - } - - excludeListValue, d := types.ListValueFrom(ctx, types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "id": types.StringType, - "service_account_user_name": types.StringType, - "saas_app_instance_name": types.StringType, - }, - }, excludeList) - diags.Append(d...) - if diags.HasError() { - return nil, diags - } - out.ExcludeList = excludeListValue - - return &out, diags -} diff --git a/oktapam/resource_okta_universal_directory_checkout_settings.go b/oktapam/resource_okta_universal_directory_checkout_settings.go index 2ab0bef459..14fa4b9521 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings.go @@ -33,7 +33,7 @@ type oktaUniversalDirectoryCheckoutSettingsResourceModel struct { Id types.String `tfsdk:"id"` ResourceGroup string `tfsdk:"resource_group"` Project string `tfsdk:"project"` - convert.ServiceAccountCheckoutSettingsModel + convert.ResourceCheckoutSettingsModel } func (r *oktaUniversalDirectoryCheckoutSettingsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -43,7 +43,7 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Metadata(_ context.Cont func (r *oktaUniversalDirectoryCheckoutSettingsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Manages checkout settings for Okta Universal Directory resources in a project", - Attributes: convert.ServiceAccountCheckoutSettingsSchemaAttributes(map[string]schema.Attribute{ + Attributes: convert.ResourceCheckoutSettingsSchemaAttributes(map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, PlanModifiers: []planmodifier.String{ @@ -76,11 +76,11 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Create(ctx context.Cont } var checkoutSettings pam.APIServiceAccountCheckoutSettings - if settings, diags := convert.ServiceAccountCheckoutSettingsFromModelToSDK(ctx, &plan.ServiceAccountCheckoutSettingsModel); diags.HasError() { + if settings, diags := convert.ResourceCheckoutSettingsFromModelToSDK(ctx, &plan.ResourceCheckoutSettingsModel); diags.HasError() { resp.Diagnostics.Append(diags...) return } else { - checkoutSettings = *settings + checkoutSettings = *convert.PamResourceCheckoutSettingsToPamServiceAccountCheckoutSettings(settings) } if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { @@ -113,11 +113,12 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Read(ctx context.Contex err.Error())) return } else { - if settingsModel, diags := convert.ServiceAccountCheckoutSettingsFromSDKToModel(ctx, checkoutSettings); diags.HasError() { + resourceCheckoutSettings := convert.PamServiceAccountCheckoutSettingsToPamResourceCheckoutSettings(checkoutSettings) + if settingsModel, diags := convert.ResourceCheckoutSettingsFromSDKToModel(ctx, resourceCheckoutSettings); diags.HasError() { resp.Diagnostics.Append(diags...) return } else { - state.ServiceAccountCheckoutSettingsModel = *settingsModel + state.ResourceCheckoutSettingsModel = *settingsModel } } @@ -135,11 +136,11 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Update(ctx context.Cont } var checkoutSettings pam.APIServiceAccountCheckoutSettings - if settings, diags := convert.ServiceAccountCheckoutSettingsFromModelToSDK(ctx, &plan.ServiceAccountCheckoutSettingsModel); diags.HasError() { + if settings, diags := convert.ResourceCheckoutSettingsFromModelToSDK(ctx, &plan.ResourceCheckoutSettingsModel); diags.HasError() { resp.Diagnostics.Append(diags...) return } else { - checkoutSettings = *settings + checkoutSettings = *convert.PamResourceCheckoutSettingsToPamServiceAccountCheckoutSettings(settings) } if _, err := r.api.UpdateResourceGroupOktaUniversalDirectoryBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { @@ -157,12 +158,12 @@ func (r *oktaUniversalDirectoryCheckoutSettingsResource) Update(ctx context.Cont err.Error())) return } else { - - if settingsModel, diags := convert.ServiceAccountCheckoutSettingsFromSDKToModel(ctx, updatedSettings); diags.HasError() { + resourceCheckoutSettings := convert.PamServiceAccountCheckoutSettingsToPamResourceCheckoutSettings(updatedSettings) + if settingsModel, diags := convert.ResourceCheckoutSettingsFromSDKToModel(ctx, resourceCheckoutSettings); diags.HasError() { resp.Diagnostics.Append(diags...) return } else { - plan.ServiceAccountCheckoutSettingsModel = *settingsModel + plan.ResourceCheckoutSettingsModel = *settingsModel } } diff --git a/oktapam/resource_okta_universal_directory_checkout_settings_test.go b/oktapam/resource_okta_universal_directory_checkout_settings_test.go index fce1c16457..b1fae91656 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings_test.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings_test.go @@ -66,13 +66,7 @@ resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_uni project = oktapam_resource_group_project.test_acc_resource_group_project.id checkout_required = true checkout_duration_in_seconds = 3600 - include_list = [ - { - id = "account1", - service_account_user_name = "user1", - saas_app_instance_name = "app1" - }, - ] + include_list = ["service_account_1"] } ` @@ -82,13 +76,7 @@ resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_uni project = oktapam_resource_group_project.test_acc_resource_group_project.id checkout_required = true checkout_duration_in_seconds = 3600 - exclude_list = [ - { - id = "account3", - service_account_user_name = "user3", - saas_app_instance_name = "app3" - } - ] + exclude_list = ["service_account_3"] } ` @@ -98,20 +86,8 @@ resource "oktapam_okta_universal_directory_checkout_settings" "test_acc_okta_uni project = oktapam_resource_group_project.test_acc_resource_group_project.id checkout_required = true checkout_duration_in_seconds = 7200 - include_list = [ - { - id = "account1", - service_account_user_name = "user1", - saas_app_instance_name = "app1" - } - ] - exclude_list = [ - { - id = "account3", - service_account_user_name = "user3", - saas_app_instance_name = "app3" - } - ] + include_list = ["service_account_1"] + exclude_list = ["service_account_3"] } ` @@ -262,9 +238,7 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), resource.TestCheckResourceAttr(resourceName, "include_list.#", "1"), - resource.TestCheckResourceAttr(resourceName, "include_list.0.id", "account1"), - resource.TestCheckResourceAttr(resourceName, "include_list.0.service_account_user_name", "user1"), - resource.TestCheckResourceAttr(resourceName, "include_list.0.saas_app_instance_name", "app1"), + resource.TestCheckResourceAttr(resourceName, "include_list.0", "service_account_1"), ), }, { @@ -274,9 +248,7 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), resource.TestCheckResourceAttr(resourceName, "exclude_list.#", "1"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.0.id", "account3"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.0.service_account_user_name", "user3"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.0.saas_app_instance_name", "app3"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0", "service_account_3"), ), }, }, diff --git a/oktapam/resource_saas_app_checkout_settings.go b/oktapam/resource_saas_app_checkout_settings.go index 2559044d1b..bd28730d9a 100644 --- a/oktapam/resource_saas_app_checkout_settings.go +++ b/oktapam/resource_saas_app_checkout_settings.go @@ -33,7 +33,7 @@ type saasAppCheckoutSettingsResourceModel struct { Id types.String `tfsdk:"id"` ResourceGroup string `tfsdk:"resource_group"` Project string `tfsdk:"project"` - convert.ServiceAccountCheckoutSettingsModel + convert.ResourceCheckoutSettingsModel } func (r *saasAppCheckoutSettingsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -43,7 +43,7 @@ func (r *saasAppCheckoutSettingsResource) Metadata(_ context.Context, req resour func (r *saasAppCheckoutSettingsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Manages checkout settings for SaaS Application resources in a project", - Attributes: convert.ServiceAccountCheckoutSettingsSchemaAttributes(map[string]schema.Attribute{ + Attributes: convert.ResourceCheckoutSettingsSchemaAttributes(map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, PlanModifiers: []planmodifier.String{ @@ -76,11 +76,11 @@ func (r *saasAppCheckoutSettingsResource) Create(ctx context.Context, req resour } var checkoutSettings pam.APIServiceAccountCheckoutSettings - if settings, diags := convert.ServiceAccountCheckoutSettingsFromModelToSDK(ctx, &plan.ServiceAccountCheckoutSettingsModel); diags.HasError() { + if settings, diags := convert.ResourceCheckoutSettingsFromModelToSDK(ctx, &plan.ResourceCheckoutSettingsModel); diags.HasError() { resp.Diagnostics.Append(diags...) return } else { - checkoutSettings = *settings + checkoutSettings = *convert.PamResourceCheckoutSettingsToPamServiceAccountCheckoutSettings(settings) } if _, err := r.api.UpdateResourceGroupSaasAppBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { @@ -113,11 +113,12 @@ func (r *saasAppCheckoutSettingsResource) Read(ctx context.Context, req resource err.Error())) return } else { - if settingsModel, diags := convert.ServiceAccountCheckoutSettingsFromSDKToModel(ctx, checkoutSettings); diags.HasError() { + resourceCheckoutSettings := convert.PamServiceAccountCheckoutSettingsToPamResourceCheckoutSettings(checkoutSettings) + if settingsModel, diags := convert.ResourceCheckoutSettingsFromSDKToModel(ctx, resourceCheckoutSettings); diags.HasError() { resp.Diagnostics.Append(diags...) return } else { - state.ServiceAccountCheckoutSettingsModel = *settingsModel + state.ResourceCheckoutSettingsModel = *settingsModel } } @@ -135,11 +136,11 @@ func (r *saasAppCheckoutSettingsResource) Update(ctx context.Context, req resour } var checkoutSettings pam.APIServiceAccountCheckoutSettings - if settings, diags := convert.ServiceAccountCheckoutSettingsFromModelToSDK(ctx, &plan.ServiceAccountCheckoutSettingsModel); diags.HasError() { + if settings, diags := convert.ResourceCheckoutSettingsFromModelToSDK(ctx, &plan.ResourceCheckoutSettingsModel); diags.HasError() { resp.Diagnostics.Append(diags...) return } else { - checkoutSettings = *settings + checkoutSettings = *convert.PamResourceCheckoutSettingsToPamServiceAccountCheckoutSettings(settings) } if _, err := r.api.UpdateResourceGroupSaasAppBasedProjectCheckoutSettings(ctx, r.teamName, plan.ResourceGroup, plan.Project).APIServiceAccountCheckoutSettings(checkoutSettings).Execute(); err != nil { @@ -157,11 +158,12 @@ func (r *saasAppCheckoutSettingsResource) Update(ctx context.Context, req resour err.Error())) return } else { - if settingsModel, diags := convert.ServiceAccountCheckoutSettingsFromSDKToModel(ctx, updatedSettings); diags.HasError() { + resourceCheckoutSettings := convert.PamServiceAccountCheckoutSettingsToPamResourceCheckoutSettings(updatedSettings) + if settingsModel, diags := convert.ResourceCheckoutSettingsFromSDKToModel(ctx, resourceCheckoutSettings); diags.HasError() { resp.Diagnostics.Append(diags...) return } else { - plan.ServiceAccountCheckoutSettingsModel = *settingsModel + plan.ResourceCheckoutSettingsModel = *settingsModel } } diff --git a/oktapam/resource_saas_app_checkout_settings_test.go b/oktapam/resource_saas_app_checkout_settings_test.go index 0738cc1451..825f383568 100644 --- a/oktapam/resource_saas_app_checkout_settings_test.go +++ b/oktapam/resource_saas_app_checkout_settings_test.go @@ -50,18 +50,7 @@ resource "oktapam_saas_app_checkout_settings" "test_acc_saas_app_checkout_settin project = oktapam_resource_group_project.test_acc_resource_group_project.id checkout_required = true checkout_duration_in_seconds = 3600 - include_list = [ - { - id = "account1", - service_account_user_name = "user1", - saas_app_instance_name = "app1" - }, - { - id = "account2", - service_account_user_name = "user2", - saas_app_instance_name = "app2" - } - ] + include_list = [ "service_account_1", "service_account_2" ] } ` @@ -71,18 +60,7 @@ resource "oktapam_saas_app_checkout_settings" "test_acc_saas_app_checkout_settin project = oktapam_resource_group_project.test_acc_resource_group_project.id checkout_required = true checkout_duration_in_seconds = 3600 - exclude_list = [ - { - id = "account3", - service_account_user_name = "user3", - saas_app_instance_name = "app3" - }, - { - id = "account4", - service_account_user_name = "user4", - saas_app_instance_name = "app4" - } - ] + exclude_list = [ "service_account_3", "service_account_4" ] } ` @@ -92,20 +70,8 @@ resource "oktapam_saas_app_checkout_settings" "test_acc_saas_app_checkout_settin project = oktapam_resource_group_project.test_acc_resource_group_project.id checkout_required = true checkout_duration_in_seconds = 7200 - include_list = [ - { - id = "account1", - service_account_user_name = "user1", - saas_app_instance_name = "app1" - } - ] - exclude_list = [ - { - id = "account3", - service_account_user_name = "user3", - saas_app_instance_name = "app3" - } - ] + include_list = [ "service_account_1", "service_account_2" ] + exclude_list = [ "service_account_3", "service_account_4" ] } ` @@ -243,12 +209,8 @@ func TestAccSaasAppCheckoutSettingsWithMockHTTPClient(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), resource.TestCheckResourceAttr(resourceName, "include_list.#", "2"), - resource.TestCheckResourceAttr(resourceName, "include_list.0.id", "account1"), - resource.TestCheckResourceAttr(resourceName, "include_list.0.service_account_user_name", "user1"), - resource.TestCheckResourceAttr(resourceName, "include_list.0.saas_app_instance_name", "app1"), - resource.TestCheckResourceAttr(resourceName, "include_list.1.id", "account2"), - resource.TestCheckResourceAttr(resourceName, "include_list.1.service_account_user_name", "user2"), - resource.TestCheckResourceAttr(resourceName, "include_list.1.saas_app_instance_name", "app2"), + resource.TestCheckResourceAttr(resourceName, "include_list.0", "service_account_1"), + resource.TestCheckResourceAttr(resourceName, "include_list.1", "service_account_2"), ), }, { @@ -257,12 +219,8 @@ func TestAccSaasAppCheckoutSettingsWithMockHTTPClient(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "3600"), resource.TestCheckResourceAttr(resourceName, "exclude_list.#", "2"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.0.id", "account3"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.0.service_account_user_name", "user3"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.0.saas_app_instance_name", "app3"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.1.id", "account4"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.1.service_account_user_name", "user4"), - resource.TestCheckResourceAttr(resourceName, "exclude_list.1.saas_app_instance_name", "app4"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.0", "service_account_3"), + resource.TestCheckResourceAttr(resourceName, "exclude_list.1", "service_account_4"), ), }, }, diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault/doc.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault/doc.go new file mode 100644 index 0000000000..3fa25de521 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package listdefault provides default values for types.List attributes. +package listdefault diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault/static_value.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault/static_value.go new file mode 100644 index 0000000000..57284f5ae4 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault/static_value.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listdefault + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// StaticValue returns a static list value default handler. +// +// Use StaticValue if a static default value for a list should be set. +func StaticValue(defaultVal types.List) defaults.List { + return staticValueDefault{ + defaultVal: defaultVal, + } +} + +// staticValueDefault is static value default handler that +// sets a value on a list attribute. +type staticValueDefault struct { + defaultVal types.List +} + +// Description returns a human-readable description of the default value handler. +func (d staticValueDefault) Description(_ context.Context) string { + return fmt.Sprintf("value defaults to %v", d.defaultVal) +} + +// MarkdownDescription returns a markdown description of the default value handler. +func (d staticValueDefault) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("value defaults to `%v`", d.defaultVal) +} + +// DefaultList implements the static default value logic. +func (d staticValueDefault) DefaultList(ctx context.Context, req defaults.ListRequest, resp *defaults.ListResponse) { + resp.PlanValue = d.defaultVal +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b09a2033b3..3ba3a1c1d3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -214,6 +214,7 @@ github.com/hashicorp/terraform-plugin-framework/resource github.com/hashicorp/terraform-plugin-framework/resource/schema github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults +github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier github.com/hashicorp/terraform-plugin-framework/schema/validator From af0a4d86e3f81f8010e5df44aae5dcd31c6eeb37 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Fri, 24 Jan 2025 18:02:14 -0800 Subject: [PATCH 08/13] Revert "Fix typo from `OktaPA` to `OktaPAM` references" This reverts commit faf562a8eabb0ca0e481745d1824495dbbe7103d. --- docs/data-sources/database.md | 2 +- docs/data-sources/database_password_settings.md | 2 +- docs/data-sources/password_settings.md | 2 +- docs/data-sources/resource_group_project.md | 2 +- docs/data-sources/resource_group_projects.md | 2 +- docs/data-sources/resource_group_server_enrollment_token.md | 2 +- docs/data-sources/resource_group_server_enrollment_tokens.md | 2 +- docs/data-sources/secret.md | 2 +- docs/data-sources/secret_folders.md | 2 +- docs/data-sources/secrets.md | 2 +- docs/resources/database.md | 2 +- docs/resources/database_password_settings.md | 2 +- docs/resources/okta_universal_directory_checkout_settings.md | 2 +- docs/resources/password_settings.md | 2 +- docs/resources/resource_group_project.md | 2 +- docs/resources/resource_group_server_enrollment_token.md | 2 +- docs/resources/saas_app_checkout_settings.md | 2 +- docs/resources/secret.md | 2 +- docs/resources/secret_folder.md | 2 +- docs/resources/server_checkout_settings.md | 2 +- oktapam/constants/descriptions/attributes.go | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/data-sources/database.md b/docs/data-sources/database.md index 3328479802..3ff341d90c 100644 --- a/docs/data-sources/database.md +++ b/docs/data-sources/database.md @@ -18,7 +18,7 @@ Returns an existing Database. ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/docs/data-sources/database_password_settings.md b/docs/data-sources/database_password_settings.md index 8361ae9229..c112a65554 100644 --- a/docs/data-sources/database_password_settings.md +++ b/docs/data-sources/database_password_settings.md @@ -18,7 +18,7 @@ Returns an existing Database Password Policy for a PAM Project. ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/docs/data-sources/password_settings.md b/docs/data-sources/password_settings.md index b18a29932e..90dddc33be 100644 --- a/docs/data-sources/password_settings.md +++ b/docs/data-sources/password_settings.md @@ -18,7 +18,7 @@ Returns an existing Server Password Policy for a PAM Project. For details, see ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Read-Only diff --git a/docs/data-sources/resource_group_project.md b/docs/data-sources/resource_group_project.md index 90c60ac387..c5d68c39db 100644 --- a/docs/data-sources/resource_group_project.md +++ b/docs/data-sources/resource_group_project.md @@ -17,7 +17,7 @@ Returns an existing PAM Project associated with a specific PAM Resource Group. F ### Required -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Read-Only diff --git a/docs/data-sources/resource_group_projects.md b/docs/data-sources/resource_group_projects.md index e5249a6299..c96b61a7ce 100644 --- a/docs/data-sources/resource_group_projects.md +++ b/docs/data-sources/resource_group_projects.md @@ -17,7 +17,7 @@ Returns a list of Projects associated with an existing Resource Group. For detai ### Required -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/docs/data-sources/resource_group_server_enrollment_token.md b/docs/data-sources/resource_group_server_enrollment_token.md index 53f920e995..cfa886083f 100644 --- a/docs/data-sources/resource_group_server_enrollment_token.md +++ b/docs/data-sources/resource_group_server_enrollment_token.md @@ -18,7 +18,7 @@ Returns an existing PAM Server Enrollment Token associated with a specific PAM p ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Read-Only diff --git a/docs/data-sources/resource_group_server_enrollment_tokens.md b/docs/data-sources/resource_group_server_enrollment_tokens.md index 361ebba330..06c0163041 100644 --- a/docs/data-sources/resource_group_server_enrollment_tokens.md +++ b/docs/data-sources/resource_group_server_enrollment_tokens.md @@ -18,7 +18,7 @@ Returns a list of Server Enrollment Tokens associated with a specific Project. F ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Read-Only diff --git a/docs/data-sources/secret.md b/docs/data-sources/secret.md index 37824cb3c3..44f41d9f09 100644 --- a/docs/data-sources/secret.md +++ b/docs/data-sources/secret.md @@ -18,7 +18,7 @@ Returns an existing PAM Secret. For details, see [Secrets](https://help.okta.com ### Required - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Read-Only diff --git a/docs/data-sources/secret_folders.md b/docs/data-sources/secret_folders.md index 422e501f11..6ac4f987f7 100644 --- a/docs/data-sources/secret_folders.md +++ b/docs/data-sources/secret_folders.md @@ -20,7 +20,7 @@ Returns a list of Secret Folders, constrained by the given parameters. For detai - `list_elements_under_path` (Boolean) If `true`, returns a list of any Secret/Secret Folder elements under the path. If `false`, returns the element defined by the path. - `path` (String) The path of the Secret Folder - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Read-Only diff --git a/docs/data-sources/secrets.md b/docs/data-sources/secrets.md index 5f4e3f9ea8..f18f3938b8 100644 --- a/docs/data-sources/secrets.md +++ b/docs/data-sources/secrets.md @@ -19,7 +19,7 @@ Returns a list of Secrets, constrained by the given parameters. For details, see - `path` (String) The path of the Secret or parent Secret Folder - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Read-Only diff --git a/docs/resources/database.md b/docs/resources/database.md index 6a353a2506..a9b1380e6d 100644 --- a/docs/resources/database.md +++ b/docs/resources/database.md @@ -18,7 +18,7 @@ The password specified in this resource will be stored *unencrypted* in your Ter - `management_connection_details` (Block List, Min: 1, Max: 1) A set of fields defining the database to connect to. (see [below for nested schema](#nestedblock--management_connection_details)) - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/docs/resources/database_password_settings.md b/docs/resources/database_password_settings.md index 5f6b1af9bb..c40c6622b1 100644 --- a/docs/resources/database_password_settings.md +++ b/docs/resources/database_password_settings.md @@ -22,7 +22,7 @@ The settings for passwords set on databases within the project. - `max_length` (Number) The maximum length allowed for the password. - `min_length` (Number) The minimum length allowed for the password. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/docs/resources/okta_universal_directory_checkout_settings.md b/docs/resources/okta_universal_directory_checkout_settings.md index cfb6f7d6a8..2f7638195a 100644 --- a/docs/resources/okta_universal_directory_checkout_settings.md +++ b/docs/resources/okta_universal_directory_checkout_settings.md @@ -20,7 +20,7 @@ Manages checkout settings for Okta Universal Directory resources in a project - `checkout_duration_in_seconds` (Number) The duration in seconds for the checkout. If the checkout is enabled, the duration is the maximum time a user can access the resource before the checkout expires. - `checkout_required` (Boolean) Indicates whether a checkout is mandatory for accessing resources within the project. If `true`, checkout is enforced for all applicable resources by default. If `false`, checkout is not required, and resources are accessible without it. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/docs/resources/password_settings.md b/docs/resources/password_settings.md index 064d954d91..7f21e5a027 100644 --- a/docs/resources/password_settings.md +++ b/docs/resources/password_settings.md @@ -23,7 +23,7 @@ The settings for passwords set on servers within the project. For details, see [ - `max_length` (Number) The maximum length allowed for the password. - `min_length` (Number) The minimum length allowed for the password. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/docs/resources/resource_group_project.md b/docs/resources/resource_group_project.md index 6e5225625c..5d2bfb24eb 100644 --- a/docs/resources/resource_group_project.md +++ b/docs/resources/resource_group_project.md @@ -18,7 +18,7 @@ A PAM construct that contains a collection of resources that share settings. For ### Required - `name` (String) The human-readable name of the resource. Values are case-sensitive. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/docs/resources/resource_group_server_enrollment_token.md b/docs/resources/resource_group_server_enrollment_token.md index 925eb5afd8..634dc45293 100644 --- a/docs/resources/resource_group_server_enrollment_token.md +++ b/docs/resources/resource_group_server_enrollment_token.md @@ -19,7 +19,7 @@ A token used to enroll servers in a PAM Project. For details, see [Server Enroll - `description` (String) The human-readable description of the resource. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Read-Only diff --git a/docs/resources/saas_app_checkout_settings.md b/docs/resources/saas_app_checkout_settings.md index 0a8a4f5770..7ad291ea7f 100644 --- a/docs/resources/saas_app_checkout_settings.md +++ b/docs/resources/saas_app_checkout_settings.md @@ -20,7 +20,7 @@ Manages checkout settings for SaaS Application resources in a project - `checkout_duration_in_seconds` (Number) The duration in seconds for the checkout. If the checkout is enabled, the duration is the maximum time a user can access the resource before the checkout expires. - `checkout_required` (Boolean) Indicates whether a checkout is mandatory for accessing resources within the project. If `true`, checkout is enforced for all applicable resources by default. If `false`, checkout is not required, and resources are accessible without it. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/docs/resources/secret.md b/docs/resources/secret.md index 340961e451..3282300fc7 100644 --- a/docs/resources/secret.md +++ b/docs/resources/secret.md @@ -19,7 +19,7 @@ The secret specified in this resource will be stored *unencrypted* in your Terra - `name` (String) The human-readable name of the resource. Values are case-sensitive. - `parent_folder` (String) The UUID of the directory which contains this Secret/Secret Folder element. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. - `secret` (Map of String, Sensitive) Defines the key value pairs that are used to store sensitive information, like usernames, passwords, API tokens, keys, or any string value. ### Optional diff --git a/docs/resources/secret_folder.md b/docs/resources/secret_folder.md index 8f490285a5..29457ca8ed 100644 --- a/docs/resources/secret_folder.md +++ b/docs/resources/secret_folder.md @@ -19,7 +19,7 @@ A file-system like construct that contains secrets or nested secret folders. For - `name` (String) The human-readable name of the resource. Values are case-sensitive. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/docs/resources/server_checkout_settings.md b/docs/resources/server_checkout_settings.md index b0cd4bb05e..f1581d676a 100644 --- a/docs/resources/server_checkout_settings.md +++ b/docs/resources/server_checkout_settings.md @@ -20,7 +20,7 @@ The settings for limitting access to vaulted (shared) accounts for a single user - `checkout_duration_in_seconds` (Number) The duration in seconds for the checkout. If the checkout is enabled, the duration is the maximum time a user can access the resource before the checkout expires. - `checkout_required` (Boolean) Indicates whether a checkout is mandatory for accessing resources within the project. If `true`, checkout is enforced for all applicable resources by default. If `false`, checkout is not required, and resources are accessible without it. - `project` (String) The UUID of a Project. -- `resource_group` (String) The UUID of a OktaPAM Resource Group. +- `resource_group` (String) The UUID of a OktaPA Resource Group. ### Optional diff --git a/oktapam/constants/descriptions/attributes.go b/oktapam/constants/descriptions/attributes.go index 9ac79ee4c2..10c0087b58 100644 --- a/oktapam/constants/descriptions/attributes.go +++ b/oktapam/constants/descriptions/attributes.go @@ -149,7 +149,7 @@ var ( RefuseConnections = "If `true`, the Gateway refuses connections." RemovedAt = "The UTC time when the resource was removed from parent resource. Format is '2022-01-01 00:00:00 +0000 UTC'." RequirePreauthForCreds = "If `true`, requires preauthorization before a User can retrieve credentials to sign in." - ResourceGroupID = "The UUID of a OktaPAM Resource Group." + ResourceGroupID = "The UUID of a OktaPA Resource Group." Roles = "A list of roles for the ASA Group. Options are 'access_user', 'access_admin', and 'reporting_user'." SSHCertificateType = fmt.Sprintf("The SSH certificate type used by access requests. Options include: [%s]. '%s' is a deprecated key algorithm type. "+ "This option should only be used to connect to legacy systems that cannot use newer SSH versions. If you do need to use '%s', it is recommended to connect via a gateway with traffic forwarding. "+ From 5b6b1566be6af2392efbec300078714073114631 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Fri, 24 Jan 2025 18:04:20 -0800 Subject: [PATCH 09/13] update doc --- ...a_universal_directory_checkout_settings.md | 23 ++----------------- docs/resources/saas_app_checkout_settings.md | 23 ++----------------- 2 files changed, 4 insertions(+), 42 deletions(-) diff --git a/docs/resources/okta_universal_directory_checkout_settings.md b/docs/resources/okta_universal_directory_checkout_settings.md index 2f7638195a..11b06651b7 100644 --- a/docs/resources/okta_universal_directory_checkout_settings.md +++ b/docs/resources/okta_universal_directory_checkout_settings.md @@ -24,28 +24,9 @@ Manages checkout settings for Okta Universal Directory resources in a project ### Optional -- `exclude_list` (List of Object) If provided, only the account identifiers listed are excluded from the checkout requirement. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. (see [below for nested schema](#nestedatt--exclude_list)) -- `include_list` (List of Object) If provided, only the account identifiers listed are required to perform a checkout to access the resource. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. (see [below for nested schema](#nestedatt--include_list)) +- `exclude_list` (List of String) If provided, only the account identifiers listed are excluded from the checkout requirement. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. +- `include_list` (List of String) If provided, only the account identifiers listed are required to perform a checkout to access the resource. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. ### Read-Only - `id` (String) The ID of this resource. - - -### Nested Schema for `exclude_list` - -Optional: - -- `id` (String) -- `saas_app_instance_name` (String) -- `service_account_user_name` (String) - - - -### Nested Schema for `include_list` - -Optional: - -- `id` (String) -- `saas_app_instance_name` (String) -- `service_account_user_name` (String) diff --git a/docs/resources/saas_app_checkout_settings.md b/docs/resources/saas_app_checkout_settings.md index 7ad291ea7f..94725e9592 100644 --- a/docs/resources/saas_app_checkout_settings.md +++ b/docs/resources/saas_app_checkout_settings.md @@ -24,28 +24,9 @@ Manages checkout settings for SaaS Application resources in a project ### Optional -- `exclude_list` (List of Object) If provided, only the account identifiers listed are excluded from the checkout requirement. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. (see [below for nested schema](#nestedatt--exclude_list)) -- `include_list` (List of Object) If provided, only the account identifiers listed are required to perform a checkout to access the resource. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. (see [below for nested schema](#nestedatt--include_list)) +- `exclude_list` (List of String) If provided, only the account identifiers listed are excluded from the checkout requirement. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. +- `include_list` (List of String) If provided, only the account identifiers listed are required to perform a checkout to access the resource. This list is only considered if `checkout_required` is set to `true`. Only one of `include_list` and `exclude_list` can be specified in a request since they are mutually exclusive. ### Read-Only - `id` (String) The ID of this resource. - - -### Nested Schema for `exclude_list` - -Optional: - -- `id` (String) -- `saas_app_instance_name` (String) -- `service_account_user_name` (String) - - - -### Nested Schema for `include_list` - -Optional: - -- `id` (String) -- `saas_app_instance_name` (String) -- `service_account_user_name` (String) From 522fb9f71c5f358132f5d031ed5201f9d94966e8 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Fri, 24 Jan 2025 18:07:28 -0800 Subject: [PATCH 10/13] nit --- oktapam/provider_pluginframework.go | 2 +- oktapam/resource_okta_universal_directory_checkout_settings.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/oktapam/provider_pluginframework.go b/oktapam/provider_pluginframework.go index 12756d1216..b256f0b815 100644 --- a/oktapam/provider_pluginframework.go +++ b/oktapam/provider_pluginframework.go @@ -123,7 +123,7 @@ func (p *FrameworkProvider) Resources(_ context.Context) []func() resource.Resou NewServerCheckoutSettingsResource, NewSecurityPolicyResource, NewSaasAppCheckoutSettingsResource, - NewoktaUniversalDirectoryCheckoutSettingsResource, + NewOktaUniversalDirectoryCheckoutSettingsResource, } } diff --git a/oktapam/resource_okta_universal_directory_checkout_settings.go b/oktapam/resource_okta_universal_directory_checkout_settings.go index 14fa4b9521..aed741c5a4 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings.go @@ -20,7 +20,7 @@ var ( _ resource.ResourceWithConfigure = &oktaUniversalDirectoryCheckoutSettingsResource{} ) -func NewoktaUniversalDirectoryCheckoutSettingsResource() resource.Resource { +func NewOktaUniversalDirectoryCheckoutSettingsResource() resource.Resource { return &oktaUniversalDirectoryCheckoutSettingsResource{} } From 2c14bc0403bd4c350b7fee85ca01d589ce2313b3 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Mon, 27 Jan 2025 12:12:53 -0800 Subject: [PATCH 11/13] remove req.Plan debug check --- ...versal_directory_checkout_settings_test.go | 23 ------------------- ...esource_saas_app_checkout_settings_test.go | 6 ----- 2 files changed, 29 deletions(-) diff --git a/oktapam/resource_okta_universal_directory_checkout_settings_test.go b/oktapam/resource_okta_universal_directory_checkout_settings_test.go index b1fae91656..f49455c8ad 100644 --- a/oktapam/resource_okta_universal_directory_checkout_settings_test.go +++ b/oktapam/resource_okta_universal_directory_checkout_settings_test.go @@ -12,28 +12,11 @@ import ( "github.com/atko-pam/pam-sdk-go/client/pam" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/jarcoal/httpmock" "github.com/okta/terraform-provider-oktapam/oktapam/constants/attributes" ) -var _ plancheck.PlanCheck = debugPlan{} - -type debugPlan struct{} - -func (e debugPlan) CheckPlan(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { - rd, err := json.Marshal(req.Plan) - if err != nil { - fmt.Println("error marshalling machine-readable plan output:", err) - } - fmt.Printf("req.Plan - %s\n", string(rd)) -} - -func DebugPlan() plancheck.PlanCheck { - return debugPlan{} -} - const testAccOktaUDCheckoutSettingsBaseConfigFormat = ` resource "oktapam_group" "test_resource_group_dga_group" { name = "%s" @@ -218,12 +201,6 @@ func TestAccOktaUDCheckoutSettingsWithMockHTTPClient(t *testing.T) { Steps: []resource.TestStep{ { Config: createOktaUDCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PostApplyPreRefresh: []plancheck.PlanCheck{ - DebugPlan(), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "900"), diff --git a/oktapam/resource_saas_app_checkout_settings_test.go b/oktapam/resource_saas_app_checkout_settings_test.go index 825f383568..54446897a1 100644 --- a/oktapam/resource_saas_app_checkout_settings_test.go +++ b/oktapam/resource_saas_app_checkout_settings_test.go @@ -12,7 +12,6 @@ import ( "github.com/atko-pam/pam-sdk-go/client/pam" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/jarcoal/httpmock" "github.com/okta/terraform-provider-oktapam/oktapam/constants/attributes" @@ -193,11 +192,6 @@ func TestAccSaasAppCheckoutSettingsWithMockHTTPClient(t *testing.T) { Steps: []resource.TestStep{ { Config: createSaasAppCheckoutSettingsCreateConfig(delegatedAdminGroupName, resourceGroupName, projectName), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PostApplyPreRefresh: []plancheck.PlanCheck{ - DebugPlan(), - }, - }, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "checkout_required", "true"), resource.TestCheckResourceAttr(resourceName, "checkout_duration_in_seconds", "900"), From d6ed55143806f998d770c17f23b4899a2a0f51e4 Mon Sep 17 00:00:00 2001 From: guoliangye-okta Date: Mon, 27 Jan 2025 12:13:24 -0800 Subject: [PATCH 12/13] prevent the plan from showing differences between null and [] empty lists --- oktapam/convert/resource_checkout_settings.go | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/oktapam/convert/resource_checkout_settings.go b/oktapam/convert/resource_checkout_settings.go index 9c86ce85cd..c2783da0da 100644 --- a/oktapam/convert/resource_checkout_settings.go +++ b/oktapam/convert/resource_checkout_settings.go @@ -9,9 +9,7 @@ import ( "github.com/okta/terraform-provider-oktapam/oktapam/constants/descriptions" "github.com/atko-pam/pam-sdk-go/client/pam" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -40,15 +38,11 @@ func ResourceCheckoutSettingsSchemaAttributes(mergeIntoMap map[string]schema.Att ElementType: types.StringType, Optional: true, Description: descriptions.ExcludeList, - Computed: true, - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, "include_list": schema.ListAttribute{ ElementType: types.StringType, Optional: true, Description: descriptions.IncludeList, - Computed: true, - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, } @@ -69,19 +63,32 @@ func ResourceCheckoutSettingsFromModelToSDK(ctx context.Context, in *ResourceChe out.CheckoutDurationInSeconds = in.CheckoutDurationInSeconds.ValueInt32Pointer() } + // Initialize empty lists + out.IncludeList = []string{} + out.ExcludeList = []string{} + if !in.IncludeList.IsNull() && !in.IncludeList.IsUnknown() { - diags.Append(in.IncludeList.ElementsAs(ctx, &out.IncludeList, false)...) + var includeList []string + diags.Append(in.IncludeList.ElementsAs(ctx, &includeList, false)...) if diags.HasError() { return nil, diags } + if len(includeList) > 0 { + out.IncludeList = includeList + } } if !in.ExcludeList.IsNull() && !in.ExcludeList.IsUnknown() { - diags.Append(in.ExcludeList.ElementsAs(ctx, &out.ExcludeList, false)...) + var excludeList []string + diags.Append(in.ExcludeList.ElementsAs(ctx, &excludeList, false)...) if diags.HasError() { return nil, diags } + if len(excludeList) > 0 { + out.ExcludeList = excludeList + } } + return &out, diags } @@ -97,19 +104,27 @@ func ResourceCheckoutSettingsFromSDKToModel(ctx context.Context, in *pam.Resourc out.CheckoutDurationInSeconds = types.Int32PointerValue(val) } - includeList, d := types.ListValueFrom(ctx, types.StringType, in.IncludeList) - diags.Append(d...) - if diags.HasError() { - return nil, diags + if len(in.IncludeList) == 0 { + out.IncludeList = types.ListNull(types.StringType) + } else { + includeList, d := types.ListValueFrom(ctx, types.StringType, in.IncludeList) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + out.IncludeList = includeList } - out.IncludeList = includeList - excludeList, d := types.ListValueFrom(ctx, types.StringType, in.ExcludeList) - diags.Append(d...) - if diags.HasError() { - return nil, diags + if len(in.ExcludeList) == 0 { + out.ExcludeList = types.ListNull(types.StringType) + } else { + excludeList, d := types.ListValueFrom(ctx, types.StringType, in.ExcludeList) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + out.ExcludeList = excludeList } - out.ExcludeList = excludeList return &out, diags } From 891c133a211a10937c144791fab68c7995621ae7 Mon Sep 17 00:00:00 2001 From: Hanif Jetha Date: Thu, 16 Jan 2025 15:47:38 -0500 Subject: [PATCH 13/13] Run scan on every branch --- .circleci/config.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 788e5525bd..5da204bae6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,7 +56,3 @@ workflows: - reversing-labs: context: - static-analysis - filters: - branches: - only: - - master