Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Okta, TOTP and PingID MFA methods #1395

Merged
merged 7 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions vault/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}"},
Expand Down
162 changes: 162 additions & 0 deletions vault/resource_mfa_okta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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: mfaOktaUpdate,
Delete: mfaOktaDelete,
Read: mfaOktaRead,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"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,
Default: "okta.com",
ForceNew: true,
Description: "If set, will be used as the base domain for API requests.",
benashz marked this conversation as resolved.
Show resolved Hide resolved
},
"primary_email": {
Type: schema.TypeBool,
Optional: 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.",
},
},
}
}

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{}{}

if v, ok := d.GetOkExists("primary_email"); ok {
data["primary_email"] = v.(bool)
}

fields := []string{
"name", "api_token", "mount_accessor",
"username_format", "org_name", "base_url",
}

for _, k := range fields {
if v, ok := d.GetOk(k); ok {
benashz marked this conversation as resolved.
Show resolved Hide resolved
data[k] = v
}
}

return data
}

func mfaOktaWrite(d *schema.ResourceData, meta interface{}) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are using this function for both create and update, would it make sense to give it a more explicit name e.g. mfaOktaCreateOrUpdate()

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(name)

return mfaOktaRead(d, meta)
}

func mfaOktaRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*api.Client)
path := mfaOktaPath(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",
"id",
}

for _, k := range fields {
if err := d.Set(k, resp.Data[k]); err != nil {
return err
}
}

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 := mfaOktaPath(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
}
55 changes: 55 additions & 0 deletions vault/resource_mfa_okta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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) {
path := acctest.RandomWithPrefix("mfa-okta")
resourceName := "vault_mfa_okta.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testutil.TestEntPreCheck(t) },
Providers: testProviders,
Steps: []resource.TestStep{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should add an import step.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added import step in 5d506d7

{
Config: testMFAOktaConfig(path),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", path),
resource.TestCheckResourceAttr(resourceName, "username_format", "user@example.com"),
resource.TestCheckResourceAttr(resourceName, "org_name", "hashicorp"),
resource.TestCheckResourceAttrSet(resourceName, "id"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"api_token"},
},
},
})
}

func testMFAOktaConfig(path string) string {
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"
}
`, acctest.RandomWithPrefix("userpass"), path)
}
Loading