Skip to content

Commit

Permalink
New data source "st-aws_iam_users". (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
styumyum authored Oct 29, 2024
1 parent c00d027 commit 74a5f18
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b # v4.2.0
uses: goreleaser/goreleaser-action@5742e2a039330cbb23ebf35f046f814d4c6ff811 # v5.1.0
with:
args: release --clean
env:
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ The reason behind every resources and data sources are stated as below:
- Added client_config block to allow overriding the Provider configuration.
- **st-aws_iam_users**
The data source
[*aws_iam_users*](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_users)
in official AWS Terraform provider does not support querying AWS IAM users using tags.
References
----------
Expand Down
242 changes: 242 additions & 0 deletions aws/data_source_iam_users.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package aws

import (
"context"
"time"

awsIAMClient "github.com/aws/aws-sdk-go-v2/service/iam"
awsTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/cenkalti/backoff"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)

var (
_ datasource.DataSource = &iamUsersDataSource{}
_ datasource.DataSourceWithConfigure = &iamUsersDataSource{}
)

func NewIamUsersDataSource() datasource.DataSource {
return &iamUsersDataSource{}
}

type iamUsersDataSource struct {
client *awsIAMClient.Client
}

type iamUsersDataSourceModel struct {
Tags types.Map `tfsdk:"tags"`
Users []*iamUserDetail `tfsdk:"users"`
}

type iamUserDetail struct {
Arn types.String `tfsdk:"arn"`
CreateDate types.String `tfsdk:"create_date"`
Path types.String `tfsdk:"path"`
UserId types.String `tfsdk:"user_id"`
UserName types.String `tfsdk:"user_name"`
PasswordLastUsed types.String `tfsdk:"password_last_used"`
Tags types.Map `tfsdk:"tags"`
}

func (d *iamUsersDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_iam_users"
}

func (d *iamUsersDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Use this data source to retrieve list of IAM users with attached tags.",
Attributes: map[string]schema.Attribute{
"tags": schema.MapAttribute{
Description: "Filter by map of tags assigned to the IAM user.",
ElementType: types.StringType,
Optional: true,
},
"users": schema.ListNestedAttribute{
Description: "List of IAM users.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"arn": schema.StringAttribute{
Description: "The Amazon Resource Name (ARN) that identifies the user.",
Computed: true,
},
"create_date": schema.StringAttribute{
Description: "The date and time, in ISO 8601 date-time string format, when the user was created.",
Computed: true,
},
"path": schema.StringAttribute{
Description: "The path to the user.",
Computed: true,
},
"user_id": schema.StringAttribute{
Description: "The stable and unique string identifying the user.",
Computed: true,
},
"user_name": schema.StringAttribute{
Description: "The friendly name identifying the user.",
Computed: true,
},
"password_last_used": schema.StringAttribute{
Description: "The date and time, in ISO 8601 date-time string format, when the user's password was last used to sign in to an Amazon Web Services website.",
Computed: true,
},
"tags": schema.MapAttribute{
Description: "A list of tags that are associated with the user.",
ElementType: types.StringType,
Computed: true,
},
},
},
},
},
}
}

func (r *iamUsersDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
r.client = req.ProviderData.(awsClients).iamClient
}

