Skip to content

Commit 5227f6b

Browse files
authored
Merge pull request #32462 from hashicorp/b-sweep-mfa-device
resource/aws_iam_virtual_mfa_device: Correctly sweep associated devices and add `enable_date` and `user_name` attributes
2 parents 4e662e1 + 9e74fb7 commit 5227f6b

File tree

5 files changed

+148
-19
lines changed

5 files changed

+148
-19
lines changed

.changelog/32462.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/aws_iam_virtual_mfa_device: Add `enable_date` and `user_name` attributes
3+
```

internal/service/iam/sweep.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -918,8 +918,10 @@ func sweepVirtualMFADevice(region string) error {
918918
}
919919
conn := client.IAMConn(ctx)
920920
var sweeperErrs *multierror.Error
921-
input := &iam.ListVirtualMFADevicesInput{}
922921

922+
accessDenied := regexp.MustCompile(`AccessDenied: .+ with an explicit deny`)
923+
924+
input := &iam.ListVirtualMFADevicesInput{}
923925
err = conn.ListVirtualMFADevicesPagesWithContext(ctx, input, func(page *iam.ListVirtualMFADevicesOutput, lastPage bool) bool {
924926
if len(page.VirtualMFADevices) == 0 {
925927
log.Printf("[INFO] No IAM Virtual MFA Devices to sweep")
@@ -936,11 +938,19 @@ func sweepVirtualMFADevice(region string) error {
936938
r := ResourceVirtualMFADevice()
937939
d := r.Data(nil)
938940
d.SetId(serialNum)
941+
942+
if err := sdk.ReadResource(ctx, r, d, client); err != nil {
943+
sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("reading IAM Virtual MFA Device (%s): %w", serialNum, err))
944+
continue
945+
}
946+
939947
err := sdk.DeleteResource(ctx, r, d, client)
940948
if err != nil {
941-
sweeperErr := fmt.Errorf("error deleting IAM Virtual MFA Device (%s): %w", device, err)
942-
log.Printf("[ERROR] %s", sweeperErr)
943-
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
949+
if accessDenied.MatchString(err.Error()) {
950+
log.Printf("[DEBUG] Skipping IAM Virtual MFA Device (%s): %s", serialNum, err)
951+
continue
952+
}
953+
sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("deleting IAM Virtual MFA Device (%s): %w", serialNum, err))
944954
continue
945955
}
946956
}

internal/service/iam/virtual_mfa_device.go

+56
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ package iam
55

66
import (
77
"context"
8+
"fmt"
89
"log"
910
"regexp"
11+
"time"
1012

1113
"github.com/aws/aws-sdk-go/aws"
14+
"github.com/aws/aws-sdk-go/aws/arn"
1215
"github.com/aws/aws-sdk-go/service/iam"
1316
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
1417
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -46,6 +49,10 @@ func ResourceVirtualMFADevice() *schema.Resource {
4649
Type: schema.TypeString,
4750
Computed: true,
4851
},
52+
"enable_date": {
53+
Type: schema.TypeString,
54+
Computed: true,
55+
},
4956
"path": {
5057
Type: schema.TypeString,
5158
Optional: true,
@@ -59,6 +66,10 @@ func ResourceVirtualMFADevice() *schema.Resource {
5966
},
6067
names.AttrTags: tftags.TagsSchema(),
6168
names.AttrTagsAll: tftags.TagsSchemaComputed(),
69+
"user_name": {
70+
Type: schema.TypeString,
71+
Computed: true,
72+
},
6273
"virtual_mfa_device_name": {
6374
Type: schema.TypeString,
6475
Required: true,
@@ -101,6 +112,7 @@ func resourceVirtualMFADeviceCreate(ctx context.Context, d *schema.ResourceData,
101112
vMFA := output.VirtualMFADevice
102113
d.SetId(aws.StringValue(vMFA.SerialNumber))
103114

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

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

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

154+
path, name, err := parseVirtualMFADeviceARN(aws.StringValue(vMFA.SerialNumber))
155+
if err != nil {
156+
return sdkdiag.AppendErrorf(diags, "reading IAM Virtual MFA Device (%s): %s", d.Id(), err)
157+
}
158+
159+
d.Set("path", path)
160+
d.Set("virtual_mfa_device_name", name)
161+
162+
if v := vMFA.EnableDate; v != nil {
163+
d.Set("enable_date", aws.TimeValue(v).Format(time.RFC3339))
164+
}
165+
166+
if u := vMFA.User; u != nil {
167+
d.Set("user_name", u.UserName)
168+
}
169+
142170
// The call above returns empty tags.
143171
output, err := conn.ListMFADeviceTagsWithContext(ctx, &iam.ListMFADeviceTagsInput{
144172
SerialNumber: aws.String(d.Id()),
@@ -177,6 +205,19 @@ func resourceVirtualMFADeviceDelete(ctx context.Context, d *schema.ResourceData,
177205
var diags diag.Diagnostics
178206
conn := meta.(*conns.AWSClient).IAMConn(ctx)
179207

208+
if v := d.Get("user_name"); v != "" {
209+
_, err := conn.DeactivateMFADeviceWithContext(ctx, &iam.DeactivateMFADeviceInput{
210+
UserName: aws.String(v.(string)),
211+
SerialNumber: aws.String(d.Id()),
212+
})
213+
if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
214+
return diags
215+
}
216+
if err != nil {
217+
return sdkdiag.AppendErrorf(diags, "deactivating IAM Virtual MFA Device (%s): %s", d.Id(), err)
218+
}
219+
}
220+
180221
log.Printf("[INFO] Deleting IAM Virtual MFA Device: %s", d.Id())
181222
_, err := conn.DeleteVirtualMFADeviceWithContext(ctx, &iam.DeleteVirtualMFADeviceInput{
182223
SerialNumber: aws.String(d.Id()),
@@ -222,3 +263,18 @@ func FindVirtualMFADeviceBySerialNumber(ctx context.Context, conn *iam.IAM, seri
222263

223264
return output, nil
224265
}
266+
267+
func parseVirtualMFADeviceARN(s string) (path, name string, err error) {
268+
arn, err := arn.Parse(s)
269+
if err != nil {
270+
return "", "", err
271+
}
272+
273+
re := regexp.MustCompile(`^mfa(/|/[\x{0021}-\x{007E}]+/)([-A-Za-z0-9_+=,.@]+)$`)
274+
matches := re.FindStringSubmatch(arn.Resource)
275+
if len(matches) != 3 {
276+
return "", "", fmt.Errorf("IAM Virtual MFA Device ARN: invalid resource section (%s)", arn.Resource)
277+
}
278+
279+
return matches[1], matches[2], nil
280+
}

internal/service/iam/virtual_mfa_device_test.go

+68-14
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,60 @@ func TestAccIAMVirtualMFADevice_basic(t *testing.T) {
3434
Steps: []resource.TestStep{
3535
{
3636
Config: testAccVirtualMFADeviceConfig_basic(rName),
37-
Check: resource.ComposeTestCheckFunc(
37+
Check: resource.ComposeAggregateTestCheckFunc(
3838
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
3939
acctest.CheckResourceAttrGlobalARN(resourceName, "arn", "iam", fmt.Sprintf("mfa/%s", rName)),
4040
resource.TestCheckResourceAttrSet(resourceName, "base_32_string_seed"),
41+
resource.TestCheckNoResourceAttr(resourceName, "enable_date"),
42+
resource.TestCheckResourceAttr(resourceName, "path", "/"),
4143
resource.TestCheckResourceAttrSet(resourceName, "qr_code_png"),
44+
resource.TestCheckNoResourceAttr(resourceName, "user_name"),
4245
),
4346
},
4447
{
45-
ResourceName: resourceName,
46-
ImportState: true,
47-
ImportStateVerify: true,
48-
ImportStateVerifyIgnore: []string{"path", "virtual_mfa_device_name", "base_32_string_seed", "qr_code_png"},
48+
ResourceName: resourceName,
49+
ImportState: true,
50+
ImportStateVerify: true,
51+
ImportStateVerifyIgnore: []string{
52+
"base_32_string_seed",
53+
"qr_code_png",
54+
},
55+
},
56+
},
57+
})
58+
}
59+
60+
func TestAccIAMVirtualMFADevice_path(t *testing.T) {
61+
ctx := acctest.Context(t)
62+
var conf iam.VirtualMFADevice
63+
resourceName := "aws_iam_virtual_mfa_device.test"
64+
65+
path := "/path/"
66+
67+
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
68+
69+
resource.ParallelTest(t, resource.TestCase{
70+
PreCheck: func() { acctest.PreCheck(ctx, t) },
71+
ErrorCheck: acctest.ErrorCheck(t, iam.EndpointsID),
72+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
73+
CheckDestroy: testAccCheckVirtualMFADeviceDestroy(ctx),
74+
Steps: []resource.TestStep{
75+
{
76+
Config: testAccVirtualMFADeviceConfig_path(rName, path),
77+
Check: resource.ComposeAggregateTestCheckFunc(
78+
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
79+
acctest.CheckResourceAttrGlobalARN(resourceName, "arn", "iam", fmt.Sprintf("mfa%s%s", path, rName)),
80+
resource.TestCheckResourceAttr(resourceName, "path", path),
81+
),
82+
},
83+
{
84+
ResourceName: resourceName,
85+
ImportState: true,
86+
ImportStateVerify: true,
87+
ImportStateVerifyIgnore: []string{
88+
"base_32_string_seed",
89+
"qr_code_png",
90+
},
4991
},
5092
},
5193
})
@@ -66,21 +108,24 @@ func TestAccIAMVirtualMFADevice_tags(t *testing.T) {
66108
Steps: []resource.TestStep{
67109
{
68110
Config: testAccVirtualMFADeviceConfig_tags1(rName, "key1", "value1"),
69-
Check: resource.ComposeTestCheckFunc(
111+
Check: resource.ComposeAggregateTestCheckFunc(
70112
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
71113
resource.TestCheckResourceAttr(resourceName, "tags.%", "1"),
72114
resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"),
73115
),
74116
},
75117
{
76-
ResourceName: resourceName,
77-
ImportState: true,
78-
ImportStateVerify: true,
79-
ImportStateVerifyIgnore: []string{"path", "virtual_mfa_device_name", "base_32_string_seed", "qr_code_png"},
118+
ResourceName: resourceName,
119+
ImportState: true,
120+
ImportStateVerify: true,
121+
ImportStateVerifyIgnore: []string{
122+
"base_32_string_seed",
123+
"qr_code_png",
124+
},
80125
},
81126
{
82127
Config: testAccVirtualMFADeviceConfig_tags2(rName, "key1", "value1updated", "key2", "value2"),
83-
Check: resource.ComposeTestCheckFunc(
128+
Check: resource.ComposeAggregateTestCheckFunc(
84129
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
85130
resource.TestCheckResourceAttr(resourceName, "tags.%", "2"),
86131
resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"),
@@ -89,7 +134,7 @@ func TestAccIAMVirtualMFADevice_tags(t *testing.T) {
89134
},
90135
{
91136
Config: testAccVirtualMFADeviceConfig_tags1(rName, "key2", "value2"),
92-
Check: resource.ComposeTestCheckFunc(
137+
Check: resource.ComposeAggregateTestCheckFunc(
93138
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
94139
resource.TestCheckResourceAttr(resourceName, "tags.%", "1"),
95140
resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"),
@@ -114,10 +159,9 @@ func TestAccIAMVirtualMFADevice_disappears(t *testing.T) {
114159
Steps: []resource.TestStep{
115160
{
116161
Config: testAccVirtualMFADeviceConfig_basic(rName),
117-
Check: resource.ComposeTestCheckFunc(
162+
Check: resource.ComposeAggregateTestCheckFunc(
118163
testAccCheckVirtualMFADeviceExists(ctx, resourceName, &conf),
119164
acctest.CheckResourceDisappears(ctx, acctest.Provider, tfiam.ResourceVirtualMFADevice(), resourceName),
120-
acctest.CheckResourceDisappears(ctx, acctest.Provider, tfiam.ResourceVirtualMFADevice(), resourceName),
121165
),
122166
ExpectNonEmptyPlan: true,
123167
},
@@ -182,6 +226,16 @@ resource "aws_iam_virtual_mfa_device" "test" {
182226
`, rName)
183227
}
184228

