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

Adds MFA Duo configuration #443

Merged
merged 1 commit into from
Aug 2, 2019
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
5 changes: 5 additions & 0 deletions vault/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,11 @@ var (
PathInventory: []string{"/sys/policies/rgp/{name}"},
EnterpriseOnly: true,
},
"vault_mfa_duo": {
Resource: mfaDuoResource(),
PathInventory: []string{"/sys/mfa/method/duo/{name}"},
EnterpriseOnly: true,
},
"vault_mount": {
Resource: mountResource(),
PathInventory: []string{"/sys/mounts/{path}"},
Expand Down
160 changes: 160 additions & 0 deletions vault/resource_mfa_duo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package vault

import (
"fmt"
"log"
"strings"

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/vault/api"
)

func mfaDuoResource() *schema.Resource {
return &schema.Resource{
Create: mfaDuoWrite,
Update: mfaDuoWrite,
Delete: mfaDuoDelete,
Read: mfaDuoRead,
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": {
petems marked this conversation as resolved.
Show resolved Hide resolved
Type: schema.TypeString,
Optional: true,
Description: "A format string for mapping Identity names to MFA method names. Values to substitute should be placed in `{{}}`.",
},
"secret_key": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
Description: "Secret key for Duo.",
},
"integration_key": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
Description: "Integration key for Duo.",
petems marked this conversation as resolved.
Show resolved Hide resolved
},
"api_hostname": {
Type: schema.TypeString,
Required: true,
Description: "API hostname for Duo.",
},
"push_info": {
Type: schema.TypeString,
Optional: true,
Description: "Push information for Duo.",
},
},
}
}

func mfaDuoWrite(d *schema.ResourceData, meta interface{}) error {
client := meta.(*api.Client)

name := d.Get("name").(string)

data := map[string]interface{}{}
mfaDuoUpdateFields(d, data)

log.Printf("[DEBUG] Writing role %q to MFA Duo auth backend", name)
d.SetId(name)

log.Printf("[DEBUG] Creating mfaDuo %s in Vault", name)
_, err := client.Logical().Write(mfaDuoPath(name), data)

if err != nil {
return fmt.Errorf("error writing to Vault: %s", err)
}

return mfaDuoRead(d, meta)
}

func mfaDuoDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*api.Client)

name := d.Get("name").(string)

log.Printf("[DEBUG] Deleting mfaDuo %s from Vault", mfaDuoPath(name))

_, err := client.Logical().Delete(mfaDuoPath(name))

if err != nil {
return fmt.Errorf("error deleting from Vault: %s", err)
}

return nil
}

func mfaDuoRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*api.Client)

name := d.Get("name").(string)

resp, err := client.Logical().Read(mfaDuoPath(name))

if err != nil {
return fmt.Errorf("error reading from Vault: %s", err)
}

log.Printf("[DEBUG] Read MFA Duo config %q", mfaDuoPath(name))

d.Set("mount_accessor", resp.Data["mount_accessor"])
d.Set("username_format", resp.Data["username_format"])
d.Set("api_hostname", resp.Data["api_hostname"])

// When you push the data up, it's push_info
// when vault responds, it's pushinfo :(
d.Set("push_info", resp.Data["pushinfo"])

// secret_key and integration_key, can't read out from the api
// So... if it drifts, it drift.

d.SetId(name)

return nil
}

func mfaDuoUpdateFields(d *schema.ResourceData, data map[string]interface{}) {
if v, ok := d.GetOk("mount_accessor"); ok {
data["mount_accessor"] = v.(string)
}

if v, ok := d.GetOk("username_format"); ok {
data["username_format"] = v.(string)
}

if v, ok := d.GetOk("secret_key"); ok {
data["secret_key"] = v.(string)
}

if v, ok := d.GetOk("integration_key"); ok {
data["integration_key"] = v.(string)
}

if v, ok := d.GetOk("api_hostname"); ok {
data["api_hostname"] = v.(string)
}

if v, ok := d.GetOk("push_info"); ok {
data["push_info"] = v.(string)
}

}

func mfaDuoPath(name string) string {
return "sys/mfa/method/duo/" + strings.Trim(name, "/") + "/"
}
62 changes: 62 additions & 0 deletions vault/resource_mfa_duo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package vault