func (d *iamUsersDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var plan *iamUsersDataSourceModel
getPlanDiags := req.Config.Get(ctx, &plan)
resp.Diagnostics.Append(getPlanDiags...)
if resp.Diagnostics.HasError() {
return
}
state := &iamUsersDataSourceModel{}
state.Users = []*iamUserDetail{}

inputTags := make(map[string]string)
if !(plan.Tags.IsUnknown() && plan.Tags.IsNull()) {
state.Tags = plan.Tags
// Convert from Terraform map type to Go map type
convertTagsDiags := plan.Tags.ElementsAs(ctx, &inputTags, false)
resp.Diagnostics.Append(convertTagsDiags...)
if resp.Diagnostics.HasError() {
return
}
}

var iamUsersList []awsTypes.User
listUsersFunc := func() error {
// Init variable iamUsersList to solve redundant values in backoff retry.
iamUsersList = []awsTypes.User{}
listUsersInput := &awsIAMClient.ListUsersInput{}
for {
iamUsers, err := d.client.ListUsers(ctx, listUsersInput)
if err != nil {
if _t, ok := err.(awserr.Error); ok {
if isAbleToRetry(_t.Code()) {
return err
} else {
return backoff.Permanent(err)
}
} else {
return backoff.Permanent(err)
}
}
iamUsersList = append(iamUsersList, iamUsers.Users...)
if !iamUsers.IsTruncated {
break
}
listUsersInput.Marker = iamUsers.Marker
}
return nil
}

listUsersBackoff := backoff.NewExponentialBackOff()
listUsersBackoff.MaxElapsedTime = 1 * time.Minute
err := backoff.Retry(listUsersFunc, listUsersBackoff)
if err != nil {
resp.Diagnostics.AddError(
"[API ERROR] Failed to List IAM Users",
err.Error(),
)
return
}

iamUsersLoop:
for _, iamUser := range iamUsersList {
var iamUserTagsList []awsTypes.Tag
listUserTagsFunc := func() error {
// Init variable iamUserTagsList to solve redundant values in backoff retry.
iamUserTagsList = []awsTypes.Tag{}
listUserTagsInput := &awsIAMClient.ListUserTagsInput{
UserName: iamUser.UserName,
}
for {
iamUserTags, err := d.client.ListUserTags(ctx, listUserTagsInput)
if err != nil {
if _t, ok := err.(awserr.Error); ok {
if isAbleToRetry(_t.Code()) {
return err
} else {
return backoff.Permanent(err)
}
} else {
return backoff.Permanent(err)
}
}
iamUserTagsList = append(iamUserTagsList, iamUserTags.Tags...)
if !iamUserTags.IsTruncated {
break
}
listUserTagsInput.Marker = iamUserTags.Marker
}
return nil
}

listUserTagsBackoff := backoff.NewExponentialBackOff()
listUserTagsBackoff.MaxElapsedTime = 1 * time.Minute
err := backoff.Retry(listUserTagsFunc, listUserTagsBackoff)
if err != nil {
resp.Diagnostics.AddError(
"[API ERROR] Failed to List IAM User's Tags",
err.Error(),
)
return
}

matchUserTagsLoop:
for key, value := range inputTags {
for _, iamUserTag := range iamUserTagsList {
if *iamUserTag.Key == key && *iamUserTag.Value == value {
// When the tag is found, continue to next tag.
continue matchUserTagsLoop
}
}
// When a pair of tag is not matched, continue to next user.
continue iamUsersLoop
}

stateUser := &iamUserDetail{
Arn: types.StringValue(*iamUser.Arn),
CreateDate: types.StringValue(iamUser.CreateDate.String()),
Path: types.StringValue(*iamUser.Path),
UserId: types.StringValue(*iamUser.UserId),
UserName: types.StringValue(*iamUser.UserName),
}
if iamUser.PasswordLastUsed != nil {
stateUser.PasswordLastUsed = types.StringValue(iamUser.PasswordLastUsed.String())
}
stateUserTags := make(map[string]attr.Value)
for _, iamUserTag := range iamUserTagsList {
stateUserTags[*iamUserTag.Key] = types.StringValue(*iamUserTag.Value)
}
stateUser.Tags = types.MapValueMust(types.StringType, stateUserTags)
state.Users = append(state.Users, stateUser)
}

setStateDiags := resp.State.Set(ctx, &state)
resp.Diagnostics.Append(setStateDiags...)
if resp.Diagnostics.HasError() {
return
}
}
6 changes: 3 additions & 3 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ func (p *awsServicesProvider) Schema(_ context.Context, _ provider.SchemaRequest
},
"access_key": schema.StringAttribute{
Description: "URI for AWS Services API. May also be provided via " +
"AWS_ACCESS_KEY_ID environment variable",
"AWS_ACCESS_KEY_ID environment variable.",
Optional: true,
},
"secret_key": schema.StringAttribute{
Description: "API key for AWS Services API. May also be provided " +
"via AWS_SECRET_ACCESS_KEY environment variable",
"via AWS_SECRET_ACCESS_KEY environment variable.",
Optional: true,
Sensitive: true,
},
Expand Down Expand Up @@ -231,8 +231,8 @@ func (p *awsServicesProvider) Configure(ctx context.Context, req provider.Config
func (p *awsServicesProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewCdnDomainDataSource,
NewIamUsersDataSource,
}
//return nil
}

