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

resource/aws_iam_virtual_mfa_device: Correctly sweep associated devices and add enable_date and user_name attributes #32462

Merged
merged 6 commits into from
Jul 12, 2023
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
3 changes: 3 additions & 0 deletions .changelog/32462.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_iam_virtual_mfa_device: Add `enable_date` and `user_name` attributes
```
18 changes: 14 additions & 4 deletions internal/service/iam/sweep.go
Original file line number Diff line number Diff line change
Expand Up @@ -918,8 +918,10 @@ func sweepVirtualMFADevice(region string) error {
}
conn := client.IAMConn(ctx)
var sweeperErrs *multierror.Error
input := &iam.ListVirtualMFADevicesInput{}

accessDenied := regexp.MustCompile(`AccessDenied: .+ with an explicit deny`)

input := &iam.ListVirtualMFADevicesInput{}
err = conn.ListVirtualMFADevicesPagesWithContext(ctx, input, func(page *iam.ListVirtualMFADevicesOutput, lastPage bool) bool {
if len(page.VirtualMFADevices) == 0 {
log.Printf("[INFO] No IAM Virtual MFA Devices to sweep")
Expand All @@ -936,11 +938,19 @@ func sweepVirtualMFADevice(region string) error {
r := ResourceVirtualMFADevice()
d := r.Data(nil)
d.SetId(serialNum)

if err := sdk.ReadResource(ctx, r, d, client); err != nil {
sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("reading IAM Virtual MFA Device (%s): %w", serialNum, err))
continue
}

err := sdk.DeleteResource(ctx, r, d, client)
if err != nil {
sweeperErr := fmt.Errorf("error deleting IAM Virtual MFA Device (%s): %w", device, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
if accessDenied.MatchString(err.Error()) {
log.Printf("[DEBUG] Skipping IAM Virtual MFA Device (%s): %s", serialNum, err)
continue
}
sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("deleting IAM Virtual MFA Device (%s): %w", serialNum, err))
continue
}
}
Expand Down
56 changes: 56 additions & 0 deletions internal/service/iam/virtual_mfa_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ package iam

import (
"context"
"fmt"
"log"
"regexp"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand Down Expand Up @@ -46,6 +49,10 @@ func ResourceVirtualMFADevice() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"enable_date": {
Type: schema.TypeString,
Computed: true,
},
"path": {
Type: schema.TypeString,
Optional: true,
Expand All @@ -59,6 +66,10 @@ func ResourceVirtualMFADevice() *schema.Resource {
},
names.AttrTags: tftags.TagsSchema(),
names.AttrTagsAll: tftags.TagsSchemaComputed(),
"user_name": {
Type: schema.TypeString,
Computed: true,
},
"virtual_mfa_device_name": {
Type: schema.TypeString,
Required: true,
Expand Down Expand Up @@ -101,6 +112,7 @@ func resourceVirtualMFADeviceCreate(ctx context.Context, d *schema.ResourceData,
vMFA := output.VirtualMFADevice
d.SetId(aws.StringValue(vMFA.SerialNumber))

// Base32StringSeed and QRCodePNG must be read here, because they are not available via ListVirtualMFADevices
d.Set("base_32_string_seed", string(vMFA.Base32StringSeed))
d.Set("qr_code_png", string(vMFA.QRCodePNG))

Expand Down Expand Up @@ -139,6 +151,22 @@ func resourceVirtualMFADeviceRead(ctx context.Context, d *schema.ResourceData, m

d.Set("arn", vMFA.SerialNumber)

path, name, err := parseVirtualMFADeviceARN(aws.StringValue(vMFA.SerialNumber))
if err != nil {
return sdkdiag.AppendErrorf(diags, "reading IAM Virtual MFA Device (%s): %s", d.Id(), err)
}

d.Set("path", path)
d.Set("virtual_mfa_device_name", name)

if v := vMFA.EnableDate; v != nil {
d.Set("enable_date", aws.TimeValue(v).Format(time.RFC3339))
}

if u := vMFA.User; u != nil {
d.Set("user_name", u.UserName)
}

// The call above returns empty tags.
output, err := conn.ListMFADeviceTagsWithContext(ctx, &iam.ListMFADeviceTagsInput{
SerialNumber: aws.String(d.Id()),
Expand Down Expand Up @@ -177,6 +205,19 @@ func resourceVirtualMFADeviceDelete(ctx context.Context, d *schema.ResourceData,
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).IAMConn(ctx)

if v := d.Get("user_name"); v != "" {
_, err := conn.DeactivateMFADeviceWithContext(ctx, &iam.DeactivateMFADeviceInput{
UserName: aws.String(v.(string)),
SerialNumber: aws.String(d.Id()),
})
if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
return diags
}
if err != nil {
return sdkdiag.AppendErrorf(diags, "deactivating IAM Virtual MFA Device (%s): %s", d.Id(), err)
}
}

log.Printf("[INFO] Deleting IAM Virtual MFA Device: %s", d.Id())
_, err := conn.DeleteVirtualMFADeviceWithContext(ctx, &iam.DeleteVirtualMFADeviceInput{
SerialNumber: aws.String(d.Id()),
Expand Down Expand Up @@ -222,3 +263,18 @@ func FindVirtualMFADeviceBySerialNumber(ctx context.Context, conn *iam.IAM, seri

return output, nil
}

func parseVirtualMFADeviceARN(s string) (path, name string, err error) {
arn, err := arn.Parse(s)
if err != nil {
return "", "", err
}

re := regexp.MustCompile(`^mfa(/|/[\x{0021}-\x{007E}]+/)([-A-Za-z0-9_+=,.@]+)$`)
matches := re.FindStringSubmatch(arn.Resource)
if len(matches) != 3 {
return "", "", fmt.Errorf("IAM Virtual MFA Device ARN: invalid resource section (%s)", arn.Resource)
}

return matches[1], matches[2], nil
}
82 changes: 68 additions & 14 deletions internal/service/iam/virtual_mfa_device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,60 @@ func TestAccIAMVirtualMFADevice_basic(t *testing.T) {
Steps: []resource.TestStep{
{
Config: testAccVirtualMFADeviceConfig_basic(rName),
Check: resource.ComposeTestCheckFunc(
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
acctest.CheckResourceAttrGlobalARN(resourceName, "arn", "iam", fmt.Sprintf("mfa/%s", rName)),
resource.TestCheckResourceAttrSet(resourceName, "base_32_string_seed"),
resource.TestCheckNoResourceAttr(resourceName, "enable_date"),
resource.TestCheckResourceAttr(resourceName, "path", "/"),
resource.TestCheckResourceAttrSet(resourceName, "qr_code_png"),
resource.TestCheckNoResourceAttr(resourceName, "user_name"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"path", "virtual_mfa_device_name", "base_32_string_seed", "qr_code_png"},
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"base_32_string_seed",
"qr_code_png",
},
},
},
})
}

func TestAccIAMVirtualMFADevice_path(t *testing.T) {
ctx := acctest.Context(t)
var conf iam.VirtualMFADevice
resourceName := "aws_iam_virtual_mfa_device.test"

path := "/path/"

rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, iam.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckVirtualMFADeviceDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccVirtualMFADeviceConfig_path(rName, path),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
acctest.CheckResourceAttrGlobalARN(resourceName, "arn", "iam", fmt.Sprintf("mfa%s%s", path, rName)),
resource.TestCheckResourceAttr(resourceName, "path", path),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"base_32_string_seed",
"qr_code_png",
},
},
},
})
Expand All @@ -66,21 +108,24 @@ func TestAccIAMVirtualMFADevice_tags(t *testing.T) {
Steps: []resource.TestStep{
{
Config: testAccVirtualMFADeviceConfig_tags1(rName, "key1", "value1"),
Check: resource.ComposeTestCheckFunc(
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "tags.%", "1"),
resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"path", "virtual_mfa_device_name", "base_32_string_seed", "qr_code_png"},
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"base_32_string_seed",
"qr_code_png",
},
},
{
Config: testAccVirtualMFADeviceConfig_tags2(rName, "key1", "value1updated", "key2", "value2"),
Check: resource.ComposeTestCheckFunc(
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "tags.%", "2"),
resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"),
Expand All @@ -89,7 +134,7 @@ func TestAccIAMVirtualMFADevice_tags(t *testing.T) {
},
{
Config: testAccVirtualMFADeviceConfig_tags1(rName, "key2", "value2"),
Check: resource.ComposeTestCheckFunc(
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "tags.%", "1"),
resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"),
Expand All @@ -114,10 +159,9 @@ func TestAccIAMVirtualMFADevice_disappears(t *testing.T) {
Steps: []resource.TestStep{
{
Config: testAccVirtualMFADeviceConfig_basic(rName),
Check: resource.ComposeTestCheckFunc(
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
acctest.CheckResourceDisappears(ctx, acctest.Provider, tfiam.ResourceVirtualMFADevice(), resourceName),
acctest.CheckResourceDisappears(ctx, acctest.Provider, tfiam.ResourceVirtualMFADevice(), resourceName),
),
ExpectNonEmptyPlan: true,
},
Expand Down Expand Up @@ -182,6 +226,16 @@ resource "aws_iam_virtual_mfa_device" "test" {
`, rName)
}