229+
func testAccVirtualMFADeviceConfig_path(rName, path string) string {
230+
return fmt.Sprintf(`
231+
resource "aws_iam_virtual_mfa_device" "test" {
232+
virtual_mfa_device_name = %[1]q
233+
234+
path = %[2]q
235+
}
236+
`, rName, path)
237+
}
238+
185239
func testAccVirtualMFADeviceConfig_tags1(rName, tagKey1, tagValue1 string) string {
186240
return fmt.Sprintf(`
187241
resource "aws_iam_virtual_mfa_device" "test" {

website/docs/r/iam_virtual_mfa_device.html.markdown

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Provides an IAM Virtual MFA Device.
1313
~> **Note:** All attributes will be stored in the raw state as plain-text.
1414
[Read more about sensitive data in state](https://www.terraform.io/docs/state/sensitive-data.html).
1515

16+
~> **Note:** A virtual MFA device cannot be directly associated with an IAM User from Terraform.
17+
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.
18+
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).
19+
1620
## Example Usage
1721

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

3842
* `arn` - The Amazon Resource Name (ARN) specifying the virtual mfa device.
3943
* `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.
40-
* `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.
44+
* `enable_date` - The date and time when the virtual MFA device was enabled.
45+
* `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.
4146
* `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).
47+
* `user_name` - The associated IAM User name if the virtual MFA device is enabled.
4248

4349
## Import
4450

0 commit comments

Comments
 (0)