diff --git a/examples/README.md b/examples/README.md index 87383eb6d..860a54e6e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -55,6 +55,7 @@ the provider, but we don't quite have the manpower yet to do so. Attribute Schemas. - [okta_user](./okta_user) Supports the management of Okta Users. - [okta_users](./okta_users) Data source to retrieve a group of users. +- [okta_app_oauth_post_logout_redirect_uri](./okta_app_oauth_post_logout_redirect_uri) Supports decentralizing post logout redirect uri config. - [okta_app_oauth_redirect_uri](./okta_app_oauth_redirect_uri) Supports decentralizing redirect uri config. Due to Okta's API not allowing this field to be null, you must set a redirect uri in your app, and ignore changes to this attribute. We follow TF best practices and detect config drift. The best case scenario is Okta makes this field diff --git a/examples/okta_app_oauth_post_logout_redirect_uri/README.md b/examples/okta_app_oauth_post_logout_redirect_uri/README.md new file mode 100644 index 000000000..a53c42874 --- /dev/null +++ b/examples/okta_app_oauth_post_logout_redirect_uri/README.md @@ -0,0 +1,6 @@ +# okta_app_oauth_post_logout_redirect_uri + +Resource to support configuring post logout redirect +uris. [See Okta documentation for more details](https://developer.okta.com/docs/api/resources/apps#settings-7). + +- Simple example [can be found here](./basic.tf) diff --git a/examples/okta_app_oauth_post_logout_redirect_uri/basic.tf b/examples/okta_app_oauth_post_logout_redirect_uri/basic.tf new file mode 100644 index 000000000..7513497c4 --- /dev/null +++ b/examples/okta_app_oauth_post_logout_redirect_uri/basic.tf @@ -0,0 +1,23 @@ +// This would normally be in another repo if you were decentralizing redirect_uri settings +resource "okta_app_oauth" "test" { + label = "testAcc_replace_with_uuid" + type = "web" + grant_types = ["authorization_code"] + response_types = ["code"] + + // Okta requires at least one redirect URI to create an app + redirect_uris = ["myapp://callback"] + + // After logout, Okta redirects users to one of these URIs + post_logout_redirect_uris = ["https://www.example.com"] + + // Since Okta forces us to create it with a redirect URI we have to ignore future changes, they will be detected as config drift. + lifecycle { + ignore_changes = [redirect_uris] + } +} + +resource "okta_app_oauth_post_logout_redirect_uri" "test" { + app_id = okta_app_oauth.test.id + uri = "http://google.com" +} diff --git a/examples/okta_app_oauth_post_logout_redirect_uri/basic_updated.tf b/examples/okta_app_oauth_post_logout_redirect_uri/basic_updated.tf new file mode 100644 index 000000000..55d7031c7 --- /dev/null +++ b/examples/okta_app_oauth_post_logout_redirect_uri/basic_updated.tf @@ -0,0 +1,24 @@ +// This would normally be in another repo if you were decentralizing redirect_uri settings +resource "okta_app_oauth" "test" { + label = "testAcc_replace_with_uuid" + type = "web" + grant_types = ["authorization_code"] + response_types = ["code"] + + // Okta requires at least one redirect URI to create an app + redirect_uris = ["myapp://callback"] + + // After logout, Okta redirects users to one of these URIs + post_logout_redirect_uris = ["https://www.example.com"] + + + // Since Okta forces us to create it with a redirect URI we have to ignore future changes, they will be detected as config drift. + lifecycle { + ignore_changes = [redirect_uris] + } +} + +resource "okta_app_oauth_post_logout_redirect_uri" "test" { + app_id = okta_app_oauth.test.id + uri = "https://www.example-updated.com" +} diff --git a/go.mod b/go.mod index 5168a8d2f..a56b2e87a 100644 --- a/go.mod +++ b/go.mod @@ -56,10 +56,11 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/zclconf/go-cty v1.9.1 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/mod v0.4.0 // indirect golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect golang.org/x/text v0.3.6 // indirect - golang.org/x/tools v0.0.0-20201028111035-eafbe7b904eb // indirect + golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c // indirect google.golang.org/api v0.34.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20211029142109-e255c875f7c7 // indirect diff --git a/go.sum b/go.sum index 01ab724b0..c6df35cdc 100644 --- a/go.sum +++ b/go.sum @@ -443,8 +443,9 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -595,8 +596,8 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201028111035-eafbe7b904eb h1:KVWk3RW1AZlxWum4tYqegLgwJHb5oouozcGM8HfNQaw= -golang.org/x/tools v0.0.0-20201028111035-eafbe7b904eb/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c h1:dS09fXwOFF9cXBnIzZexIuUBj95U1NyQjkEhkgidDow= +golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/okta/internal/mutexkv/mutexkv.go b/okta/internal/mutexkv/mutexkv.go new file mode 100644 index 000000000..a623e12c7 --- /dev/null +++ b/okta/internal/mutexkv/mutexkv.go @@ -0,0 +1,51 @@ +package mutexkv + +import ( + "log" + "sync" +) + +// MutexKV is a simple key/value store for arbitrary mutexes. It can be used to +// serialize changes across arbitrary collaborators that share knowledge of the +// keys they must serialize on. +// +// The initial use case is to let aws_security_group_rule resources serialize +// their access to individual security groups based on SG ID. +type MutexKV struct { + lock sync.Mutex + store map[string]*sync.Mutex +} + +// Locks the mutex for the given key. Caller is responsible for calling Unlock +// for the same key. +func (m *MutexKV) Lock(key string) { + log.Printf("[DEBUG] Locking %q", key) + m.get(key).Lock() + log.Printf("[DEBUG] Locked %q", key) +} + +// Unlock the mutex for the given key. Caller must have called Lock for the same key first. +func (m *MutexKV) Unlock(key string) { + log.Printf("[DEBUG] Unlocking %q", key) + m.get(key).Unlock() + log.Printf("[DEBUG] Unlocked %q", key) +} + +// Returns a mutex for the given key, no guarantee of its lock status. +func (m *MutexKV) get(key string) *sync.Mutex { + m.lock.Lock() + defer m.lock.Unlock() + mutex, ok := m.store[key] + if !ok { + mutex = &sync.Mutex{} + m.store[key] = mutex + } + return mutex +} + +// Returns a properly initialized MutexKV. +func NewMutexKV() *MutexKV { + return &MutexKV{ + store: make(map[string]*sync.Mutex), + } +} diff --git a/okta/internal/mutexkv/mutexkv_test.go b/okta/internal/mutexkv/mutexkv_test.go new file mode 100644 index 000000000..983560074 --- /dev/null +++ b/okta/internal/mutexkv/mutexkv_test.go @@ -0,0 +1,67 @@ +package mutexkv + +import ( + "testing" + "time" +) + +func TestMutexKVLock(t *testing.T) { + mkv := NewMutexKV() + + mkv.Lock("foo") + + doneCh := make(chan struct{}) + + go func() { + mkv.Lock("foo") + close(doneCh) + }() + + select { + case <-doneCh: + t.Fatal("Second lock was able to be taken. This shouldn't happen.") + case <-time.After(50 * time.Millisecond): + // pass + } +} + +func TestMutexKVUnlock(t *testing.T) { + mkv := NewMutexKV() + + mkv.Lock("foo") + mkv.Unlock("foo") + + doneCh := make(chan struct{}) + + go func() { + mkv.Lock("foo") + close(doneCh) + }() + + select { + case <-doneCh: + // pass + case <-time.After(50 * time.Millisecond): + t.Fatal("Second lock blocked after unlock. This shouldn't happen.") + } +} + +func TestMutexKVDifferentKeys(t *testing.T) { + mkv := NewMutexKV() + + mkv.Lock("foo") + + doneCh := make(chan struct{}) + + go func() { + mkv.Lock("bar") + close(doneCh) + }() + + select { + case <-doneCh: + // pass + case <-time.After(50 * time.Millisecond): + t.Fatal("Second lock on a different key blocked. This shouldn't happen.") + } +} diff --git a/okta/provider.go b/okta/provider.go index 8fd2f14be..33eac2802 100644 --- a/okta/provider.go +++ b/okta/provider.go @@ -11,112 +11,115 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/okta/terraform-provider-okta/okta/internal/mutexkv" ) // Resource names, defined in place, used throughout the provider and tests const ( - adminRoleCustom = "okta_admin_role_custom" - adminRoleCustomAssignments = "okta_admin_role_custom_assignments" - adminRoleTargets = "okta_admin_role_targets" - app = "okta_app" - appAutoLogin = "okta_app_auto_login" - appBasicAuth = "okta_app_basic_auth" - appBookmark = "okta_app_bookmark" - appGroupAssignment = "okta_app_group_assignment" - appGroupAssignments = "okta_app_group_assignments" - appMetadataSaml = "okta_app_metadata_saml" - appOAuth = "okta_app_oauth" - appOAuthAPIScope = "okta_app_oauth_api_scope" - appOAuthRedirectURI = "okta_app_oauth_redirect_uri" - appSaml = "okta_app_saml" - appSamlAppSettings = "okta_app_saml_app_settings" - appSecurePasswordStore = "okta_app_secure_password_store" - appSharedCredentials = "okta_app_shared_credentials" - appSignOnPolicy = "okta_app_signon_policy" - appSignOnPolicyRule = "okta_app_signon_policy_rule" - appSwa = "okta_app_swa" - appThreeField = "okta_app_three_field" - appUser = "okta_app_user" - appUserAssignments = "okta_app_user_assignments" - appUserBaseSchemaProperty = "okta_app_user_base_schema_property" - appUserSchemaProperty = "okta_app_user_schema_property" - authenticator = "okta_authenticator" - authServer = "okta_auth_server" - authServerClaim = "okta_auth_server_claim" - authServerClaimDefault = "okta_auth_server_claim_default" - authServerClaims = "okta_auth_server_claims" - authServerDefault = "okta_auth_server_default" - authServerPolicy = "okta_auth_server_policy" - authServerPolicyRule = "okta_auth_server_policy_rule" - authServerScope = "okta_auth_server_scope" - authServerScopes = "okta_auth_server_scopes" - behavior = "okta_behavior" - behaviors = "okta_behaviors" - captcha = "okta_captcha" - captchaOrgWideSettings = "okta_captcha_org_wide_settings" - defaultPolicies = "okta_default_policies" - defaultPolicy = "okta_default_policy" - domain = "okta_domain" - domainCertificate = "okta_domain_certificate" - domainVerification = "okta_domain_verification" - emailSender = "okta_email_sender" - emailSenderVerification = "okta_email_sender_verification" - eventHook = "okta_event_hook" - eventHookVerification = "okta_event_hook_verification" - factor = "okta_factor" - factorTotp = "okta_factor_totp" - group = "okta_group" - groupEveryone = "okta_everyone_group" - groupMembership = "okta_group_membership" - groupMemberships = "okta_group_memberships" - groupRole = "okta_group_role" - groupRoles = "okta_group_roles" - groupRule = "okta_group_rule" - groups = "okta_groups" - groupSchemaProperty = "okta_group_schema_property" - idpMetadataSaml = "okta_idp_metadata_saml" - idpOidc = "okta_idp_oidc" - idpSaml = "okta_idp_saml" - idpSamlKey = "okta_idp_saml_key" - idpSocial = "okta_idp_social" - inlineHook = "okta_inline_hook" - linkDefinition = "okta_link_definition" - linkValue = "okta_link_value" - networkZone = "okta_network_zone" - orgConfiguration = "okta_org_configuration" - orgSupport = "okta_org_support" - policy = "okta_policy" - policyMfa = "okta_policy_mfa" - policyMfaDefault = "okta_policy_mfa_default" - policyPassword = "okta_policy_password" - policyPasswordDefault = "okta_policy_password_default" - policyProfileEnrollment = "okta_policy_profile_enrollment" - policyRuleIdpDiscovery = "okta_policy_rule_idp_discovery" - policyRuleMfa = "okta_policy_rule_mfa" - policyRulePassword = "okta_policy_rule_password" - policyRuleProfileEnrollment = "okta_policy_rule_profile_enrollment" - policyRuleSignOn = "okta_policy_rule_signon" - policySignOn = "okta_policy_signon" - profileMapping = "okta_profile_mapping" - rateLimiting = "okta_rate_limiting" - resourceSet = "okta_resource_set" - roleSubscription = "okta_role_subscription" - securityNotificationEmails = "okta_security_notification_emails" - templateEmail = "okta_template_email" - templateSms = "okta_template_sms" - threatInsightSettings = "okta_threat_insight_settings" - trustedOrigin = "okta_trusted_origin" - trustedOrigins = "okta_trusted_origins" - user = "okta_user" - userAdminRoles = "okta_user_admin_roles" - userBaseSchemaProperty = "okta_user_base_schema_property" - userFactorQuestion = "okta_user_factor_question" - userGroupMemberships = "okta_user_group_memberships" - userProfileMappingSource = "okta_user_profile_mapping_source" - users = "okta_users" - userSchemaProperty = "okta_user_schema_property" - userSecurityQuestions = "okta_user_security_questions" - userType = "okta_user_type" + adminRoleCustom = "okta_admin_role_custom" + adminRoleCustomAssignments = "okta_admin_role_custom_assignments" + adminRoleTargets = "okta_admin_role_targets" + app = "okta_app" + appAutoLogin = "okta_app_auto_login" + appBasicAuth = "okta_app_basic_auth" + appBookmark = "okta_app_bookmark" + appGroupAssignment = "okta_app_group_assignment" + appGroupAssignments = "okta_app_group_assignments" + appMetadataSaml = "okta_app_metadata_saml" + appOAuth = "okta_app_oauth" + appOAuthAPIScope = "okta_app_oauth_api_scope" + appOAuthPostLogoutRedirectURI = "okta_app_oauth_post_logout_redirect_uri" + appOAuthRedirectURI = "okta_app_oauth_redirect_uri" + appSaml = "okta_app_saml" + appSamlAppSettings = "okta_app_saml_app_settings" + appSecurePasswordStore = "okta_app_secure_password_store" + appSharedCredentials = "okta_app_shared_credentials" + appSignOnPolicy = "okta_app_signon_policy" + appSignOnPolicyRule = "okta_app_signon_policy_rule" + appSwa = "okta_app_swa" + appThreeField = "okta_app_three_field" + appUser = "okta_app_user" + appUserAssignments = "okta_app_user_assignments" + appUserBaseSchemaProperty = "okta_app_user_base_schema_property" + appUserSchemaProperty = "okta_app_user_schema_property" + authenticator = "okta_authenticator" + authServer = "okta_auth_server" + authServerClaim = "okta_auth_server_claim" + authServerClaimDefault = "okta_auth_server_claim_default" + authServerClaims = "okta_auth_server_claims" + authServerDefault = "okta_auth_server_default" + authServerPolicy = "okta_auth_server_policy" + authServerPolicyRule = "okta_auth_server_policy_rule" + authServerScope = "okta_auth_server_scope" + authServerScopes = "okta_auth_server_scopes" + behavior = "okta_behavior" + behaviors = "okta_behaviors" + captcha = "okta_captcha" + captchaOrgWideSettings = "okta_captcha_org_wide_settings" + defaultPolicies = "okta_default_policies" + defaultPolicy = "okta_default_policy" + domain = "okta_domain" + domainCertificate = "okta_domain_certificate" + domainVerification = "okta_domain_verification" + emailSender = "okta_email_sender" + emailSenderVerification = "okta_email_sender_verification" + eventHook = "okta_event_hook" + eventHookVerification = "okta_event_hook_verification" + factor = "okta_factor" + factorTotp = "okta_factor_totp" + group = "okta_group" + groupEveryone = "okta_everyone_group" + groupMembership = "okta_group_membership" + groupMemberships = "okta_group_memberships" + groupRole = "okta_group_role" + groupRoles = "okta_group_roles" + groupRule = "okta_group_rule" + groups = "okta_groups" + groupSchemaProperty = "okta_group_schema_property" + idpMetadataSaml = "okta_idp_metadata_saml" + idpOidc = "okta_idp_oidc" + idpSaml = "okta_idp_saml" + idpSamlKey = "okta_idp_saml_key" + idpSocial = "okta_idp_social" + inlineHook = "okta_inline_hook" + linkDefinition = "okta_link_definition" + linkValue = "okta_link_value" + networkZone = "okta_network_zone" + orgConfiguration = "okta_org_configuration" + orgSupport = "okta_org_support" + policy = "okta_policy" + policyMfa = "okta_policy_mfa" + policyMfaDefault = "okta_policy_mfa_default" + policyPassword = "okta_policy_password" + policyPasswordDefault = "okta_policy_password_default" + policyProfileEnrollment = "okta_policy_profile_enrollment" + policyRuleIdpDiscovery = "okta_policy_rule_idp_discovery" + policyRuleMfa = "okta_policy_rule_mfa" + policyRulePassword = "okta_policy_rule_password" + policyRuleProfileEnrollment = "okta_policy_rule_profile_enrollment" + policyRuleSignOn = "okta_policy_rule_signon" + policySignOn = "okta_policy_signon" + profileMapping = "okta_profile_mapping" + rateLimiting = "okta_rate_limiting" + resourceSet = "okta_resource_set" + roleSubscription = "okta_role_subscription" + securityNotificationEmails = "okta_security_notification_emails" + templateEmail = "okta_template_email" + templateSms = "okta_template_sms" + threatInsightSettings = "okta_threat_insight_settings" + trustedOrigin = "okta_trusted_origin" + trustedOrigins = "okta_trusted_origins" + user = "okta_user" + userAdminRoles = "okta_user_admin_roles" + userBaseSchemaProperty = "okta_user_base_schema_property" + userFactorQuestion = "okta_user_factor_question" + userGroupMemberships = "okta_user_group_memberships" + userProfileMappingSource = "okta_user_profile_mapping_source" + users = "okta_users" + userSchemaProperty = "okta_user_schema_property" + userSecurityQuestions = "okta_user_security_questions" + userType = "okta_user_type" ) // Provider establishes a client connection to an okta site @@ -223,91 +226,92 @@ func Provider() *schema.Provider { }, }, ResourcesMap: map[string]*schema.Resource{ - adminRoleCustom: resourceAdminRoleCustom(), - adminRoleCustomAssignments: resourceAdminRoleCustomAssignments(), - adminRoleTargets: resourceAdminRoleTargets(), - appAutoLogin: resourceAppAutoLogin(), - appBasicAuth: resourceAppBasicAuth(), - appBookmark: resourceAppBookmark(), - appGroupAssignment: resourceAppGroupAssignment(), - appGroupAssignments: resourceAppGroupAssignments(), - appOAuth: resourceAppOAuth(), - appOAuthAPIScope: resourceAppOAuthAPIScope(), - appOAuthRedirectURI: resourceAppOAuthRedirectURI(), - appSaml: resourceAppSaml(), - appSamlAppSettings: resourceAppSamlAppSettings(), - appSecurePasswordStore: resourceAppSecurePasswordStore(), - appSharedCredentials: resourceAppSharedCredentials(), - appSignOnPolicyRule: resourceAppSignOnPolicyRule(), - appSwa: resourceAppSwa(), - appThreeField: resourceAppThreeField(), - appUser: resourceAppUser(), - appUserBaseSchemaProperty: resourceAppUserBaseSchemaProperty(), - appUserSchemaProperty: resourceAppUserSchemaProperty(), - authenticator: resourceAuthenticator(), - authServer: resourceAuthServer(), - authServerClaim: resourceAuthServerClaim(), - authServerClaimDefault: resourceAuthServerClaimDefault(), - authServerDefault: resourceAuthServerDefault(), - authServerPolicy: resourceAuthServerPolicy(), - authServerPolicyRule: resourceAuthServerPolicyRule(), - authServerScope: resourceAuthServerScope(), - behavior: resourceBehavior(), - captcha: resourceCaptcha(), - captchaOrgWideSettings: resourceCaptchaOrgWideSettings(), - domain: resourceDomain(), - domainCertificate: resourceDomainCertificate(), - domainVerification: resourceDomainVerification(), - emailSender: resourceEmailSender(), - emailSenderVerification: resourceEmailSenderVerification(), - eventHook: resourceEventHook(), - eventHookVerification: resourceEventHookVerification(), - factor: resourceFactor(), - factorTotp: resourceFactorTOTP(), - group: resourceGroup(), - groupMembership: resourceGroupMembership(), - groupMemberships: resourceGroupMemberships(), - groupRole: resourceGroupRole(), - groupRoles: resourceGroupRoles(), - groupRule: resourceGroupRule(), - groupSchemaProperty: resourceGroupCustomSchemaProperty(), - idpOidc: resourceIdpOidc(), - idpSaml: resourceIdpSaml(), - idpSamlKey: resourceIdpSigningKey(), - idpSocial: resourceIdpSocial(), - inlineHook: resourceInlineHook(), - linkDefinition: resourceLinkDefinition(), - linkValue: resourceLinkValue(), - networkZone: resourceNetworkZone(), - orgConfiguration: resourceOrgConfiguration(), - orgSupport: resourceOrgSupport(), - policyMfa: resourcePolicyMfa(), - policyMfaDefault: resourcePolicyMfaDefault(), - policyPassword: resourcePolicyPassword(), - policyPasswordDefault: resourcePolicyPasswordDefault(), - policyProfileEnrollment: resourcePolicyProfileEnrollment(), - policyRuleIdpDiscovery: resourcePolicyRuleIdpDiscovery(), - policyRuleMfa: resourcePolicyMfaRule(), - policyRulePassword: resourcePolicyPasswordRule(), - policyRuleProfileEnrollment: resourcePolicyProfileEnrollmentRule(), - policyRuleSignOn: resourcePolicySignOnRule(), - policySignOn: resourcePolicySignOn(), - profileMapping: resourceProfileMapping(), - rateLimiting: resourceRateLimiting(), - resourceSet: resourceResourceSet(), - roleSubscription: resourceRoleSubscription(), - securityNotificationEmails: resourceSecurityNotificationEmails(), - templateEmail: resourceTemplateEmail(), - templateSms: resourceTemplateSms(), - threatInsightSettings: resourceThreatInsightSettings(), - trustedOrigin: resourceTrustedOrigin(), - user: resourceUser(), - userAdminRoles: resourceUserAdminRoles(), - userBaseSchemaProperty: resourceUserBaseSchemaProperty(), - userFactorQuestion: resourceUserFactorQuestion(), - userGroupMemberships: resourceUserGroupMemberships(), - userSchemaProperty: resourceUserCustomSchemaProperty(), - userType: resourceUserType(), + adminRoleCustom: resourceAdminRoleCustom(), + adminRoleCustomAssignments: resourceAdminRoleCustomAssignments(), + adminRoleTargets: resourceAdminRoleTargets(), + appAutoLogin: resourceAppAutoLogin(), + appBasicAuth: resourceAppBasicAuth(), + appBookmark: resourceAppBookmark(), + appGroupAssignment: resourceAppGroupAssignment(), + appGroupAssignments: resourceAppGroupAssignments(), + appOAuth: resourceAppOAuth(), + appOAuthAPIScope: resourceAppOAuthAPIScope(), + appOAuthPostLogoutRedirectURI: resourceAppOAuthPostLogoutRedirectURI(), + appOAuthRedirectURI: resourceAppOAuthRedirectURI(), + appSaml: resourceAppSaml(), + appSamlAppSettings: resourceAppSamlAppSettings(), + appSecurePasswordStore: resourceAppSecurePasswordStore(), + appSharedCredentials: resourceAppSharedCredentials(), + appSignOnPolicyRule: resourceAppSignOnPolicyRule(), + appSwa: resourceAppSwa(), + appThreeField: resourceAppThreeField(), + appUser: resourceAppUser(), + appUserBaseSchemaProperty: resourceAppUserBaseSchemaProperty(), + appUserSchemaProperty: resourceAppUserSchemaProperty(), + authenticator: resourceAuthenticator(), + authServer: resourceAuthServer(), + authServerClaim: resourceAuthServerClaim(), + authServerClaimDefault: resourceAuthServerClaimDefault(), + authServerDefault: resourceAuthServerDefault(), + authServerPolicy: resourceAuthServerPolicy(), + authServerPolicyRule: resourceAuthServerPolicyRule(), + authServerScope: resourceAuthServerScope(), + behavior: resourceBehavior(), + captcha: resourceCaptcha(), + captchaOrgWideSettings: resourceCaptchaOrgWideSettings(), + domain: resourceDomain(), + domainCertificate: resourceDomainCertificate(), + domainVerification: resourceDomainVerification(), + emailSender: resourceEmailSender(), + emailSenderVerification: resourceEmailSenderVerification(), + eventHook: resourceEventHook(), + eventHookVerification: resourceEventHookVerification(), + factor: resourceFactor(), + factorTotp: resourceFactorTOTP(), + group: resourceGroup(), + groupMembership: resourceGroupMembership(), + groupMemberships: resourceGroupMemberships(), + groupRole: resourceGroupRole(), + groupRoles: resourceGroupRoles(), + groupRule: resourceGroupRule(), + groupSchemaProperty: resourceGroupCustomSchemaProperty(), + idpOidc: resourceIdpOidc(), + idpSaml: resourceIdpSaml(), + idpSamlKey: resourceIdpSigningKey(), + idpSocial: resourceIdpSocial(), + inlineHook: resourceInlineHook(), + linkDefinition: resourceLinkDefinition(), + linkValue: resourceLinkValue(), + networkZone: resourceNetworkZone(), + orgConfiguration: resourceOrgConfiguration(), + orgSupport: resourceOrgSupport(), + policyMfa: resourcePolicyMfa(), + policyMfaDefault: resourcePolicyMfaDefault(), + policyPassword: resourcePolicyPassword(), + policyPasswordDefault: resourcePolicyPasswordDefault(), + policyProfileEnrollment: resourcePolicyProfileEnrollment(), + policyRuleIdpDiscovery: resourcePolicyRuleIdpDiscovery(), + policyRuleMfa: resourcePolicyMfaRule(), + policyRulePassword: resourcePolicyPasswordRule(), + policyRuleProfileEnrollment: resourcePolicyProfileEnrollmentRule(), + policyRuleSignOn: resourcePolicySignOnRule(), + policySignOn: resourcePolicySignOn(), + profileMapping: resourceProfileMapping(), + rateLimiting: resourceRateLimiting(), + resourceSet: resourceResourceSet(), + roleSubscription: resourceRoleSubscription(), + securityNotificationEmails: resourceSecurityNotificationEmails(), + templateEmail: resourceTemplateEmail(), + templateSms: resourceTemplateSms(), + threatInsightSettings: resourceThreatInsightSettings(), + trustedOrigin: resourceTrustedOrigin(), + user: resourceUser(), + userAdminRoles: resourceUserAdminRoles(), + userBaseSchemaProperty: resourceUserBaseSchemaProperty(), + userFactorQuestion: resourceUserFactorQuestion(), + userGroupMemberships: resourceUserGroupMemberships(), + userSchemaProperty: resourceUserCustomSchemaProperty(), + userType: resourceUserType(), // The day I realized I was naming stuff wrong :'-( "okta_idp": deprecateIncorrectNaming(resourceIdpOidc(), idpOidc), @@ -404,6 +408,9 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{} return &config, nil } +// This is a global MutexKV for use within this plugin. +var oktaMutexKV = mutexkv.NewMutexKV() + func envDefaultSetFunc(k string, dv interface{}) schema.SchemaDefaultFunc { return func() (interface{}, error) { if v := os.Getenv(k); v != "" { diff --git a/okta/resource_okta_app_oauth.go b/okta/resource_okta_app_oauth.go index c6c2bbf61..392540006 100644 --- a/okta/resource_okta_app_oauth.go +++ b/okta/resource_okta_app_oauth.go @@ -219,7 +219,7 @@ func resourceAppOAuth() *schema.Resource { Type: schema.TypeSet, Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, - Description: "List of URIs for redirection after logout", + Description: "List of URIs for redirection after logout. Note: see okta_app_oauth_post_logout_redirect_uri for appending to this list in a decentralized way.", }, "response_types": { Type: schema.TypeSet, diff --git a/okta/resource_okta_app_oauth_post_logout_redirect_uri.go b/okta/resource_okta_app_oauth_post_logout_redirect_uri.go new file mode 100644 index 000000000..d96c77ef8 --- /dev/null +++ b/okta/resource_okta_app_oauth_post_logout_redirect_uri.go @@ -0,0 +1,105 @@ +package okta + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/okta/okta-sdk-golang/v2/okta" +) + +func resourceAppOAuthPostLogoutRedirectURI() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAppOAuthPostLogoutRedirectURICreate, + ReadContext: resourceAppOAuthPostLogoutRedirectURIRead, + UpdateContext: resourceAppOAuthPostLogoutRedirectURIUpdate, + DeleteContext: resourceAppOAuthPostLogoutRedirectURIDelete, + // The id for this is the uri + Importer: createCustomNestedResourceImporter([]string{"app_id", "id"}, "Expecting the following format: /"), + Schema: map[string]*schema.Schema{ + "app_id": { + Required: true, + Type: schema.TypeString, + ForceNew: true, + }, + "uri": { + Required: true, + Type: schema.TypeString, + Description: "Post Logout Redirect URI to append to Okta OIDC application.", + ValidateDiagFunc: stringIsURL(validURLSchemes...), + }, + }, + } +} + +func resourceAppOAuthPostLogoutRedirectURICreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + err := appendPostLogoutRedirectURI(ctx, d, m) + if err != nil { + return diag.Errorf("failed to create post logout redirect URI: %v", err) + } + d.SetId(d.Get("uri").(string)) + return resourceAppOAuthPostLogoutRedirectURIRead(ctx, d, m) +} + +// read does nothing due to the nature of this resource +func resourceAppOAuthPostLogoutRedirectURIRead(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics { + return nil +} + +func resourceAppOAuthPostLogoutRedirectURIUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + if err := appendPostLogoutRedirectURI(ctx, d, m); err != nil { + return diag.Errorf("failed to update post logout redirect URI: %v", err) + } + // Normally not advisable, but ForceNew generated unnecessary calls + d.SetId(d.Get("uri").(string)) + return resourceAppOAuthPostLogoutRedirectURIRead(ctx, d, m) +} + +func resourceAppOAuthPostLogoutRedirectURIDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + appID := d.Get("app_id").(string) + + oktaMutexKV.Lock(appID) + defer oktaMutexKV.Unlock(appID) + + app := okta.NewOpenIdConnectApplication() + err := fetchAppByID(ctx, appID, m, app) + if err != nil { + return diag.Errorf("failed to get application: %v", err) + } + if app.Id == "" { + return diag.Errorf("application with id %s does not exist", appID) + } + if !contains(app.Settings.OauthClient.PostLogoutRedirectUris, d.Id()) { + logger(m).Info(fmt.Sprintf("application with appID %s does not have post logout redirect URI %s", appID, d.Id())) + return nil + } + app.Settings.OauthClient.PostLogoutRedirectUris = remove(app.Settings.OauthClient.PostLogoutRedirectUris, d.Id()) + err = updateAppByID(ctx, appID, m, app) + if err != nil { + return diag.Errorf("failed to delete post logout redirect URI: %v", err) + } + return nil +} + +func appendPostLogoutRedirectURI(ctx context.Context, d *schema.ResourceData, m interface{}) error { + appID := d.Get("app_id").(string) + + oktaMutexKV.Lock(appID) + defer oktaMutexKV.Unlock(appID) + + app := okta.NewOpenIdConnectApplication() + if err := fetchAppByID(ctx, appID, m, app); err != nil { + return err + } + if app.Id == "" { + return fmt.Errorf("application with id %s does not exist", appID) + } + if contains(app.Settings.OauthClient.PostLogoutRedirectUris, d.Id()) { + logger(m).Info(fmt.Sprintf("application with appID %s already has post logout redirect URI %s", appID, d.Id())) + return nil + } + uri := d.Get("uri").(string) + app.Settings.OauthClient.PostLogoutRedirectUris = append(app.Settings.OauthClient.PostLogoutRedirectUris, uri) + return updateAppByID(ctx, appID, m, app) +} diff --git a/okta/resource_okta_app_oauth_post_logout_redirect_uri_test.go b/okta/resource_okta_app_oauth_post_logout_redirect_uri_test.go new file mode 100644 index 000000000..afdf5b2fc --- /dev/null +++ b/okta/resource_okta_app_oauth_post_logout_redirect_uri_test.go @@ -0,0 +1,71 @@ +package okta + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/okta/okta-sdk-golang/v2/okta" +) + +func createPostLogoutRedirectURIExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + missingErr := fmt.Errorf("resource not found: %s", name) + rs, ok := s.RootModule().Resources[name] + if !ok { + return missingErr + } + + uri := rs.Primary.ID + appID := rs.Primary.Attributes["app_id"] + client := getOktaClientFromMetadata(testAccProvider.Meta()) + app := okta.NewOpenIdConnectApplication() + _, response, err := client.Application.GetApplication(context.Background(), appID, app, nil) + + // We don't want to consider a 404 an error in some cases and thus the delineation + if response != nil && response.StatusCode == 404 { + return missingErr + } else if err != nil && contains(app.Settings.OauthClient.PostLogoutRedirectUris, uri) { + return nil + } + + return err + } +} + +func TestAccAppOAuthApplication_postLogoutRedirectCrud(t *testing.T) { + ri := acctest.RandInt() + mgr := newFixtureManager(appOAuthPostLogoutRedirectURI) + config := mgr.GetFixtures("basic.tf", ri, t) + updatedConfig := mgr.GetFixtures("basic_updated.tf", ri, t) + resourceName := fmt.Sprintf("%s.test", appOAuthPostLogoutRedirectURI) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProvidersFactories, + CheckDestroy: createCheckResourceDestroy(appOAuth, createDoesAppExist(okta.NewOpenIdConnectApplication())), + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + createPostLogoutRedirectURIExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "id", "https://www.example.com"), + resource.TestCheckResourceAttr(resourceName, "uri", "https://www.example.com"), + resource.TestCheckResourceAttrSet(resourceName, "app_id"), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + createPostLogoutRedirectURIExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "id", "https://www.example-updated.com"), + resource.TestCheckResourceAttr(resourceName, "uri", "https://www.example-updated.com"), + resource.TestCheckResourceAttrSet(resourceName, "app_id"), + ), + }, + }, + }) +} diff --git a/okta/resource_okta_app_oauth_redirect_uri.go b/okta/resource_okta_app_oauth_redirect_uri.go index 26ebe12b4..d6e3f0d8b 100644 --- a/okta/resource_okta_app_oauth_redirect_uri.go +++ b/okta/resource_okta_app_oauth_redirect_uri.go @@ -58,14 +58,22 @@ func resourceAppOAuthRedirectURIUpdate(ctx context.Context, d *schema.ResourceDa func resourceAppOAuthRedirectURIDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { appID := d.Get("app_id").(string) + + oktaMutexKV.Lock(appID) + defer oktaMutexKV.Unlock(appID) + app := okta.NewOpenIdConnectApplication() err := fetchAppByID(ctx, appID, m, app) if err != nil { return diag.Errorf("failed to get application: %v", err) } - if app.Id == "" || contains(app.Settings.OauthClient.RedirectUris, d.Id()) { + if app.Id == "" { return diag.Errorf("application with id %s does not exist", appID) } + if !contains(app.Settings.OauthClient.RedirectUris, d.Id()) { + logger(m).Info(fmt.Sprintf("application with appID %s does not have redirect URI %s", appID, d.Id())) + return nil + } app.Settings.OauthClient.RedirectUris = remove(app.Settings.OauthClient.RedirectUris, d.Id()) err = updateAppByID(ctx, appID, m, app) if err != nil { @@ -76,6 +84,10 @@ func resourceAppOAuthRedirectURIDelete(ctx context.Context, d *schema.ResourceDa func appendRedirectURI(ctx context.Context, d *schema.ResourceData, m interface{}) error { appID := d.Get("app_id").(string) + + oktaMutexKV.Lock(appID) + defer oktaMutexKV.Unlock(appID) + app := okta.NewOpenIdConnectApplication() if err := fetchAppByID(ctx, appID, m, app); err != nil { return err diff --git a/website/docs/r/app_oauth_post_logout_redirect_uri.html.markdown b/website/docs/r/app_oauth_post_logout_redirect_uri.html.markdown new file mode 100644 index 000000000..24c0c18c0 --- /dev/null +++ b/website/docs/r/app_oauth_post_logout_redirect_uri.html.markdown @@ -0,0 +1,55 @@ +--- +layout: 'okta' +page_title: 'Okta: okta_app_oauth_post_logout_redirect_uri' +sidebar_current: 'docs-okta-resource-app-oauth-post-logout-redirect-uri' +description: |- + Manager app OAuth post logout redirect URI +--- + +# okta_app_oauth_post_logout_redirect_uri + +This resource allows you to manage post logout redirection URI for use in redirect-based flows. + +## Example Usage + +```hcl +resource "okta_app_oauth" "test" { + label = "testAcc_replace_with_uuid" + type = "web" + grant_types = ["authorization_code"] + response_types = ["code"] + + // Okta requires at least one redirect URI to create an app + redirect_uris = ["myapp://callback"] + + post_logout_redirect_uris = ["https://www.example.com"] + + // Since Okta forces us to create it with a redirect URI we have to ignore future changes, they will be detected as config drift. + lifecycle { + ignore_changes = [redirect_uris] + } +} + +resource "okta_app_oauth_post_logout_redirect_uri" "test" { + app_id = okta_app_oauth.test.id + uri = "https://www.example.com" +} +``` + +## Argument Reference + +- `app_id` - (Required) OAuth application ID. + +- `uri` - (Required) Post Logout Redirect URI to append to Okta OIDC application. + +## Attributes Reference + +- `id` - ID of the resource, equals to `uri`. + +## Import + +A post logout redirect URI can be imported via the Okta ID. + +``` +$ terraform import okta_app_oauth_post_logout_redirect_uri.example / +```