func testAccVirtualMFADeviceConfig_path(rName, path string) string {
return fmt.Sprintf(`
resource "aws_iam_virtual_mfa_device" "test" {
virtual_mfa_device_name = %[1]q

path = %[2]q
}
`, rName, path)
}

func testAccVirtualMFADeviceConfig_tags1(rName, tagKey1, tagValue1 string) string {
return fmt.Sprintf(`
resource "aws_iam_virtual_mfa_device" "test" {
Expand Down
2 changes: 1 addition & 1 deletion internal/service/opsworks/sweep.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ func newUserProfileSweeper(resource *schema.Resource, d *schema.ResourceData, cl

func (ups userProfileSweeper) Delete(ctx context.Context, timeout time.Duration, optFns ...tfresource.OptionsFunc) error {
err := ups.sweepable.Delete(ctx, timeout, optFns...)
if strings.Contains(err.Error(), "Cannot delete self") {
if err != nil && strings.Contains(err.Error(), "Cannot delete self") {
log.Printf("[WARN] Skipping OpsWorks User Profile (%s): %s", ups.d.Id(), err)
return nil
}
Expand Down
8 changes: 7 additions & 1 deletion website/docs/r/iam_virtual_mfa_device.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Provides an IAM Virtual MFA Device.
~> **Note:** All attributes will be stored in the raw state as plain-text.
[Read more about sensitive data in state](https://www.terraform.io/docs/state/sensitive-data.html).

~> **Note:** A virtual MFA device cannot be directly associated with an IAM User from Terraform.
To associate the virtual MFA device with a user and enable it, use the code returned in either `base_32_string_seed` or `qr_code_png` to generate TOTP authentication codes.
The authentication codes can then be used with the AWS CLI command [`aws iam enable-mfa-device`](https://docs.aws.amazon.com/cli/latest/reference/iam/enable-mfa-device.html) or the AWS API call [`EnableMFADevice`](https://docs.aws.amazon.com/IAM/latest/APIReference/API_EnableMFADevice.html).

## Example Usage

**Using certs on file:**
Expand All @@ -37,8 +41,10 @@ In addition to all arguments above, the following attributes are exported:

* `arn` - The Amazon Resource Name (ARN) specifying the virtual mfa device.
* `base_32_string_seed` - The base32 seed defined as specified in [RFC3548](https://tools.ietf.org/html/rfc3548.txt). The `base_32_string_seed` is base64-encoded.
* `qr_code_png` - A QR code PNG image that encodes `otpauth://totp/$virtualMFADeviceName@$AccountName?secret=$Base32String` where `$virtualMFADeviceName` is one of the create call arguments. AccountName is the user name if set (otherwise, the account ID otherwise), and Base32String is the seed in base32 format.
* `enable_date` - The date and time when the virtual MFA device was enabled.
* `qr_code_png` - A QR code PNG image that encodes `otpauth://totp/$virtualMFADeviceName@$AccountName?secret=$Base32String` where `$virtualMFADeviceName` is one of the create call arguments. AccountName is the user name if set (otherwise, the account ID), and Base32String is the seed in base32 format.
* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block).
* `user_name` - The associated IAM User name if the virtual MFA device is enabled.

## Import

Expand Down