import (
"fmt"
"testing"

"os"

"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)

func TestMFADuoBasic(t *testing.T) {

isEnterprise := os.Getenv("TF_ACC_ENTERPRISE")
if isEnterprise == "" {
t.Skip("TF_ACC_ENTERPRISE is not set, test is applicable only for Enterprise version of Vault")
}

mfaDuoPath := acctest.RandomWithPrefix("mfa-duo")

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testProviders,
Steps: []resource.TestStep{
{
Config: testMFADuoConfig(mfaDuoPath),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("vault_mfa_duo.test", "name", mfaDuoPath),
resource.TestCheckResourceAttr("vault_mfa_duo.test", "secret_key", "8C7THtrIigh2rPZQMbguugt8IUftWhMRCOBzbuyz"),
resource.TestCheckResourceAttr("vault_mfa_duo.test", "integration_key", "BIACEUEAXI20BNWTEYXT"),
resource.TestCheckResourceAttr("vault_mfa_duo.test", "api_hostname", "api-2b5c39f5.duosecurity.com"),
resource.TestCheckResourceAttr("vault_mfa_duo.test", "username_format", "user@example.com"),
resource.TestCheckResourceAttr("vault_mfa_duo.test", "push_info", "from=loginortal&domain=example.com"),
),
},
},
})
}

func testMFADuoConfig(path string) string {

userPassPath := acctest.RandomWithPrefix("userpass")

return fmt.Sprintf(`
resource "vault_auth_backend" "userpass" {
type = "userpass"
path = %q
}

resource "vault_mfa_duo" "test" {
name = %q
mount_accessor = "${vault_auth_backend.userpass.accessor}"
secret_key = "8C7THtrIigh2rPZQMbguugt8IUftWhMRCOBzbuyz"
integration_key = "BIACEUEAXI20BNWTEYXT"
petems marked this conversation as resolved.
Show resolved Hide resolved
api_hostname = "api-2b5c39f5.duosecurity.com"
username_format = "user@example.com"
push_info = "from=loginortal&domain=example.com"
}
`, userPassPath, path)

}
61 changes: 61 additions & 0 deletions website/docs/r/mfa_duo.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
layout: "vault"
page_title: "Vault: vault_mfa_duo resource"
sidebar_current: "docs-vault-resource-mfa-duo"
description: |-
Managing the MFA Duo method configuration
---

# vault\_mfa-duo

Provides a resource to manage [Duo MFA](https://www.vaultproject.io/docs/enterprise/mfa/mfa-duo.html).

**Note** this feature is available only with Vault Enterprise.

## Example Usage

```hcl
resource "vault_auth_backend" "userpass" {
type = "userpass"
path = "userpass"
}

resource "vault_mfa_duo" "my_duo" {
name = "my_duo"
mount_accessor = "${vault_auth_backend.userpass.accessor}"
secret_key = "8C7THtrIigh2rPZQMbguugt8IUftWhMRCOBzbuyz"
integration_key = "BIACEUEAXI20BNWTEYXT"
api_hostname = "api-2b5c39f5.duosecurity.com"
}
```

## Argument Reference

The following arguments are supported:

- `name` `(string: <required>)` – Name of the MFA method.

- `mount_accessor` `(string: <required>)` - 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.`<key>`: The value of the Alias's metadata parameter
- entity.metadata.`<key>`: The value of the Entity's metadata parameter

- `secret_key` `(string: <required>)` - Secret key for Duo.

- `integration_key` `(string: <required>)` - Integration key for Duo.

- `api_hostname` `(string: <required>)` - API hostname for Duo.

- `push_info` `(string)` - Push information for Duo.

## Import

Mounts can be imported using the `path`, e.g.

```
$ terraform import vault_mfa_duo.my_duo my_duo
```

4 changes: 4 additions & 0 deletions website/vault.erb
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@
<a href="/docs/providers/vault/r/ldap_auth_backend_group.html">vault_ldap_auth_backend_group</a>
</li>

<li<%= sidebar_current("docs-vault-resource-mfa-duo") %>>
<a href="/docs/providers/vault/r/mfa_duo.html">vault_mfa_duo</a>
</li>

<li<%= sidebar_current("docs-vault-resource-mount") %>>
<a href="/docs/providers/vault/r/mount.html">vault_mount</a>
</li>
Expand Down