diff --git a/examples/okta_authenticator/on_prem_provider_json.tf b/examples/okta_authenticator/on_prem_provider_json.tf new file mode 100644 index 000000000..5a8cd44d4 --- /dev/null +++ b/examples/okta_authenticator/on_prem_provider_json.tf @@ -0,0 +1,17 @@ +resource "okta_authenticator" "test" { + name = "On-Prem MFA" + key = "onprem_mfa" + provider_json = jsonencode( + { + "type": "DEL_OATH", + "configuration": { + "authPort": 999, + "userNameTemplate": { + "template": "global.assign.userName.login" + }, + "hostName": "localhost", + "sharedSecret": "Sh4r3d s3cr3t" + } + } + ) +} \ No newline at end of file diff --git a/go.mod b/go.mod index f517a71aa..b4bff5bf8 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/hashicorp/go-hclog v1.3.1 github.com/hashicorp/go-retryablehttp v0.7.1 github.com/hashicorp/terraform-plugin-sdk/v2 v2.19.0 - github.com/okta/okta-sdk-golang/v2 v2.14.1-0.20221028200237-77af9c89f8f3 + github.com/okta/okta-sdk-golang/v2 v2.14.1-0.20221118044255-7f74a659b1d6 github.com/stretchr/testify v1.8.1 ) diff --git a/go.sum b/go.sum index 323155402..0f0a3aa17 100644 --- a/go.sum +++ b/go.sum @@ -226,6 +226,10 @@ github.com/okta/okta-sdk-golang/v2 v2.14.1-0.20221004202115-54b1d8b60afe h1:sfh3 github.com/okta/okta-sdk-golang/v2 v2.14.1-0.20221004202115-54b1d8b60afe/go.mod h1:dz30v3ctAiMb7jpsCngGfQUAEGm1/NsWT92uTbNDQIs= github.com/okta/okta-sdk-golang/v2 v2.14.1-0.20221028200237-77af9c89f8f3 h1:fSnYLCs/70m2/5kIBd+YJYZpk07G5a7o/r+CCujsbf4= github.com/okta/okta-sdk-golang/v2 v2.14.1-0.20221028200237-77af9c89f8f3/go.mod h1:dz30v3ctAiMb7jpsCngGfQUAEGm1/NsWT92uTbNDQIs= +github.com/okta/okta-sdk-golang/v2 v2.14.1-0.20221117172752-975486ea0e42 h1:ApLoRKqg/Fx5yS2ZoI7VRs1D4IAarOmgfFaxFZhZG34= +github.com/okta/okta-sdk-golang/v2 v2.14.1-0.20221117172752-975486ea0e42/go.mod h1:dz30v3ctAiMb7jpsCngGfQUAEGm1/NsWT92uTbNDQIs= +github.com/okta/okta-sdk-golang/v2 v2.14.1-0.20221118044255-7f74a659b1d6 h1:eEbwfO6G8NcztOgUZ3MiV0Q+K4vYHQFYSIm5SaYfBgY= +github.com/okta/okta-sdk-golang/v2 v2.14.1-0.20221118044255-7f74a659b1d6/go.mod h1:dz30v3ctAiMb7jpsCngGfQUAEGm1/NsWT92uTbNDQIs= github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 h1:pSCLCl6joCFRnjpeojzOpEYs4q7Vditq8fySFG5ap3Y= github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= diff --git a/okta/data_source_okta_authenticator.go b/okta/data_source_okta_authenticator.go index 1d22a2a07..9e4e1e9ae 100644 --- a/okta/data_source_okta_authenticator.go +++ b/okta/data_source_okta_authenticator.go @@ -43,16 +43,21 @@ func dataSourceAuthenticator() *schema.Resource { Computed: true, Description: "Type of the authenticator", }, - "provider_hostname": { + "provider_json": { Type: schema.TypeString, Computed: true, - Description: "Server host name or IP address", + Description: "Authenticator Provider in JSON format", }, "provider_auth_port": { Type: schema.TypeInt, Computed: true, Description: "The RADIUS server port (for example 1812). This is defined when the On-Prem RADIUS server is configured", }, + "provider_hostname": { + Type: schema.TypeString, + Computed: true, + Description: "Server host name or IP address", + }, "provider_instance_id": { Type: schema.TypeString, Computed: true, @@ -103,10 +108,26 @@ func dataSourceAuthenticatorRead(ctx context.Context, d *schema.ResourceData, m _ = d.Set("settings", string(b)) } if authenticator.Provider != nil { + b, _ := json.Marshal(authenticator.Provider) + dataMap := map[string]interface{}{} + _ = json.Unmarshal([]byte(string(b)), &dataMap) + b, _ = json.Marshal(dataMap) + _ = d.Set("provider_json", string(b)) + _ = d.Set("provider_type", authenticator.Provider.Type) - _ = d.Set("provider_hostname", authenticator.Provider.Configuration.HostName) - _ = d.Set("provider_auth_port", authenticator.Provider.Configuration.AuthPort) - _ = d.Set("provider_instance_id", authenticator.Provider.Configuration.InstanceId) + + if authenticator.Type == "security_key" { + _ = d.Set("provider_hostname", authenticator.Provider.Configuration.HostName) + _ = d.Set("provider_auth_port", authenticator.Provider.Configuration.AuthPort) + _ = d.Set("provider_instance_id", authenticator.Provider.Configuration.InstanceId) + } + + if authenticator.Provider.Type == "DUO" { + _ = d.Set("provider_host", authenticator.Provider.Configuration.Host) + _ = d.Set("provider_secret_key", authenticator.Provider.Configuration.SecretKey) + _ = d.Set("provider_integration_key", authenticator.Provider.Configuration.IntegrationKey) + } + if authenticator.Provider.Configuration.UserNameTemplate != nil { _ = d.Set("provider_user_name_template", authenticator.Provider.Configuration.UserNameTemplate.Template) } diff --git a/okta/data_source_okta_authenticator_test.go b/okta/data_source_okta_authenticator_test.go index 865f88f1e..4e2f88eb5 100644 --- a/okta/data_source_okta_authenticator_test.go +++ b/okta/data_source_okta_authenticator_test.go @@ -12,8 +12,8 @@ func TestAccOktaDataSourceAuthenticator_read(t *testing.T) { ri := acctest.RandInt() mgr := newFixtureManager(authenticator) config := mgr.GetFixtures("datasource.tf", ri, t) - resourceName := fmt.Sprintf("data.%s.test", authenticator) - resourceName1 := fmt.Sprintf("data.%s.test_1", authenticator) + resourceName := fmt.Sprintf("data.%s.test", authenticator) // security question + resourceName1 := fmt.Sprintf("data.%s.test_1", authenticator) // okta verify resource.Test(t, resource.TestCase{ PreCheck: testAccPreCheck(t), @@ -23,22 +23,36 @@ func TestAccOktaDataSourceAuthenticator_read(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "id"), - resource.TestCheckResourceAttrSet(resourceName, "key"), - resource.TestCheckResourceAttrSet(resourceName, "name"), - resource.TestCheckResourceAttrSet(resourceName, "status"), - resource.TestCheckResourceAttrSet(resourceName, "settings"), resource.TestCheckResourceAttr(resourceName, "type", "security_question"), resource.TestCheckResourceAttr(resourceName, "key", "security_question"), resource.TestCheckResourceAttr(resourceName, "name", "Security Question"), - resource.TestCheckResourceAttrSet(resourceName1, "id"), - resource.TestCheckResourceAttrSet(resourceName1, "key"), - resource.TestCheckResourceAttrSet(resourceName1, "name"), - resource.TestCheckResourceAttrSet(resourceName1, "status"), - resource.TestCheckResourceAttrSet(resourceName1, "settings"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "settings"), + resource.TestCheckNoResourceAttr(resourceName, "provider"), + resource.TestCheckNoResourceAttr(resourceName, "provider_type"), + resource.TestCheckNoResourceAttr(resourceName, "provider_hostname"), + resource.TestCheckNoResourceAttr(resourceName, "provider_auth_port"), + resource.TestCheckNoResourceAttr(resourceName, "provider_instance_id"), + resource.TestCheckNoResourceAttr(resourceName, "provider_host"), + resource.TestCheckNoResourceAttr(resourceName, "provider_secret_key"), + resource.TestCheckNoResourceAttr(resourceName, "provider_integration_key"), + resource.TestCheckResourceAttr(resourceName1, "type", "app"), resource.TestCheckResourceAttr(resourceName1, "key", "okta_verify"), resource.TestCheckResourceAttr(resourceName1, "name", "Okta Verify"), + resource.TestCheckResourceAttrSet(resourceName1, "id"), + resource.TestCheckResourceAttrSet(resourceName1, "status"), + resource.TestCheckResourceAttrSet(resourceName1, "settings"), + resource.TestCheckNoResourceAttr(resourceName1, "provider"), + resource.TestCheckNoResourceAttr(resourceName1, "provider"), + resource.TestCheckNoResourceAttr(resourceName1, "provider_type"), + resource.TestCheckNoResourceAttr(resourceName1, "provider_hostname"), + resource.TestCheckNoResourceAttr(resourceName1, "provider_auth_port"), + resource.TestCheckNoResourceAttr(resourceName1, "provider_instance_id"), + resource.TestCheckNoResourceAttr(resourceName1, "provider_host"), + resource.TestCheckNoResourceAttr(resourceName1, "provider_secret_key"), + resource.TestCheckNoResourceAttr(resourceName1, "provider_integration_key"), ), }, }, diff --git a/okta/resource_okta_authenticator.go b/okta/resource_okta_authenticator.go index 14c94834c..d44a3e614 100644 --- a/okta/resource_okta_authenticator.go +++ b/okta/resource_okta_authenticator.go @@ -8,6 +8,7 @@ import ( "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" + "github.com/okta/okta-sdk-golang/v2/okta/query" "github.com/okta/terraform-provider-okta/sdk" ) @@ -46,6 +47,27 @@ func resourceAuthenticator() *schema.Resource { return new == "" }, }, + "provider_json": { + Type: schema.TypeString, + Optional: true, + Description: "Provider in JSON format", + ValidateDiagFunc: stringIsJSON, + StateFunc: normalizeDataJSON, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return new == "" + }, + ConflictsWith: []string{ + // general + "provider_auth_port", + "provider_hostname", + "provider_shared_secret", + "provider_user_name_template", + // duo + "provider_host", + "provider_integration_key", + "provider_secret_key", + }, + }, "status": { Type: schema.TypeString, Optional: true, @@ -58,26 +80,63 @@ func resourceAuthenticator() *schema.Resource { Computed: true, Description: "The type of Authenticator", }, - "provider_hostname": { - Type: schema.TypeString, - Optional: true, - Default: "localhost", - Description: "Server host name or IP address", - }, + // General Provider Arguments "provider_auth_port": { - Type: schema.TypeInt, - Optional: true, - Default: 9000, - Description: "The RADIUS server port (for example 1812). This is defined when the On-Prem RADIUS server is configured", - RequiredWith: []string{"provider_hostname"}, + Type: schema.TypeInt, + Optional: true, + Description: "The RADIUS server port (for example 1812). This is defined when the On-Prem RADIUS server is configured", + RequiredWith: []string{"provider_hostname"}, + ConflictsWith: []string{"provider_json"}, + DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool { + if _, ok := d.GetOk("provider_json"); ok { + return true + } + return false + }, + }, + "provider_hostname": { + Type: schema.TypeString, + Optional: true, + Default: "localhost", + Description: "Server host name or IP address", + ConflictsWith: []string{"provider_json"}, }, "provider_shared_secret": { - Type: schema.TypeString, - Sensitive: true, - Optional: true, - Description: "An authentication key that must be defined when the RADIUS server is configured, and must be the same on both the RADIUS client and server.", - RequiredWith: []string{"provider_hostname"}, + Type: schema.TypeString, + Sensitive: true, + Optional: true, + Description: "An authentication key that must be defined when the RADIUS server is configured, and must be the same on both the RADIUS client and server.", + RequiredWith: []string{"provider_hostname"}, + ConflictsWith: []string{"provider_json"}, + }, + "provider_user_name_template": { + Type: schema.TypeString, + Optional: true, + Default: "global.assign.userName.login", + Description: "Format expected by the provider", + RequiredWith: []string{"provider_hostname"}, + ConflictsWith: []string{"provider_json"}, + }, + // DUO specific provider arguments + "provider_host": { + Type: schema.TypeString, + Optional: true, + Description: "The Duo Security API hostname", + ConflictsWith: []string{"provider_json"}, + }, + "provider_integration_key": { + Type: schema.TypeString, + Optional: true, + Description: "The Duo Security integration key", + ConflictsWith: []string{"provider_json"}, + }, + "provider_secret_key": { + Type: schema.TypeString, + Optional: true, + Description: "The Duo Security secret key", + ConflictsWith: []string{"provider_json"}, }, + // General Provider Attributes "provider_instance_id": { Type: schema.TypeString, Computed: true, @@ -86,40 +145,59 @@ func resourceAuthenticator() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "provider_user_name_template": { - Type: schema.TypeString, - Optional: true, - Default: "global.assign.userName.login", - Description: "Format expected by the provider", - RequiredWith: []string{"provider_hostname"}, - }, }, } } -// authenticator API is immutable, create is just a read of the key set on the resource +// resourceAuthenticatorCreate Okta API has an odd notion of create for +// authenticators. If the authenticator doesn't exist then a one time `POST +// /api/v1/authenticators` to create the authenticator (hard create) is to be +// performed. Thereafter, that authenticator is never deleted, it is only +// deactivated (soft delete). Therefore, if the authenticator already exists +// create is just a soft import of an existing authenticator. func resourceAuthenticatorCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { if isClassicOrg(m) { return resourceOIEOnlyFeatureError(authenticator) } - authenticator, err := findAuthenticator(ctx, m, "", d.Get("key").(string)) - if err != nil { - return diag.FromErr(err) + var err error + // soft create if the authenticator already exists + authenticator, _ := findAuthenticator(ctx, m, "", d.Get("key").(string)) + if authenticator == nil { + // otherwise hard create + authenticator, err = buildAuthenticator(d) + if err != nil { + return diag.FromErr(err) + } + activate := (d.Get("status").(string) == statusActive) + qp := &query.Params{ + Activate: boolPtr(activate), + } + authenticator, _, err = getOktaClientFromMetadata(m).Authenticator.CreateAuthenticator(ctx, *authenticator, qp) + if err != nil { + return diag.FromErr(err) + } } + d.SetId(authenticator.Id) + + // If status is defined in the config, and the actual status reported by the + // API is not the same, then toggle the status. Soft update. status, ok := d.GetOk("status") if ok && authenticator.Status != status.(string) { + var err error if status.(string) == statusInactive { - _, _, err = getOktaClientFromMetadata(m).Authenticator.DeactivateAuthenticator(ctx, d.Id()) + authenticator, _, err = getOktaClientFromMetadata(m).Authenticator.DeactivateAuthenticator(ctx, d.Id()) } else { - _, _, err = getOktaClientFromMetadata(m).Authenticator.ActivateAuthenticator(ctx, d.Id()) + authenticator, _, err = getOktaClientFromMetadata(m).Authenticator.ActivateAuthenticator(ctx, d.Id()) } if err != nil { return diag.Errorf("failed to change authenticator status: %v", err) } } - return resourceAuthenticatorRead(ctx, d, m) + + establishAuthenticator(authenticator, d) + return nil } func resourceAuthenticatorRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { @@ -127,39 +205,12 @@ func resourceAuthenticatorRead(ctx context.Context, d *schema.ResourceData, m in return resourceOIEOnlyFeatureError(authenticator) } - authenticator, resp, err := getOktaClientFromMetadata(m).Authenticator.GetAuthenticator(ctx, d.Id()) - if err := suppressErrorOn404(resp, err); err != nil { + authenticator, _, err := getOktaClientFromMetadata(m).Authenticator.GetAuthenticator(ctx, d.Id()) + if err != nil { return diag.Errorf("failed to get authenticator: %v", err) } - _ = d.Set("key", authenticator.Key) - _ = d.Set("name", authenticator.Name) - _ = d.Set("status", authenticator.Status) - _ = d.Set("type", authenticator.Type) - if authenticator.Settings != nil { - b, _ := json.Marshal(authenticator.Settings) - - dataMap := map[string]interface{}{} - _ = json.Unmarshal([]byte(string(b)), &dataMap) - b, _ = json.Marshal(dataMap) - - _ = d.Set("settings", string(b)) - } - if authenticator.Provider != nil { - _ = d.Set("provider_type", authenticator.Provider.Type) - _ = d.Set("provider_hostname", authenticator.Provider.Configuration.HostName) - _ = d.Set("provider_auth_port", authenticator.Provider.Configuration.AuthPort) - _ = d.Set("provider_instance_id", authenticator.Provider.Configuration.InstanceId) - if authenticator.Provider.Configuration.UserNameTemplate != nil { - _ = d.Set("provider_user_name_template", authenticator.Provider.Configuration.UserNameTemplate.Template) - } + establishAuthenticator(authenticator, d) - // Duo specific setup - if authenticator.Provider.Type == "DUO" { - _ = d.Set("provider_host", authenticator.Provider.Configuration.Host) - _ = d.Set("provider_secret_key", authenticator.Provider.Configuration.SecretKey) - _ = d.Set("provider_integration_key", authenticator.Provider.Configuration.IntegrationKey) - } - } return nil } @@ -172,7 +223,11 @@ func resourceAuthenticatorUpdate(ctx context.Context, d *schema.ResourceData, m if err != nil { return diag.FromErr(err) } - _, _, err = getOktaClientFromMetadata(m).Authenticator.UpdateAuthenticator(ctx, d.Id(), *buildAuthenticator(d)) + authenticator, err := buildAuthenticator(d) + if err != nil { + return diag.Errorf("failed to update authenticator: %v", err) + } + _, _, err = getOktaClientFromMetadata(m).Authenticator.UpdateAuthenticator(ctx, d.Id(), *authenticator) if err != nil { return diag.Errorf("failed to update authenticator: %v", err) } @@ -190,16 +245,23 @@ func resourceAuthenticatorUpdate(ctx context.Context, d *schema.ResourceData, m return resourceAuthenticatorRead(ctx, d, m) } -// delete is NOOP, authenticators are immutable for create and delete +// resourceAuthenticatorDelete Delete is soft, authenticators are immutable for +// true delete. However, deactivate the authenticator as a stand in for delete. +// Authenticators that are utilized by existing policies can not be deactivated. func resourceAuthenticatorDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { if isClassicOrg(m) { return resourceOIEOnlyFeatureError(authenticator) } + _, _, err := getOktaClientFromMetadata(m).Authenticator.DeactivateAuthenticator(ctx, d.Id()) + if err != nil { + logger(m).Warn(fmt.Sprintf("Attempted to deactivate authenticator %q as soft delete and received error: %s", d.Get("key"), err)) + } + return nil } -func buildAuthenticator(d *schema.ResourceData) *okta.Authenticator { +func buildAuthenticator(d *schema.ResourceData) (*okta.Authenticator, error) { authenticator := okta.Authenticator{ Type: d.Get("type").(string), Id: d.Id(), @@ -223,7 +285,7 @@ func buildAuthenticator(d *schema.ResourceData) *okta.Authenticator { authenticator.Provider = &okta.AuthenticatorProvider{ Type: d.Get("provider_type").(string), Configuration: &okta.AuthenticatorProviderConfiguration{ - Host: d.Get("provider_hostname").(string), + Host: d.Get("provider_host").(string), SecretKey: d.Get("provider_secret_key").(string), IntegrationKey: d.Get("provider_integration_key").(string), UserNameTemplate: &okta.AuthenticatorProviderConfigurationUserNamePlate{ @@ -232,13 +294,26 @@ func buildAuthenticator(d *schema.ResourceData) *okta.Authenticator { }, } } else { - var settings okta.AuthenticatorSettings if s, ok := d.GetOk("settings"); ok { - _ = json.Unmarshal([]byte(s.(string)), &settings) + var settings okta.AuthenticatorSettings + err := json.Unmarshal([]byte(s.(string)), &settings) + if err != nil { + return nil, err + } + authenticator.Settings = &settings + } + } + + if p, ok := d.GetOk("provider_json"); ok { + var provider okta.AuthenticatorProvider + err := json.Unmarshal([]byte(p.(string)), &provider) + if err != nil { + return nil, err } - authenticator.Settings = &settings + authenticator.Provider = &provider } - return &authenticator + + return &authenticator, nil } func validateAuthenticator(d *schema.ResourceData) error { @@ -253,6 +328,8 @@ func validateAuthenticator(d *schema.ResourceData) error { "'provider_auth_port', 'provider_shared_secret' and 'provider_user_name_template' are required", typ) } } + + typ = d.Get("provider_type").(string) if typ == "DUO" { h := d.Get("provider_host").(string) sk := d.Get("provider_secret_key").(string) @@ -265,3 +342,37 @@ func validateAuthenticator(d *schema.ResourceData) error { } return nil } + +func establishAuthenticator(authenticator *okta.Authenticator, d *schema.ResourceData) { + _ = d.Set("key", authenticator.Key) + _ = d.Set("name", authenticator.Name) + _ = d.Set("status", authenticator.Status) + _ = d.Set("type", authenticator.Type) + if authenticator.Settings != nil { + b, _ := json.Marshal(authenticator.Settings) + dataMap := map[string]interface{}{} + _ = json.Unmarshal([]byte(string(b)), &dataMap) + b, _ = json.Marshal(dataMap) + _ = d.Set("settings", string(b)) + } + + if authenticator.Provider != nil { + _ = d.Set("provider_type", authenticator.Provider.Type) + + if authenticator.Type == "security_key" { + _ = d.Set("provider_hostname", authenticator.Provider.Configuration.HostName) + _ = d.Set("provider_auth_port", authenticator.Provider.Configuration.AuthPort) + _ = d.Set("provider_instance_id", authenticator.Provider.Configuration.InstanceId) + } + + if authenticator.Provider.Configuration.UserNameTemplate != nil { + _ = d.Set("provider_user_name_template", authenticator.Provider.Configuration.UserNameTemplate.Template) + } + + if authenticator.Provider.Type == "DUO" { + _ = d.Set("provider_host", authenticator.Provider.Configuration.Host) + _ = d.Set("provider_secret_key", authenticator.Provider.Configuration.SecretKey) + _ = d.Set("provider_integration_key", authenticator.Provider.Configuration.IntegrationKey) + } + } +} diff --git a/okta/resource_okta_authenticator_test.go b/okta/resource_okta_authenticator_test.go index 65349a26e..f4c04186d 100644 --- a/okta/resource_okta_authenticator_test.go +++ b/okta/resource_okta_authenticator_test.go @@ -6,7 +6,6 @@ import ( "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" ) func TestAccOktaAuthenticator_crud(t *testing.T) { @@ -29,7 +28,7 @@ func TestAccOktaAuthenticator_crud(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "type", "security_question"), resource.TestCheckResourceAttr(resourceName, "key", "security_question"), resource.TestCheckResourceAttr(resourceName, "name", "Security Question"), - testAuthenticatorSettings(resourceName, `{"allowedFor" : "recovery"}`), + testAttributeJSON(resourceName, "settings", `{"allowedFor" : "recovery"}`), ), }, { @@ -39,27 +38,123 @@ func TestAccOktaAuthenticator_crud(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "type", "security_question"), resource.TestCheckResourceAttr(resourceName, "key", "security_question"), resource.TestCheckResourceAttr(resourceName, "name", "Security Question"), - testAuthenticatorSettings(resourceName, `{"allowedFor" : "any"}`), + testAttributeJSON(resourceName, "settings", `{"allowedFor" : "any"}`), ), }, { Config: config, }, + { + Config: ` +resource "okta_authenticator" "test" { + status = "INACTIVE" + name = "Security Question" + key = "security_question" + settings = jsonencode( + { + "allowedFor" : "recovery" + } + ) +} + `, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "status", statusInactive), + ), + }, }, }) } -func testAuthenticatorSettings(name, expectedSettingsJSON string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[name] - if !ok { - return fmt.Errorf("not found: %s", name) - } - actualSettingsJSON := rs.Primary.Attributes["settings"] - eq := areJSONStringsEqual(expectedSettingsJSON, actualSettingsJSON) - if !eq { - return fmt.Errorf("attribute 'settings' expected %q, got %q", expectedSettingsJSON, actualSettingsJSON) +// TestAccOktaAuthenticator_Issue1367_simple +// https://github.com/okta/terraform-provider-okta/issues/1367 +// Google OTP is a simple example of the solution for #1367 +func TestAccOktaAuthenticator_Issue1367_simple(t *testing.T) { + config := ` +resource "okta_authenticator" "google_otp" { + name = "Google Authenticator" + key = "google_otp" + status = "INACTIVE" +} +` + resourceName := fmt.Sprintf("%s.google_otp", authenticator) + + resource.Test(t, resource.TestCase{ + PreCheck: testAccPreCheck(t), + ErrorCheck: testAccErrorChecks(t), + ProviderFactories: testAccProvidersFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "status", statusInactive), + resource.TestCheckResourceAttr(resourceName, "type", "app"), + resource.TestCheckResourceAttr(resourceName, "key", "google_otp"), + resource.TestCheckResourceAttr(resourceName, "name", "Google Authenticator"), + ), + }, + }, + }) +} + +// TestAccOktaAuthenticator_Issue1367_provider_json +// https://github.com/okta/terraform-provider-okta/issues/1367 +// Demonstrates provider input as freeform JSON +// Example from `POST /api/v1/authenticator` API docs +// https://developer.okta.com/docs/reference/api/authenticators-admin/#create-authenticator +func TestAccOktaAuthenticator_Issue1367_provider_json(t *testing.T) { + config := ` +resource "okta_authenticator" "test" { + name = "On-Prem MFA" + key = "onprem_mfa" + provider_json = jsonencode( + { + "type": "DEL_OATH", + "configuration": { + "authPort": 999, + "userNameTemplate": { + "template": "global.assign.userName.login" + }, + "hostName": "localhost", + "sharedSecret": "Sh4r3d s3cr3t" } - return nil - } + } + ) +}` + resourceName := fmt.Sprintf("%s.test", authenticator) + + resource.Test(t, resource.TestCase{ + PreCheck: testAccPreCheck(t), + ErrorCheck: testAccErrorChecks(t), + ProviderFactories: testAccProvidersFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "status", statusActive), + resource.TestCheckResourceAttr(resourceName, "type", "security_key"), + resource.TestCheckResourceAttr(resourceName, "key", "onprem_mfa"), + resource.TestCheckResourceAttr(resourceName, "name", "On-Prem MFA"), + testAttributeJSON(resourceName, "profile_json", `{ + { + "type": "DEL_OATH", + "configuration": { + "authPort": 999, + "userNameTemplate": { + "template": "global.assign.userName.login" + }, + "hostName": "localhost", + "sharedSecret": "Sh4r3d s3cr3t" + } + }`), + resource.TestCheckResourceAttr(resourceName, "provider_type", "DEL_OATH"), + resource.TestCheckResourceAttr(resourceName, "provider_hostname", "localhost"), + resource.TestCheckResourceAttr(resourceName, "provider_auth_port", "999"), + resource.TestCheckResourceAttrSet(resourceName, "provider_instance_id"), + resource.TestCheckResourceAttr(resourceName, "provider_user_name_template", "global.assign.userName.login"), + ), + }, + }, + }) } diff --git a/okta/utils_for_test.go b/okta/utils_for_test.go index 8c63b6c2c..82b11954e 100644 --- a/okta/utils_for_test.go +++ b/okta/utils_for_test.go @@ -222,3 +222,20 @@ func orgAdminOnlyTest(t *testing.T) bool { } return allow } + +// testAttributeJSON Deep equal of the JSON at named resource attribute witht he +// expected JSON +func testAttributeJSON(name, attribute, expectedJSON string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + actualJSON := rs.Primary.Attributes[attribute] + eq := areJSONStringsEqual(expectedJSON, actualJSON) + if !eq { + return fmt.Errorf("attribute '%s' in '%s' expected %q, got %q", attribute, name, expectedJSON, actualJSON) + } + return nil + } +} diff --git a/website/docs/d/authenticator.html.markdown b/website/docs/d/authenticator.html.markdown index 854dddd3d..e3373e0cc 100644 --- a/website/docs/d/authenticator.html.markdown +++ b/website/docs/d/authenticator.html.markdown @@ -30,22 +30,36 @@ data "okta_authenticator" "test" { The following arguments are supported: +- `id` - (Optional) ID of the authenticator. + - `key` (Optional) A human-readable string that identifies the authenticator. - `name` - (Optional) Name of the authenticator. -- `id` - (Optional) ID of the authenticator. - ## Attributes Reference - `id` - ID of the authenticator. -- `type` - Type of the Authenticator. - - `name` - Name of the authenticator. -- `type` - Type of the Authenticator. +- `provider_auth_port` - (Specific to `security_key`) The provider server port (for example 1812). + +- `provider_hostname` - (Specific to `security_key`) Server host name or IP address. + +- `provider_instance_id` - (Specific to `security_key`) App Instance ID. + +- `provider_type` - Provider type. + +- `provider_user_name_template` - Username template expected by the provider. + +- `provider_host` - (Specific to `DUO` provider) The Duo Security API hostname. + +- `provider_integration_key` - (Specific to `DUO` provider) The Duo Security integration key. + +- `provider_secret_key` - (Specific to `DUO` provider) The Duo Security secret key. -- `settings` - Settings for the authenticator. +- `settings` - Settings for the authenticator (expressed in JSON). +- `status` - Status of the Authenticator. +- `type` - The type of Authenticator. diff --git a/website/docs/r/authenticator.html.markdown b/website/docs/r/authenticator.html.markdown index 2c5e2af94..9ff061648 100644 --- a/website/docs/r/authenticator.html.markdown +++ b/website/docs/r/authenticator.html.markdown @@ -12,7 +12,16 @@ description: |- This resource allows you to configure different authenticators. --> **NOTE:** An authenticator can only be deleted if it's not in use by any policy. +-> **Create:** The Okta API has an odd notion of create for authenticators. If +the authenticator doesn't exist then a one time `POST /api/v1/authenticators` to +create the authenticator (hard create) will be performed. Thereafter, that +authenticator is never deleted, it is only deactivated (soft delete). Therefore, +if the authenticator already exists create is just a soft import of an existing +authenticator. + +-> **Delete:** Authenticators can not be truly deleted therefore delete is soft. +Delete will attempt to deativate the authenticator. An authenticator can only be +deactivated if it's not in use by any other policy. ## Example Usage @@ -38,25 +47,39 @@ The following arguments are supported: - `status` - (Optional) Status of the authenticator. Default is `ACTIVE`. -- `settings` - (Optional) Settings for the authenticator. Settings object contains values based on Authenticator key. It is not used for authenticators with type `"security_key"`. +- `settings` - (Optional) Settings for the authenticator. The settings JSON contains values based on Authenticator key. It is not used for authenticators with type `"security_key"`. + +- `provider_json` - (Optional) Provider JSON allows for expressive provider +values. This argument conflicts with the other `provider_xxx` arguments. The +[Create +Provider](https://developer.okta.com/docs/reference/api/authenticators-admin/#request) +illustrates detailed provider values for a Duo authenticator. [Provider +values](https://developer.okta.com/docs/reference/api/authenticators-admin/#authenticators-administration-api-object) +are listed in Okta API. +- `provider_auth_port` - (Optional) The RADIUS server port (for example 1812). This is defined when the On-Prem RADIUS server is configured. Used only for authenticators with type `"security_key"`. Conflicts with `provider_json` argument. + +- `provider_hostname` - (Optional) Server host name or IP address. Default is `"localhost"`. Used only for authenticators with type `"security_key"`. Conflicts with `provider_json` argument. + + +- `provider_shared_secret` - (Optional) An authentication key that must be defined when the RADIUS server is configured, and must be the same on both the RADIUS client and server. Used only for authenticators with type `"security_key"`. Conflicts with `provider_json` argument. -- `provider_hostname` - (Optional) Server host name or IP address. Default is `"localhost"`. Used only for authenticators with type `"security_key"`. +- `provider_user_name_template` - (Optional) Username template expected by the provider. Used only for authenticators with type `"security_key"`. Conflicts with `provider_json` argument. -- `provider_auth_port` - (Optional) The RADIUS server port (for example 1812). This is defined when the On-Prem RADIUS server is configured. Default is `9000`. Used only for authenticators with type `"security_key"`. +- `provider_host` - (Optional) (DUO specific) - The Duo Security API hostname". Conflicts with `provider_json` argument. -- `provider_shared_secret` - (Optional) An authentication key that must be defined when the RADIUS server is configured, and must be the same on both the RADIUS client and server. Used only for authenticators with type `"security_key"`. +- `provider_integration_key` - (Optional) (DUO specific) - The Duo Security integration key. Conflicts with `provider_json` argument. -- `provider_user_name_template` - (Optional) Username template expected by the provider. Used only for authenticators with type `"security_key"`. +- `provider_secret_key` - (Optional) (DUO specific) - The Duo Security secret key. Conflicts with `provider_json` argument. ## Attributes Reference - `id` - ID of the authenticator. -- `type` - Type of the Authenticator. +- `type` - The type of Authenticator. Values include: `"password"`, `"security_question"`, `"phone"`, `"email"`, `"app"`, `"federated"`, and `"security_key"`. - `provider_instance_id` - App Instance ID. -- `provider_type` - The type of Authenticator. Values include: `"password"`, `"security_question"`, `"phone"`, `"email"`, `"app"`, `"federated"`, and `"security_key"`. +- `provider_type` - Provider type. Supported value for Duo: `DUO`. Supported value for Custom App: `PUSH` ## Import