From c7e0c8ff77fed66bf73d1969f8629831b4391112 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Tue, 29 Mar 2022 17:27:18 -0700 Subject: [PATCH 1/7] added support for Okta, TOTP and PingID MFA methods --- vault/provider.go | 15 +++ vault/resource_mfa_okta.go | 143 ++++++++++++++++++++++++++++ vault/resource_mfa_okta_test.go | 50 ++++++++++ vault/resource_mfa_pingid.go | 123 ++++++++++++++++++++++++ vault/resource_mfa_pingid_test.go | 49 ++++++++++ vault/resource_mfa_totp.go | 152 ++++++++++++++++++++++++++++++ vault/resource_mfa_totp_test.go | 45 +++++++++ 7 files changed, 577 insertions(+) create mode 100644 vault/resource_mfa_okta.go create mode 100644 vault/resource_mfa_okta_test.go create mode 100644 vault/resource_mfa_pingid.go create mode 100644 vault/resource_mfa_pingid_test.go create mode 100644 vault/resource_mfa_totp.go create mode 100644 vault/resource_mfa_totp_test.go diff --git a/vault/provider.go b/vault/provider.go index 5fa393946..709394f25 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -549,6 +549,21 @@ var ( PathInventory: []string{"/sys/mfa/method/duo/{name}"}, EnterpriseOnly: true, }, + "vault_mfa_okta": { + Resource: mfaOktaResource(), + PathInventory: []string{"/sys/mfa/method/okta/{name}"}, + EnterpriseOnly: true, + }, + "vault_mfa_totp": { + Resource: mfaTOTPResource(), + PathInventory: []string{"/sys/mfa/method/totp/{name}"}, + EnterpriseOnly: true, + }, + "vault_mfa_pingid": { + Resource: mfaPingIDResource(), + PathInventory: []string{"/sys/mfa/method/totp/{name}"}, + EnterpriseOnly: true, + }, "vault_mount": { Resource: MountResource(), PathInventory: []string{"/sys/mounts/{path}"}, diff --git a/vault/resource_mfa_okta.go b/vault/resource_mfa_okta.go new file mode 100644 index 000000000..6ea110700 --- /dev/null +++ b/vault/resource_mfa_okta.go @@ -0,0 +1,143 @@ +package vault + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/vault/api" +) + +func mfaOktaResource() *schema.Resource { + return &schema.Resource{ + Create: mfaOktaWrite, + Update: mfaOktaWrite, + Delete: mfaOktaDelete, + Read: mfaOktaRead, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the MFA method.", + ValidateFunc: validateNoTrailingSlash, + }, + "mount_accessor": { + Type: schema.TypeString, + Required: true, + Description: "The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.", + }, + "username_format": { + Type: schema.TypeString, + Optional: true, + Description: "A format string for mapping Identity names to MFA method names. Values to substitute should be placed in `{{}}`.", + }, + "org_name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the organization to be used in the Okta API.", + }, + "api_token": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "Okta API key.", + }, + "base_url": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "If set, will be used as the base domain for API requests.", + }, + "primary_email": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "If set, the username will only match the primary email for the account.", + }, + }, + } +} + +func mfaOktaPath(name string) string { + return "sys/mfa/method/okta/" + strings.Trim(name, "/") +} + +func mfaOktaRequestData(d *schema.ResourceData) map[string]interface{} { + data := map[string]interface{}{} + + nonBooleanAPIFields := []string{ + "name", "mount_accessor", "username_format", + "org_name", "api_token", "base_url", + } + + if v, ok := d.GetOkExists("primary_email"); ok { + data["primary_email"] = v.(string) + } + + for _, k := range nonBooleanAPIFields { + if v, ok := d.GetOk(k); ok { + data[k] = v + } + } + + return data +} + +func mfaOktaWrite(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + name := d.Get("name").(string) + path := mfaOktaPath(name) + + log.Printf("[DEBUG] Creating mfaOkta %s in Vault", name) + _, err := client.Logical().Write(path, mfaOktaRequestData(d)) + if err != nil { + return fmt.Errorf("error writing to Vault at %s, err=%w", path, err) + } + + d.SetId(path) + + return mfaOktaRead(d, meta) +} + +func mfaOktaRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + log.Printf("[DEBUG] Reading MFA Okta config %q", path) + resp, err := client.Logical().Read(path) + if err != nil { + return fmt.Errorf("error reading from Vault at %s, err=%w", path, err) + } + + fields := []string{ + "name", "mount_accessor", "username_format", + "org_name", "base_url", "primary_email", + } + + for _, k := range fields { + if err := d.Set(k, resp.Data[k]); err != nil { + return err + } + } + + return nil +} + +func mfaOktaDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + log.Printf("[DEBUG] Deleting mfaOkta %s from Vault", path) + + _, err := client.Logical().Delete(path) + if err != nil { + return fmt.Errorf("error deleting from Vault at %s, err=%w", path, err) + } + + return nil +} diff --git a/vault/resource_mfa_okta_test.go b/vault/resource_mfa_okta_test.go new file mode 100644 index 000000000..8646ceac1 --- /dev/null +++ b/vault/resource_mfa_okta_test.go @@ -0,0 +1,50 @@ +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestMFAOktaBasic(t *testing.T) { + mfaOktaPath := acctest.RandomWithPrefix("mfa-okta") + resourceName := "vault_mfa_okta.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testutil.TestEntPreCheck(t) }, + Providers: testProviders, + Steps: []resource.TestStep{ + { + Config: testMFAOktaConfig(mfaOktaPath), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", mfaOktaPath), + resource.TestCheckResourceAttr(resourceName, "username_format", "user@example.com"), + resource.TestCheckResourceAttr(resourceName, "org_name", "hashicorp"), + ), + }, + }, + }) +} + +func testMFAOktaConfig(path string) string { + userPassPath := acctest.RandomWithPrefix("userpass") + + return fmt.Sprintf(` +resource "vault_auth_backend" "userpass" { + type = "userpass" + path = %q +} + +resource "vault_mfa_okta" "test" { + name = %q + mount_accessor = vault_auth_backend.userpass.accessor + username_format = "user@example.com" + org_name = "hashicorp" + api_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +} +`, userPassPath, path) +} diff --git a/vault/resource_mfa_pingid.go b/vault/resource_mfa_pingid.go new file mode 100644 index 000000000..c076aa74a --- /dev/null +++ b/vault/resource_mfa_pingid.go @@ -0,0 +1,123 @@ +package vault + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/vault/api" +) + +func mfaPingIDResource() *schema.Resource { + return &schema.Resource{ + Create: mfaPingIDWrite, + Update: mfaPingIDWrite, + Delete: mfaPingIDDelete, + Read: mfaPingIDRead, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the MFA method.", + ValidateFunc: validateNoTrailingSlash, + }, + "mount_accessor": { + Type: schema.TypeString, + Required: true, + Description: "The mount to tie this method to for use in automatic mappings. " + + "The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.", + }, + "username_format": { + Type: schema.TypeString, + Optional: true, + Description: "A format string for mapping Identity names to MFA method names. Values to substitute should be placed in `{{}}`.", + }, + "settings_file_base64": { + Type: schema.TypeString, + Required: true, + Description: "A base64-encoded third-party settings file retrieved from PingID's configuration page.", + }, + }, + } +} + +func mfaPingIDPath(name string) string { + return "sys/mfa/method/pingid/" + strings.Trim(name, "/") +} + +func mfaPingIDRequestData(d *schema.ResourceData) map[string]interface{} { + data := map[string]interface{}{} + + // Read does not return any API Fields listed in docs + // TODO confirm expected behavior + fields := []string{ + "name", "mount_accessor", "settings_file_base64", + } + + for _, k := range fields { + if v, ok := d.GetOk(k); ok { + data[k] = v + } + } + + return data +} + +func mfaPingIDWrite(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + name := d.Get("name").(string) + path := mfaPingIDPath(name) + + log.Printf("[DEBUG] Creating mfaPingID %s in Vault", name) + _, err := client.Logical().Write(path, mfaPingIDRequestData(d)) + if err != nil { + return fmt.Errorf("error writing to Vault at %s, err=%w", path, err) + } + + d.SetId(path) + + return mfaPingIDRead(d, meta) +} + +func mfaPingIDRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + log.Printf("[DEBUG] Reading MFA PingID config %q", path) + resp, err := client.Logical().Read(path) + if err != nil { + return fmt.Errorf("error reading from Vault at %s, err=%w", path, err) + } + + fields := []string{ + "name", "mount_accessor", "username_format", + "settings_file_base64", + } + + for _, k := range fields { + if err := d.Set(k, resp.Data[k]); err != nil { + return err + } + } + + return nil +} + +func mfaPingIDDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + log.Printf("[DEBUG] Deleting mfaPingID %s from Vault", path) + + _, err := client.Logical().Delete(path) + if err != nil { + return fmt.Errorf("error deleting from Vault at %s, err=%w", path, err) + } + + return nil +} diff --git a/vault/resource_mfa_pingid_test.go b/vault/resource_mfa_pingid_test.go new file mode 100644 index 000000000..ada0f3af4 --- /dev/null +++ b/vault/resource_mfa_pingid_test.go @@ -0,0 +1,49 @@ +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestMFAPingIDBasic(t *testing.T) { + mfaPingIDPath := acctest.RandomWithPrefix("mfa-pingid") + // Base64 Encoded string taken from Vault repo example + settingsFile := "I0F1dG8tR2VuZXJhdGVkIGZyb20gUGluZ09uZSwgZG93bmxvYWRlZCBieSBpZD1bU1NPXSBlbWFpbD1baGFtaWRAaGFzaGljb3JwLmNvbV0KI1dlZCBEZWMgMTUgMTM6MDg6NDQgTVNUIDIwMjEKdXNlX2Jhc2U2NF9rZXk9YlhrdGMyVmpjbVYwTFd0bGVRPT0KdXNlX3NpZ25hdHVyZT10cnVlCnRva2VuPWxvbC10b2tlbgppZHBfdXJsPWh0dHBzOi8vaWRweG55bDNtLnBpbmdpZGVudGl0eS5jb20vcGluZ2lkCm9yZ19hbGlhcz1sb2wtb3JnLWFsaWFzCmFkbWluX3VybD1odHRwczovL2lkcHhueWwzbS5waW5naWRlbnRpdHkuY29tL3BpbmdpZAphdXRoZW50aWNhdG9yX3VybD1odHRwczovL2F1dGhlbnRpY2F0b3IucGluZ29uZS5jb20vcGluZ2lkL3BwbQ==" + resourceName := "vault_mfa_pingid.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testutil.TestEntPreCheck(t) }, + Providers: testProviders, + Steps: []resource.TestStep{ + { + Config: testMFAPingIDConfig(mfaPingIDPath, settingsFile), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", mfaPingIDPath), + ), + }, + }, + }) +} + +func testMFAPingIDConfig(path, file string) string { + userPassPath := acctest.RandomWithPrefix("userpass") + + return fmt.Sprintf(` +resource "vault_auth_backend" "userpass" { + type = "userpass" + path = %q +} + +resource "vault_mfa_pingid" "test" { + name = %q + mount_accessor = vault_auth_backend.userpass.accessor + username_format = "user@example.com" + settings_file_base64 = %q +} +`, userPassPath, path, file) +} diff --git a/vault/resource_mfa_totp.go b/vault/resource_mfa_totp.go new file mode 100644 index 000000000..ffb1f08c0 --- /dev/null +++ b/vault/resource_mfa_totp.go @@ -0,0 +1,152 @@ +package vault + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/vault/api" +) + +func mfaTOTPResource() *schema.Resource { + return &schema.Resource{ + Create: mfaTOTPWrite, + Update: mfaTOTPWrite, + Delete: mfaTOTPDelete, + Read: mfaTOTPRead, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the MFA method.", + ValidateFunc: validateNoTrailingSlash, + }, + "issuer": { + Type: schema.TypeString, + Required: true, + Description: "The name of the key's issuing organization.", + }, + "period": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "The length of time used to generate a counter for the TOTP token calculation.", + }, + "key_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Specifies the size in bytes of the generated key.", + }, + "qr_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "The pixel size of the generated square QR code.", + }, + "algorithm": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Specifies the hashing algorithm used to generate the TOTP code. " + + "Options include 'SHA1', 'SHA256' and 'SHA512'.", + }, + "digits": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "The number of digits in the generated TOTP token. " + + "This value can either be 6 or 8.", + }, + "skew": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "The number of delay periods that are allowed when validating a TOTP token. " + + "This value can either be 0 or 1.", + }, + }, + } +} + +func mfaTOTPPath(name string) string { + return "sys/mfa/method/totp/" + strings.Trim(name, "/") +} + +func mfaTOTPRequestData(d *schema.ResourceData) map[string]interface{} { + data := map[string]interface{}{} + + fields := []string{ + "name", "issuer", "period", + "key_size", "qr_size", "algorithm", + "digits", "skew", + } + + for _, k := range fields { + if v, ok := d.GetOk(k); ok { + data[k] = v + } + } + + return data +} + +func mfaTOTPWrite(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + name := d.Get("name").(string) + path := mfaTOTPPath(name) + + log.Printf("[DEBUG] Creating mfaTOTP %s in Vault", name) + _, err := client.Logical().Write(path, mfaTOTPRequestData(d)) + if err != nil { + return fmt.Errorf("error writing to Vault at %s, err=%w", path, err) + } + + d.SetId(path) + + return mfaTOTPRead(d, meta) +} + +func mfaTOTPRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + log.Printf("[DEBUG] Reading MFA TOTP config %q", path) + resp, err := client.Logical().Read(path) + if err != nil { + return fmt.Errorf("error reading from Vault at %s, err=%w", path, err) + } + + fields := []string{ + "name", "issuer", "period", + "key_size", "qr_size", "algorithm", + "digits", "skew", + } + + for _, k := range fields { + if err := d.Set(k, resp.Data[k]); err != nil { + return err + } + } + + return nil +} + +func mfaTOTPDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + log.Printf("[DEBUG] Deleting mfaTOTP %s from Vault", path) + + _, err := client.Logical().Delete(path) + if err != nil { + return fmt.Errorf("error deleting from Vault at %s, err=%w", path, err) + } + + return nil +} diff --git a/vault/resource_mfa_totp_test.go b/vault/resource_mfa_totp_test.go new file mode 100644 index 000000000..d3a591fbf --- /dev/null +++ b/vault/resource_mfa_totp_test.go @@ -0,0 +1,45 @@ +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestMFATOTPBasic(t *testing.T) { + mfaTOTPPath := acctest.RandomWithPrefix("mfa-totp") + resourceName := "vault_mfa_totp.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testutil.TestEntPreCheck(t) }, + Providers: testProviders, + Steps: []resource.TestStep{ + { + Config: testMFATOTPConfig(mfaTOTPPath), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", mfaTOTPPath), + resource.TestCheckResourceAttr(resourceName, "issuer", "hashicorp"), + resource.TestCheckResourceAttr(resourceName, "period", "60"), + resource.TestCheckResourceAttr(resourceName, "algorithm", "SHA256"), + resource.TestCheckResourceAttr(resourceName, "digits", "8"), + ), + }, + }, + }) +} + +func testMFATOTPConfig(path string) string { + return fmt.Sprintf(` +resource "vault_mfa_totp" "test" { + name = %q + issuer = "hashicorp" + period = 60 + algorithm = "SHA256" + digits = 8 +} +`, path) +} From 6807afd305855b77044257357fc9183f5549d761 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Thu, 31 Mar 2022 13:49:38 -0700 Subject: [PATCH 2/7] add computed config fields from Vault for pingId --- vault/resource_mfa_pingid.go | 46 +++++++++++++++++++++++++++++-- vault/resource_mfa_pingid_test.go | 3 ++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/vault/resource_mfa_pingid.go b/vault/resource_mfa_pingid.go index c076aa74a..155dcd87c 100644 --- a/vault/resource_mfa_pingid.go +++ b/vault/resource_mfa_pingid.go @@ -42,6 +42,46 @@ func mfaPingIDResource() *schema.Resource { Required: true, Description: "A base64-encoded third-party settings file retrieved from PingID's configuration page.", }, + "idp_url": { + Type: schema.TypeString, + Computed: true, + Description: "IDP URL computed by Vault.", + }, + "admin_url": { + Type: schema.TypeString, + Computed: true, + Description: "Admin URL computed by Vault.", + }, + "authenticator_url": { + Type: schema.TypeString, + Computed: true, + Description: "Authenticator URL computed by Vault.", + }, + "org_alias": { + Type: schema.TypeString, + Computed: true, + Description: "Org Alias computed by Vault.", + }, + "id": { + Type: schema.TypeString, + Computed: true, + Description: "ID computed by Vault.", + }, + "namespace_id": { + Type: schema.TypeString, + Computed: true, + Description: "Namespace ID computed by Vault.", + }, + "type": { + Type: schema.TypeString, + Computed: true, + Description: "Type of configuration computed by Vault.", + }, + "use_signature": { + Type: schema.TypeBool, + Computed: true, + Description: "If set, enables use of PingID signature. Computed by Vault", + }, }, } } @@ -95,8 +135,10 @@ func mfaPingIDRead(d *schema.ResourceData, meta interface{}) error { } fields := []string{ - "name", "mount_accessor", "username_format", - "settings_file_base64", + "name", "idp_url", "admin_url", + "authenticator_url", "org_alias", "type", + "use_signature", "id", "namespace_id", + // "mount_accessor", "username_format", "settings_file_base64", } for _, k := range fields { diff --git a/vault/resource_mfa_pingid_test.go b/vault/resource_mfa_pingid_test.go index ada0f3af4..4ffd5c56f 100644 --- a/vault/resource_mfa_pingid_test.go +++ b/vault/resource_mfa_pingid_test.go @@ -24,6 +24,9 @@ func TestMFAPingIDBasic(t *testing.T) { Config: testMFAPingIDConfig(mfaPingIDPath, settingsFile), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", mfaPingIDPath), + resource.TestCheckResourceAttr(resourceName, "type", "pingid"), + resource.TestCheckResourceAttr(resourceName, "use_signature", "true"), + resource.TestCheckResourceAttr(resourceName, "namespace_id", ""), ), }, }, From 3a9515793a8230082d10ee4618c7a7c392252452 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Thu, 31 Mar 2022 13:58:26 -0700 Subject: [PATCH 3/7] add username_format in write opern --- vault/resource_mfa_pingid.go | 1 + vault/resource_mfa_pingid_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/vault/resource_mfa_pingid.go b/vault/resource_mfa_pingid.go index 155dcd87c..8abeed8e5 100644 --- a/vault/resource_mfa_pingid.go +++ b/vault/resource_mfa_pingid.go @@ -97,6 +97,7 @@ func mfaPingIDRequestData(d *schema.ResourceData) map[string]interface{} { // TODO confirm expected behavior fields := []string{ "name", "mount_accessor", "settings_file_base64", + "username_format", } for _, k := range fields { diff --git a/vault/resource_mfa_pingid_test.go b/vault/resource_mfa_pingid_test.go index 4ffd5c56f..c15a77ee5 100644 --- a/vault/resource_mfa_pingid_test.go +++ b/vault/resource_mfa_pingid_test.go @@ -24,6 +24,7 @@ func TestMFAPingIDBasic(t *testing.T) { Config: testMFAPingIDConfig(mfaPingIDPath, settingsFile), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", mfaPingIDPath), + resource.TestCheckResourceAttr(resourceName, "username_format", "user@example.com"), resource.TestCheckResourceAttr(resourceName, "type", "pingid"), resource.TestCheckResourceAttr(resourceName, "use_signature", "true"), resource.TestCheckResourceAttr(resourceName, "namespace_id", ""), From 5d506d7805476b2825f935a362fdf68fc16a022b Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Fri, 1 Apr 2022 12:34:55 -0700 Subject: [PATCH 4/7] add import steps to MFA tests --- vault/resource_mfa_okta.go | 19 ++++++++++--------- vault/resource_mfa_okta_test.go | 16 ++++++++++------ vault/resource_mfa_pingid_test.go | 16 ++++++++++------ vault/resource_mfa_totp_test.go | 23 +++++++++++++++-------- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/vault/resource_mfa_okta.go b/vault/resource_mfa_okta.go index 6ea110700..65e56caca 100644 --- a/vault/resource_mfa_okta.go +++ b/vault/resource_mfa_okta.go @@ -27,9 +27,10 @@ func mfaOktaResource() *schema.Resource { ValidateFunc: validateNoTrailingSlash, }, "mount_accessor": { - Type: schema.TypeString, - Required: true, - Description: "The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.", + Type: schema.TypeString, + Required: true, + Description: "The mount to tie this method to for use in automatic mappings. " + + "The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.", }, "username_format": { Type: schema.TypeString, @@ -70,16 +71,16 @@ func mfaOktaPath(name string) string { func mfaOktaRequestData(d *schema.ResourceData) map[string]interface{} { data := map[string]interface{}{} - nonBooleanAPIFields := []string{ - "name", "mount_accessor", "username_format", - "org_name", "api_token", "base_url", + if v, ok := d.GetOkExists("primary_email"); ok { + data["primary_email"] = v.(bool) } - if v, ok := d.GetOkExists("primary_email"); ok { - data["primary_email"] = v.(string) + fields := []string{ + "name", "api_token", "mount_accessor", + "username_format", "org_name", "base_url", } - for _, k := range nonBooleanAPIFields { + for _, k := range fields { if v, ok := d.GetOk(k); ok { data[k] = v } diff --git a/vault/resource_mfa_okta_test.go b/vault/resource_mfa_okta_test.go index 8646ceac1..12b333a2a 100644 --- a/vault/resource_mfa_okta_test.go +++ b/vault/resource_mfa_okta_test.go @@ -11,7 +11,7 @@ import ( ) func TestMFAOktaBasic(t *testing.T) { - mfaOktaPath := acctest.RandomWithPrefix("mfa-okta") + path := acctest.RandomWithPrefix("mfa-okta") resourceName := "vault_mfa_okta.test" resource.Test(t, resource.TestCase{ @@ -19,20 +19,24 @@ func TestMFAOktaBasic(t *testing.T) { Providers: testProviders, Steps: []resource.TestStep{ { - Config: testMFAOktaConfig(mfaOktaPath), + Config: testMFAOktaConfig(path), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", mfaOktaPath), + resource.TestCheckResourceAttr(resourceName, "name", path), resource.TestCheckResourceAttr(resourceName, "username_format", "user@example.com"), resource.TestCheckResourceAttr(resourceName, "org_name", "hashicorp"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"api_token"}, + }, }, }) } func testMFAOktaConfig(path string) string { - userPassPath := acctest.RandomWithPrefix("userpass") - return fmt.Sprintf(` resource "vault_auth_backend" "userpass" { type = "userpass" @@ -46,5 +50,5 @@ resource "vault_mfa_okta" "test" { org_name = "hashicorp" api_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" } -`, userPassPath, path) +`, acctest.RandomWithPrefix("userpass"), path) } diff --git a/vault/resource_mfa_pingid_test.go b/vault/resource_mfa_pingid_test.go index c15a77ee5..8a82ce8ab 100644 --- a/vault/resource_mfa_pingid_test.go +++ b/vault/resource_mfa_pingid_test.go @@ -11,7 +11,7 @@ import ( ) func TestMFAPingIDBasic(t *testing.T) { - mfaPingIDPath := acctest.RandomWithPrefix("mfa-pingid") + path := acctest.RandomWithPrefix("mfa-pingid") // Base64 Encoded string taken from Vault repo example settingsFile := "I0F1dG8tR2VuZXJhdGVkIGZyb20gUGluZ09uZSwgZG93bmxvYWRlZCBieSBpZD1bU1NPXSBlbWFpbD1baGFtaWRAaGFzaGljb3JwLmNvbV0KI1dlZCBEZWMgMTUgMTM6MDg6NDQgTVNUIDIwMjEKdXNlX2Jhc2U2NF9rZXk9YlhrdGMyVmpjbVYwTFd0bGVRPT0KdXNlX3NpZ25hdHVyZT10cnVlCnRva2VuPWxvbC10b2tlbgppZHBfdXJsPWh0dHBzOi8vaWRweG55bDNtLnBpbmdpZGVudGl0eS5jb20vcGluZ2lkCm9yZ19hbGlhcz1sb2wtb3JnLWFsaWFzCmFkbWluX3VybD1odHRwczovL2lkcHhueWwzbS5waW5naWRlbnRpdHkuY29tL3BpbmdpZAphdXRoZW50aWNhdG9yX3VybD1odHRwczovL2F1dGhlbnRpY2F0b3IucGluZ29uZS5jb20vcGluZ2lkL3BwbQ==" resourceName := "vault_mfa_pingid.test" @@ -21,22 +21,26 @@ func TestMFAPingIDBasic(t *testing.T) { Providers: testProviders, Steps: []resource.TestStep{ { - Config: testMFAPingIDConfig(mfaPingIDPath, settingsFile), + Config: testMFAPingIDConfig(path, settingsFile), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", mfaPingIDPath), + resource.TestCheckResourceAttr(resourceName, "name", path), resource.TestCheckResourceAttr(resourceName, "username_format", "user@example.com"), resource.TestCheckResourceAttr(resourceName, "type", "pingid"), resource.TestCheckResourceAttr(resourceName, "use_signature", "true"), resource.TestCheckResourceAttr(resourceName, "namespace_id", ""), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"mount_accessor", "username_format", "settings_file_base64"}, + }, }, }) } func testMFAPingIDConfig(path, file string) string { - userPassPath := acctest.RandomWithPrefix("userpass") - return fmt.Sprintf(` resource "vault_auth_backend" "userpass" { type = "userpass" @@ -49,5 +53,5 @@ resource "vault_mfa_pingid" "test" { username_format = "user@example.com" settings_file_base64 = %q } -`, userPassPath, path, file) +`, acctest.RandomWithPrefix("userpass"), path, file) } diff --git a/vault/resource_mfa_totp_test.go b/vault/resource_mfa_totp_test.go index d3a591fbf..0c90ac97b 100644 --- a/vault/resource_mfa_totp_test.go +++ b/vault/resource_mfa_totp_test.go @@ -11,7 +11,7 @@ import ( ) func TestMFATOTPBasic(t *testing.T) { - mfaTOTPPath := acctest.RandomWithPrefix("mfa-totp") + path := acctest.RandomWithPrefix("mfa-totp") resourceName := "vault_mfa_totp.test" resource.Test(t, resource.TestCase{ @@ -19,15 +19,21 @@ func TestMFATOTPBasic(t *testing.T) { Providers: testProviders, Steps: []resource.TestStep{ { - Config: testMFATOTPConfig(mfaTOTPPath), + Config: testMFATOTPConfig(path), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", mfaTOTPPath), + resource.TestCheckResourceAttr(resourceName, "name", path), resource.TestCheckResourceAttr(resourceName, "issuer", "hashicorp"), resource.TestCheckResourceAttr(resourceName, "period", "60"), resource.TestCheckResourceAttr(resourceName, "algorithm", "SHA256"), resource.TestCheckResourceAttr(resourceName, "digits", "8"), + resource.TestCheckResourceAttr(resourceName, "key_size", "20"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -35,11 +41,12 @@ func TestMFATOTPBasic(t *testing.T) { func testMFATOTPConfig(path string) string { return fmt.Sprintf(` resource "vault_mfa_totp" "test" { - name = %q - issuer = "hashicorp" - period = 60 - algorithm = "SHA256" - digits = 8 + name = "%s" + issuer = "hashicorp" + period = 60 + algorithm = "SHA256" + digits = 8 + key_size = 20 } `, path) } From 2d8f1521bbd29e2191d86f4753290781c53c5764 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Fri, 1 Apr 2022 13:38:16 -0700 Subject: [PATCH 5/7] read fields not returned by Vault from TF config --- vault/resource_mfa_pingid.go | 13 ++++++++++++- vault/resource_mfa_pingid_test.go | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/vault/resource_mfa_pingid.go b/vault/resource_mfa_pingid.go index 8abeed8e5..3f1b2db03 100644 --- a/vault/resource_mfa_pingid.go +++ b/vault/resource_mfa_pingid.go @@ -135,11 +135,22 @@ func mfaPingIDRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error reading from Vault at %s, err=%w", path, err) } + if err := d.Set("mount_accessor", d.Get("mount_accessor")); err != nil { + return err + } + + if err := d.Set("username_format", d.Get("username_format")); err != nil { + return err + } + + if err := d.Set("settings_file_base64", d.Get("settings_file_base64")); err != nil { + return err + } + fields := []string{ "name", "idp_url", "admin_url", "authenticator_url", "org_alias", "type", "use_signature", "id", "namespace_id", - // "mount_accessor", "username_format", "settings_file_base64", } for _, k := range fields { diff --git a/vault/resource_mfa_pingid_test.go b/vault/resource_mfa_pingid_test.go index 8a82ce8ab..ac35073e9 100644 --- a/vault/resource_mfa_pingid_test.go +++ b/vault/resource_mfa_pingid_test.go @@ -28,6 +28,7 @@ func TestMFAPingIDBasic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "type", "pingid"), resource.TestCheckResourceAttr(resourceName, "use_signature", "true"), resource.TestCheckResourceAttr(resourceName, "namespace_id", ""), + resource.TestCheckResourceAttr(resourceName, "settings_file_base64", settingsFile), ), }, { From cf0760115ca852cb93f37151cadbf2f6a27c2643 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Fri, 1 Apr 2022 14:07:20 -0700 Subject: [PATCH 6/7] add docs for new MFA resources --- vault/resource_mfa_okta.go | 2 +- website/docs/r/mfa_okta.html.md | 71 +++++++++++++++++++++++++++ website/docs/r/mfa_pingid.html.md | 81 +++++++++++++++++++++++++++++++ website/docs/r/mfa_totp.html.md | 62 +++++++++++++++++++++++ website/vault.erb | 12 +++++ 5 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 website/docs/r/mfa_okta.html.md create mode 100644 website/docs/r/mfa_pingid.html.md create mode 100644 website/docs/r/mfa_totp.html.md diff --git a/vault/resource_mfa_okta.go b/vault/resource_mfa_okta.go index 65e56caca..7f28c343f 100644 --- a/vault/resource_mfa_okta.go +++ b/vault/resource_mfa_okta.go @@ -58,7 +58,7 @@ func mfaOktaResource() *schema.Resource { Type: schema.TypeBool, Optional: true, Computed: true, - Description: "If set, the username will only match the primary email for the account.", + Description: "If set to true, the username will only match the primary email for the account.", }, }, } diff --git a/website/docs/r/mfa_okta.html.md b/website/docs/r/mfa_okta.html.md new file mode 100644 index 000000000..125fd6625 --- /dev/null +++ b/website/docs/r/mfa_okta.html.md @@ -0,0 +1,71 @@ +--- +layout: "vault" +page_title: "Vault: vault_mfa_okta resource" +sidebar_current: "docs-vault-resource-mfa-okta" +description: |- + Managing the MFA Okta method configuration +--- + +# vault\_mfa\_okta + +Provides a resource to manage [Okta MFA](https://www.vaultproject.io/docs/enterprise/mfa/mfa-okta). + +**Note** this feature is available only with Vault Enterprise. + +## Example Usage + +```hcl +resource "vault_auth_backend" "userpass" { + type = "userpass" + path = "userpass" +} + +resource "vault_mfa_okta" "my_okta" { + name = "my_okta" + mount_accessor = vault_auth_backend.userpass.accessor + username_format = "user@example.com" + org_name = "hashicorp" + api_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +} +``` + +## Argument Reference + +The following arguments are supported: + +- `name` `(string: )` – Name of the MFA method. + +- `mount_accessor` `(string: )` - The mount to tie this method to for use in automatic mappings. + The mapping will use the Name field of Aliases associated with this mount as the username in the mapping. + +- `username_format` `(string)` - A format string for mapping Identity names to MFA method names. + Values to substitute should be placed in `{{}}`. For example, `"{{alias.name}}@example.com"`. + If blank, the Alias's Name field will be used as-is. Currently-supported mappings: + - alias.name: The name returned by the mount configured via the `mount_accessor` parameter + - entity.name: The name configured for the Entity + - alias.metadata.``: The value of the Alias's metadata parameter + - entity.metadata.``: The value of the Entity's metadata parameter + +- `org_name` `(string: )` - Name of the organization to be used in the Okta API. + +- `api_token` `(string: )` - Okta API key. + +- `base_url` `(string)` - If set, will be used as the base domain for API requests. Examples are `okta.com`, + `oktapreview.com`, and `okta-emea.com`. + +- `primary_email` `(string: )` - If set to true, the username will only match the + primary email for the account. + + +## Attributes Reference + +No additional attributes are exported by this resource. + +## Import + +Mounts can be imported using the `path`, e.g. + +``` +$ terraform import vault_mfa_okta.my_okta my_okta +``` + diff --git a/website/docs/r/mfa_pingid.html.md b/website/docs/r/mfa_pingid.html.md new file mode 100644 index 000000000..ad0aab66f --- /dev/null +++ b/website/docs/r/mfa_pingid.html.md @@ -0,0 +1,81 @@ +--- +layout: "vault" +page_title: "Vault: vault_mfa_pingid resource" +sidebar_current: "docs-vault-resource-mfa-pingid" +description: |- + Managing the MFA PingID method configuration +--- + +# vault\_mfa\_pingid + +Provides a resource to manage [PingID MFA](https://www.vaultproject.io/docs/enterprise/mfa/mfa-pingid). + +**Note** this feature is available only with Vault Enterprise. + +## Example Usage + +```hcl +variable "settings_file" {} + +resource "vault_auth_backend" "userpass" { + type = "userpass" + path = "userpass" +} + +resource "vault_mfa_pingid" "my_pingid" { + name = "my_pingid" + mount_accessor = vault_auth_backend.userpass.accessor + username_format = "user@example.com" + settings_file_base64 = var.settings_file +} +``` + +## Argument Reference + +The following arguments are supported: + +- `name` `(string: )` – Name of the MFA method. + +- `mount_accessor` `(string: )` - The mount to tie this method to for use in automatic mappings. + The mapping will use the Name field of Aliases associated with this mount as the username in the mapping. + +- `username_format` `(string)` - A format string for mapping Identity names to MFA method names. + Values to substitute should be placed in `{{}}`. For example, `"{{alias.name}}@example.com"`. + If blank, the Alias's Name field will be used as-is. Currently-supported mappings: + - alias.name: The name returned by the mount configured via the `mount_accessor` parameter + - entity.name: The name configured for the Entity + - alias.metadata.``: The value of the Alias's metadata parameter + - entity.metadata.``: The value of the Entity's metadata parameter + +- `settings_file_base64` `(string: )` - A base64-encoded third-party settings file retrieved + from PingID's configuration page. + +## Attributes Reference + +In addition to the above arguments, the following attributes are exported: + +- `idp_url` `(string)` – IDP URL computed by Vault + +- `admin_url` `(string)` – Admin URL computed by Vault + +- `authenticator_url` `(string)` – Authenticator URL computed by Vault + +- `org_alias` `(string)` – Org Alias computed by Vault + +- `id` `(string)` – ID computed by Vault + +- `namespace_id` `(string)` – Namespace ID computed by Vault + +- `type` `(string)` – Type of configuration computed by Vault + +- `use_signature` `(string)` – If set to true, enables use of PingID signature. Computed by Vault + + +## Import + +Mounts can be imported using the `path`, e.g. + +``` +$ terraform import vault_mfa_pingid.my_pingid my_pingid +``` + diff --git a/website/docs/r/mfa_totp.html.md b/website/docs/r/mfa_totp.html.md new file mode 100644 index 000000000..3376ea734 --- /dev/null +++ b/website/docs/r/mfa_totp.html.md @@ -0,0 +1,62 @@ +--- +layout: "vault" +page_title: "Vault: vault_mfa_totp resource" +sidebar_current: "docs-vault-resource-mfa-totp" +description: |- + Managing the MFA TOTP method configuration +--- + +# vault\_mfa\_totp + +Provides a resource to manage [TOTP MFA](https://www.vaultproject.io/docs/enterprise/mfa/mfa-totp). + +**Note** this feature is available only with Vault Enterprise. + +## Example Usage + +```hcl +resource "vault_mfa_totp" "my_totp" { + name = "my_totp" + issuer = "hashicorp" + period = 60 + algorithm = "SHA256" + digits = 8 + key_size = 20 +} +``` + +## Argument Reference + +The following arguments are supported: + +- `name` `(string: )` – Name of the MFA method. + +- `issuer` `(string: )` - The name of the key's issuing organization. + +- `period` `(int)` - The length of time used to generate a counter for the TOTP token calculation. + +- `key_size` `(int)` - Specifies the size in bytes of the generated key. + +- `qr_size` `(int)` - The pixel size of the generated square QR code. + +- `algorithm` `(string)` - Specifies the hashing algorithm used to generate the TOTP code. + Options include `SHA1`, `SHA256` and `SHA512` + +- `digits` `(int)` - The number of digits in the generated TOTP token. + This value can either be 6 or 8. + +- `skew` `(int)` - The number of delay periods that are allowed when validating a TOTP token. + This value can either be 0 or 1. + +## Attributes Reference + +No additional attributes are exported by this resource. + +## Import + +Mounts can be imported using the `path`, e.g. + +``` +$ terraform import vault_mfa_totp.my_totp my_totp +``` + diff --git a/website/vault.erb b/website/vault.erb index bb20a8579..226498a01 100644 --- a/website/vault.erb +++ b/website/vault.erb @@ -349,6 +349,18 @@ vault_mfa_duo + > + vault_mfa_okta + + + > + vault_mfa_totp + + + > + vault_mfa_pingid + + > vault_mount From d3274c05737ef26b37e7eb12fb097ac0e24d7f6e Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Mon, 4 Apr 2022 13:14:07 -0700 Subject: [PATCH 7/7] fix superfluous updates and add no-op update func --- vault/resource_mfa_okta.go | 30 ++++++++++++++++---- vault/resource_mfa_okta_test.go | 1 + vault/resource_mfa_pingid.go | 19 +++++++++---- vault/resource_mfa_pingid_test.go | 1 + vault/resource_mfa_totp.go | 41 +++++++++++++++++++-------- vault/resource_mfa_totp_test.go | 47 ++++++++++++++++++++++++++++--- 6 files changed, 112 insertions(+), 27 deletions(-) diff --git a/vault/resource_mfa_okta.go b/vault/resource_mfa_okta.go index 7f28c343f..c07c4eeeb 100644 --- a/vault/resource_mfa_okta.go +++ b/vault/resource_mfa_okta.go @@ -12,7 +12,7 @@ import ( func mfaOktaResource() *schema.Resource { return &schema.Resource{ Create: mfaOktaWrite, - Update: mfaOktaWrite, + Update: mfaOktaUpdate, Delete: mfaOktaDelete, Read: mfaOktaRead, Importer: &schema.ResourceImporter{ @@ -23,43 +23,56 @@ func mfaOktaResource() *schema.Resource { "name": { Type: schema.TypeString, Required: true, + ForceNew: true, Description: "Name of the MFA method.", ValidateFunc: validateNoTrailingSlash, }, "mount_accessor": { Type: schema.TypeString, Required: true, + ForceNew: true, Description: "The mount to tie this method to for use in automatic mappings. " + "The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.", }, "username_format": { Type: schema.TypeString, Optional: true, + ForceNew: true, Description: "A format string for mapping Identity names to MFA method names. Values to substitute should be placed in `{{}}`.", }, "org_name": { Type: schema.TypeString, Required: true, + ForceNew: true, Description: "Name of the organization to be used in the Okta API.", }, "api_token": { Type: schema.TypeString, Required: true, + ForceNew: true, Sensitive: true, Description: "Okta API key.", }, "base_url": { Type: schema.TypeString, Optional: true, - Computed: true, + Default: "okta.com", + ForceNew: true, Description: "If set, will be used as the base domain for API requests.", }, "primary_email": { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: false, + ForceNew: true, Description: "If set to true, the username will only match the primary email for the account.", }, + "id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: "ID computed by Vault.", + }, }, } } @@ -100,14 +113,14 @@ func mfaOktaWrite(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error writing to Vault at %s, err=%w", path, err) } - d.SetId(path) + d.SetId(name) return mfaOktaRead(d, meta) } func mfaOktaRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*api.Client) - path := d.Id() + path := mfaOktaPath(d.Id()) log.Printf("[DEBUG] Reading MFA Okta config %q", path) resp, err := client.Logical().Read(path) @@ -118,6 +131,7 @@ func mfaOktaRead(d *schema.ResourceData, meta interface{}) error { fields := []string{ "name", "mount_accessor", "username_format", "org_name", "base_url", "primary_email", + "id", } for _, k := range fields { @@ -129,9 +143,13 @@ func mfaOktaRead(d *schema.ResourceData, meta interface{}) error { return nil } +func mfaOktaUpdate(d *schema.ResourceData, meta interface{}) error { + return nil +} + func mfaOktaDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*api.Client) - path := d.Id() + path := mfaOktaPath(d.Id()) log.Printf("[DEBUG] Deleting mfaOkta %s from Vault", path) diff --git a/vault/resource_mfa_okta_test.go b/vault/resource_mfa_okta_test.go index 12b333a2a..e00f84e3c 100644 --- a/vault/resource_mfa_okta_test.go +++ b/vault/resource_mfa_okta_test.go @@ -24,6 +24,7 @@ func TestMFAOktaBasic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", path), resource.TestCheckResourceAttr(resourceName, "username_format", "user@example.com"), resource.TestCheckResourceAttr(resourceName, "org_name", "hashicorp"), + resource.TestCheckResourceAttrSet(resourceName, "id"), ), }, { diff --git a/vault/resource_mfa_pingid.go b/vault/resource_mfa_pingid.go index 3f1b2db03..cfb9e755a 100644 --- a/vault/resource_mfa_pingid.go +++ b/vault/resource_mfa_pingid.go @@ -12,7 +12,7 @@ import ( func mfaPingIDResource() *schema.Resource { return &schema.Resource{ Create: mfaPingIDWrite, - Update: mfaPingIDWrite, + Update: mfaPingIDUpdate, Delete: mfaPingIDDelete, Read: mfaPingIDRead, Importer: &schema.ResourceImporter{ @@ -24,22 +24,26 @@ func mfaPingIDResource() *schema.Resource { Type: schema.TypeString, Required: true, Description: "Name of the MFA method.", + ForceNew: true, ValidateFunc: validateNoTrailingSlash, }, "mount_accessor": { Type: schema.TypeString, Required: true, + ForceNew: true, Description: "The mount to tie this method to for use in automatic mappings. " + "The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.", }, "username_format": { Type: schema.TypeString, Optional: true, + ForceNew: true, Description: "A format string for mapping Identity names to MFA method names. Values to substitute should be placed in `{{}}`.", }, "settings_file_base64": { Type: schema.TypeString, Required: true, + ForceNew: true, Description: "A base64-encoded third-party settings file retrieved from PingID's configuration page.", }, "idp_url": { @@ -65,6 +69,7 @@ func mfaPingIDResource() *schema.Resource { "id": { Type: schema.TypeString, Computed: true, + Optional: true, Description: "ID computed by Vault.", }, "namespace_id": { @@ -93,8 +98,6 @@ func mfaPingIDPath(name string) string { func mfaPingIDRequestData(d *schema.ResourceData) map[string]interface{} { data := map[string]interface{}{} - // Read does not return any API Fields listed in docs - // TODO confirm expected behavior fields := []string{ "name", "mount_accessor", "settings_file_base64", "username_format", @@ -120,14 +123,14 @@ func mfaPingIDWrite(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error writing to Vault at %s, err=%w", path, err) } - d.SetId(path) + d.SetId(name) return mfaPingIDRead(d, meta) } func mfaPingIDRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*api.Client) - path := d.Id() + path := mfaPingIDPath(d.Id()) log.Printf("[DEBUG] Reading MFA PingID config %q", path) resp, err := client.Logical().Read(path) @@ -162,9 +165,13 @@ func mfaPingIDRead(d *schema.ResourceData, meta interface{}) error { return nil } +func mfaPingIDUpdate(d *schema.ResourceData, meta interface{}) error { + return nil +} + func mfaPingIDDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*api.Client) - path := d.Id() + path := mfaPingIDPath(d.Id()) log.Printf("[DEBUG] Deleting mfaPingID %s from Vault", path) diff --git a/vault/resource_mfa_pingid_test.go b/vault/resource_mfa_pingid_test.go index ac35073e9..0b329d7ec 100644 --- a/vault/resource_mfa_pingid_test.go +++ b/vault/resource_mfa_pingid_test.go @@ -29,6 +29,7 @@ func TestMFAPingIDBasic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "use_signature", "true"), resource.TestCheckResourceAttr(resourceName, "namespace_id", ""), resource.TestCheckResourceAttr(resourceName, "settings_file_base64", settingsFile), + resource.TestCheckResourceAttrSet(resourceName, "id"), ), }, { diff --git a/vault/resource_mfa_totp.go b/vault/resource_mfa_totp.go index ffb1f08c0..05ee5b0e2 100644 --- a/vault/resource_mfa_totp.go +++ b/vault/resource_mfa_totp.go @@ -12,7 +12,7 @@ import ( func mfaTOTPResource() *schema.Resource { return &schema.Resource{ Create: mfaTOTPWrite, - Update: mfaTOTPWrite, + Update: mfaTOTPUpdate, Delete: mfaTOTPDelete, Read: mfaTOTPRead, Importer: &schema.ResourceImporter{ @@ -23,53 +23,67 @@ func mfaTOTPResource() *schema.Resource { "name": { Type: schema.TypeString, Required: true, + ForceNew: true, Description: "Name of the MFA method.", ValidateFunc: validateNoTrailingSlash, }, "issuer": { Type: schema.TypeString, Required: true, + ForceNew: true, Description: "The name of the key's issuing organization.", }, "period": { Type: schema.TypeInt, Optional: true, - Computed: true, + Default: 30, + ForceNew: true, Description: "The length of time used to generate a counter for the TOTP token calculation.", }, "key_size": { Type: schema.TypeInt, Optional: true, - Computed: true, + Default: 20, + ForceNew: true, Description: "Specifies the size in bytes of the generated key.", }, "qr_size": { Type: schema.TypeInt, Optional: true, - Computed: true, + Default: 200, + ForceNew: true, Description: "The pixel size of the generated square QR code.", }, "algorithm": { Type: schema.TypeString, Optional: true, - Computed: true, + Default: "SHA1", + ForceNew: true, Description: "Specifies the hashing algorithm used to generate the TOTP code. " + "Options include 'SHA1', 'SHA256' and 'SHA512'.", }, "digits": { Type: schema.TypeInt, Optional: true, - Computed: true, + Default: 6, + ForceNew: true, Description: "The number of digits in the generated TOTP token. " + "This value can either be 6 or 8.", }, "skew": { Type: schema.TypeInt, Optional: true, - Computed: true, + Default: 1, + ForceNew: true, Description: "The number of delay periods that are allowed when validating a TOTP token. " + "This value can either be 0 or 1.", }, + "id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: "ID computed by Vault.", + }, }, } } @@ -107,14 +121,15 @@ func mfaTOTPWrite(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error writing to Vault at %s, err=%w", path, err) } - d.SetId(path) + d.SetId(name) return mfaTOTPRead(d, meta) } func mfaTOTPRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*api.Client) - path := d.Id() + name := d.Id() + path := mfaTOTPPath(name) log.Printf("[DEBUG] Reading MFA TOTP config %q", path) resp, err := client.Logical().Read(path) @@ -125,7 +140,7 @@ func mfaTOTPRead(d *schema.ResourceData, meta interface{}) error { fields := []string{ "name", "issuer", "period", "key_size", "qr_size", "algorithm", - "digits", "skew", + "digits", "skew", "id", } for _, k := range fields { @@ -137,9 +152,13 @@ func mfaTOTPRead(d *schema.ResourceData, meta interface{}) error { return nil } +func mfaTOTPUpdate(d *schema.ResourceData, meta interface{}) error { + return nil +} + func mfaTOTPDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*api.Client) - path := d.Id() + path := mfaTOTPPath(d.Id()) log.Printf("[DEBUG] Deleting mfaTOTP %s from Vault", path) diff --git a/vault/resource_mfa_totp_test.go b/vault/resource_mfa_totp_test.go index 0c90ac97b..73538d34f 100644 --- a/vault/resource_mfa_totp_test.go +++ b/vault/resource_mfa_totp_test.go @@ -6,6 +6,8 @@ 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" + "github.com/hashicorp/vault/api" "github.com/hashicorp/terraform-provider-vault/testutil" ) @@ -14,12 +16,13 @@ func TestMFATOTPBasic(t *testing.T) { path := acctest.RandomWithPrefix("mfa-totp") resourceName := "vault_mfa_totp.test" + var id string resource.Test(t, resource.TestCase{ PreCheck: func() { testutil.TestEntPreCheck(t) }, Providers: testProviders, Steps: []resource.TestStep{ { - Config: testMFATOTPConfig(path), + Config: testMFATOTPConfig(path, 20), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", path), resource.TestCheckResourceAttr(resourceName, "issuer", "hashicorp"), @@ -27,6 +30,32 @@ func TestMFATOTPBasic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "algorithm", "SHA256"), resource.TestCheckResourceAttr(resourceName, "digits", "8"), resource.TestCheckResourceAttr(resourceName, "key_size", "20"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + ), + }, + { + PreConfig: func() { + client := testProvider.Meta().(*api.Client) + resp, err := client.Logical().Read(mfaTOTPPath(path)) + if err != nil { + t.Fatal(err) + } + + id = resp.Data["id"].(string) + if id == "" { + t.Fatal("expected ID to be set; got empty") + } + }, + Config: testMFATOTPConfig(path, 30), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + testCheckNotResourceAttr(resourceName, "id", id), + resource.TestCheckResourceAttr(resourceName, "name", path), + resource.TestCheckResourceAttr(resourceName, "issuer", "hashicorp"), + resource.TestCheckResourceAttr(resourceName, "period", "60"), + resource.TestCheckResourceAttr(resourceName, "algorithm", "SHA256"), + resource.TestCheckResourceAttr(resourceName, "digits", "8"), + resource.TestCheckResourceAttr(resourceName, "key_size", "30"), ), }, { @@ -38,7 +67,17 @@ func TestMFATOTPBasic(t *testing.T) { }) } -func testMFATOTPConfig(path string) string { +func testCheckNotResourceAttr(name, key, value string) resource.TestCheckFunc { + return func(state *terraform.State) error { + if err := resource.TestCheckResourceAttr(name, key, value); err == nil { + return fmt.Errorf("expected value %s to change for key %s.%s", value, name, key) + } + + return nil + } +} + +func testMFATOTPConfig(path string, keySize int) string { return fmt.Sprintf(` resource "vault_mfa_totp" "test" { name = "%s" @@ -46,7 +85,7 @@ resource "vault_mfa_totp" "test" { period = 60 algorithm = "SHA256" digits = 8 - key_size = 20 + key_size = %d } -`, path) +`, path, keySize) }