func (p *awsServicesProvider) Resources(_ context.Context) []func() resource.Resource {
Expand Down
2 changes: 1 addition & 1 deletion aws/resource_iam_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ func (r *iamPolicyResource) getPolicyDocument(ctx context.Context, plan *iamPoli
}
}
} else {
return nil, nil, fmt.Errorf("The %v policy not found.", policyName)
return nil, nil, fmt.Errorf("the %v policy not found", policyName)
}
}

Expand Down
47 changes: 47 additions & 0 deletions docs/data-sources/iam_users.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "st-aws_iam_users Data Source - st-aws"
subcategory: ""
description: |-
Use this data source to retrieve list of IAM users.
---

# st-aws_iam_users (Data Source)

Use this data source to retrieve list of IAM users.

## Example Usage

```terraform
data "st-aws_iam_users" "aws_iam_users" {
tags = {
"key" = "value"
}
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Optional

- `tags` (Map of String) Filter by map of tags assigned to the IAM user.

### Read-Only

- `users` (Attributes List) List of IAM users. (see [below for nested schema](#nestedatt--users))

<a id="nestedatt--users"></a>
### Nested Schema for `users`

Read-Only:

- `arn` (String) The Amazon Resource Name (ARN) that identifies the user.
- `create_date` (String) The date and time, in ISO 8601 date-time string format, when the user was created.
- `password_last_used` (String) The date and time, in ISO 8601 date-time string format, when the user's password was last used to sign in to an Amazon Web Services website.
- `path` (String) The path to the user.
- `tags` (Map of String) A list of tags that are associated with the user.
- `user_id` (String) The stable and unique string identifying the user.
- `user_name` (String) The friendly name identifying the user.


6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ provider "st-aws" {}

### Optional

- `region` (String) Region for AWS Services API. May also be provided via AWS_REGION environment variable. Default to use ap-southeast-1.
- `access_key` (String) URI for AWS Services API. May also be provided via AWS_ACCESS_KEY_ID environment variable
- `secret_key` (String, Sensitive) API key for AWS Services API. May also be provided via AWS_SECRET_ACCESS_KEY environment variable
- `region` (String) Region for AWS Services API. May also be provided via AWS_REGION environment variable.
- `access_key` (String) URI for AWS Services API. May also be provided via AWS_ACCESS_KEY_ID environment variable.
- `secret_key` (String, Sensitive) API key for AWS Services API. May also be provided via AWS_SECRET_ACCESS_KEY environment variable.
2 changes: 1 addition & 1 deletion docs/resources/iam_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
page_title: "st-aws_iam_policy Resource - st-aws"
subcategory: ""
description: |-
Provides a RAM Policy resource that manages policy content exceeding character limits by splitting it into smaller segments. These segments are combined to form a complete policy attached to the user. However, the policy like `ReadOnlyAccess` that exceed the maximum length of a policy, they will be attached directly to the user.
Provides a RAM Policy resource that manages policy content exceeding character limits by splitting it into smaller segments. These segments are combined to form a complete policy attached to the user. However, the policy like ReadOnlyAccess that exceed the maximum length of a policy, they will be attached directly to the user.
---

# st-aws_iam_policy (Resource)
Expand Down
5 changes: 5 additions & 0 deletions examples/data-sources/st-aws_iam_users/data-source.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
data "st-aws_iam_users" "aws_iam_users" {
tags = {
"key" = "value"
}
}

0 comments on commit 74a5f18

Please sign in to comment.