diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e3834c..0abe244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,35 +1,47 @@ # Versions -* v1.0.0 +## Version 1.x + +* 1.0.0 - tag initial release for tf 0.11 -* v2.0.0 +## Version 2.x + +* 2.0.0 - for tf-0.12 support -* v2.0.1 +* 2.0.1 - add create_access_key variable, but do not implement - format - change saml_role to list -* v2.0.2 +* 2.0.2 - do not include groups if groups empty - add ```service_account``` variable (true|false) - if service account, use contact vs user -* v2.1 +* 2.1 - add pre-commit to generate docs -* v2.2 +* 2.2 - add enable_sending_mail to enable email address in SES (this generates a request to the user) -* v2.3 +* 2.3 - change default for enable_sending_mail to false -* v2.4.0 -- 20210405 +* 2.4.0 -- 20210405 - use `ldap-get-attribute` to get email address -* v2.5.0 -- 20210512 +* 2.5.0 -- 20210512 - add `attached_policies` to enable adding policies for an IAM account (generally, just services) -* v2.5.1 -- 20210526 +* 2.5.1 -- 20210526 - change computing of output of user_password + +* 2.6.0 -- 2022-05-20 + - update bin/show-user-info.sh to accept TFCOMMAND for an alternate terraform binary, or to pull from the git root file + +* 2.7.0 -- 2022-06-17 + - add inline_policies + - add path + - enable code for creating access keys diff --git a/README.md b/README.md index 55c9a00..0a11828 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ module "admin_user_bond0007" { # roles = [ ] # saml_role = [ "role-name" ] # create_access_keys = true + # inline_policies = [ { name = "my-policy", policy = data.aws_iam_policy_document.my-policy.json } ] } ``` @@ -47,6 +48,9 @@ No requirements. | Name | Version | |------|---------| | [aws](#provider\_aws) | n/a | +| [local](#provider\_local) | n/a | +| [null](#provider\_null) | n/a | +| [time](#provider\_time) | n/a | ## Modules @@ -58,11 +62,17 @@ No requirements. | Name | Type | |------|------| +| [aws_iam_access_key.iam_access_key_v1](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource | | [aws_iam_user.user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource | | [aws_iam_user_group_membership.user_groups](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_group_membership) | resource | | [aws_iam_user_login_profile.user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_login_profile) | resource | +| [aws_iam_user_policy.user_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy) | resource | | [aws_iam_user_policy_attachment.user_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment) | resource | | [aws_ses_email_identity.user_send_mail](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ses_email_identity) | resource | +| [local_file.access_key_file](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource | +| [local_file.rotate_keys_tfvars](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource | +| [null_resource.rotate_keys_tfvars](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [time_sleep.wait_iam_access_key_v1](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | | [aws_arn.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/arn) | data source | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | @@ -72,13 +82,18 @@ No requirements. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [attached\_policies](#input\_attached\_policies) | List of IAM Policy ARNs to attach to this user (only for service accounts) | `list(string)` | `[]` | no | -| [create\_access\_keys](#input\_create\_access\_keys) | Set to 1 or true to create access keys (not implemented) | `bool` | `false` | no | +| [component\_tags](#input\_component\_tags) | Additional tags for Components (role, policy) | `map(map(string))` |
{
"policy": {},
"user": {}
} | no |
+| [create\_access\_keys](#input\_create\_access\_keys) | Flag to create access keys | `bool` | `false` | no |
| [email\_address](#input\_email\_address) | Email address for the user | `string` | n/a | yes |
| [enable\_sending\_mail](#input\_enable\_sending\_mail) | Enable using email address for the user from SES | `bool` | `false` | no |
| [generate\_password](#input\_generate\_password) | Flag to generate\_password upon creation (requires pgp\_key set) | `bool` | `false` | no |
| [groups](#input\_groups) | Groups to which the user belongs | `list(string)` | `[]` | no |
| [iam\_username](#input\_iam\_username) | AWS username for user (default a-username) | `string` | `""` | no |
+| [inline\_policies](#input\_inline\_policies) | List of IAM Policy Document objects to include in this role. Format is {name=name,policy=policy-json} | `list(object({ name = string, policy = string }))` | `[]` | no |
+| [path](#input\_path) | IAM path | `string` | `"/"` | no |
| [pgp\_key](#input\_pgp\_key) | PGP key used to encrypt access key | `string` | `""` | no |
+| [pgp\_key\_file](#input\_pgp\_key\_file) | PGP key filename for encrypting the access key (supercedes pgp\_key) | `string` | `""` | no |
+| [profile](#input\_profile) | AWS Profile Name, used generating key rotation file | `string` | `null` | no |
| [reference](#input\_reference) | Remedy ticket reference number for the user | `string` | `""` | no |
| [roles](#input\_roles) | Roles to which the user belongs | `list(string)` | `[]` | no |
| [saml\_role](#input\_saml\_role) | SAML role for user | `list(string)` | `[]` | no |
@@ -90,6 +105,8 @@ No requirements.
| Name | Description |
|------|-------------|
+| [aws\_access\_key\_id](#output\_aws\_access\_key\_id) | AWS Access Key (current) |
+| [aws\_secret\_access\_key](#output\_aws\_secret\_access\_key) | AWS Secret Access Key [encrypted] (current) |
| [user\_arn](#output\_user\_arn) | User ARN |
| [user\_name](#output\_user\_name) | User name |
| [user\_password](#output\_user\_password) | User Password |
diff --git a/access_keys.tf b/access_keys.tf
new file mode 100644
index 0000000..6471343
--- /dev/null
+++ b/access_keys.tf
@@ -0,0 +1,75 @@
+resource "aws_iam_access_key" "iam_access_key_v1" {
+ count = var.create_access_keys ? 1 : 0
+ user = aws_iam_user.user.name
+ pgp_key = local.pgp_key
+
+ depends_on = [time_sleep.wait_iam_access_key_v1]
+}
+
+# this gives a prior key time to be removed. When doing just one key like
+# here, this may not be necessary
+resource "time_sleep" "wait_iam_access_key_v1" {
+ count = var.create_access_keys ? 1 : 0
+
+ create_duration = "60s"
+}
+
+locals {
+ rotate_keys_tfvars = templatefile("${path.module}/templates/rotate-keys.tpl",
+ {
+ username = aws_iam_user.user.name
+ region = local.account_environment == "ew" ? "us-east-1" : "us-gov-east-1"
+ profile = var.profile == null ? "" : var.profile
+ pgp_key_file = fileexists(var.pgp_key_file) ? var.pgp_key_file : ""
+ })
+ access_key_file = templatefile("${path.module}/templates/access_key.tpl",
+ {
+ username = aws_iam_user.user.name
+ profile = var.profile == null ? "" : var.profile
+ account_id = local.account_id
+ create_date = var.create_access_keys ? aws_iam_access_key.iam_access_key_v1[0].create_date : ""
+ access_key_id = var.create_access_keys ? aws_iam_access_key.iam_access_key_v1[0].id : ""
+ encrypted_secret = var.create_access_keys ? aws_iam_access_key.iam_access_key_v1[0].encrypted_secret : ""
+ })
+}
+
+resource "null_resource" "rotate_keys_tfvars" {
+ count = var.create_access_keys ? 1 : 0
+ triggers = {
+ username = aws_iam_user.user.name
+ access_keys_directory = format("access_keys/%v", aws_iam_user.user.name)
+ }
+
+ provisioner "local-exec" {
+ when = create
+ command = "test -d ${path.root}/${self.triggers.access_keys_directory} || mkdir -p ${path.root}/${self.triggers.access_keys_directory}"
+ }
+
+ provisioner "local-exec" {
+ when = create
+ working_dir = self.triggers.access_keys_directory
+ command = "echo access_key.yml > .gitignore"
+ }
+
+ # provisioner "local-exec" {
+ # when = create
+ # working_dir = self.triggers.access_keys_directory
+ # command = "ln -sf ../../${var.ppg_key_file} ."
+ # }
+}
+
+resource "local_file" "rotate_keys_tfvars" {
+ count = var.create_access_keys ? 1 : 0
+ content = local.rotate_keys_tfvars
+ filename = var.create_access_keys ? format("%v/%v/variables.auto.tfvars", path.root, null_resource.rotate_keys_tfvars[0].triggers.access_keys_directory) : "empty.txt"
+
+ depends_on = [null_resource.rotate_keys_tfvars]
+}
+
+resource "local_file" "access_key_file" {
+ count = var.create_access_keys ? 1 : 0
+ content = local.access_key_file
+ filename = var.create_access_keys ? format("%v/%v/access_key.yml", path.root, null_resource.rotate_keys_tfvars[0].triggers.access_keys_directory) : "empty.txt"
+
+ depends_on = [null_resource.rotate_keys_tfvars]
+}
diff --git a/base_tags.tf b/base_tags.tf
new file mode 100644
index 0000000..848cfbc
--- /dev/null
+++ b/base_tags.tf
@@ -0,0 +1,7 @@
+locals {
+ base_tags = {
+ "boc:tf_module_version" = local._module_version
+ "boc:tf_module_name" = local._module_name
+ "boc:created_by" = "terraform"
+ }
+}
diff --git a/bin/show-user-info.sh b/bin/show-user-info.sh
index dbc7da6..360706e 100755
--- a/bin/show-user-info.sh
+++ b/bin/show-user-info.sh
@@ -1,5 +1,15 @@
#!/bin/bash
+get_git_root()
+{
+ TOP=$(git rev-parse --show-toplevel 2>/dev/null)
+ if [ -z "$TOP" ]
+ then
+ TOP=$HOME
+ fi
+}
+
+VERSION="1.1.0"
if [ ! -x /usr/bin/jq ]
then
echo "* no 'jq' program installed, cannot execute"
@@ -19,7 +29,44 @@ fi
VARS="user_name user_arn user_password"
declare -A values
-jsondata=$(terraform output -json $MODULE)
+# path or name of terraform binary
+# get from top of git repo or $HOME/.tf-control
+get_git_root
+if [ -z "$TFCONTROL" ]
+then
+ if [ -r $TOP/.tf-control ]
+ then
+ TFCONTROL=$TOP/.tf-control
+ elif [ -r $HOME/.tf-control ]
+ then
+ TFCONTROL=$HOME/.tf-control
+ fi
+fi
+if [ ! -z "$TFCONTROL" ]
+then
+ source $TFCONTROL
+fi
+
+if [ -z $TFCOMMAND ]
+then
+ TFCOMMAND="terraform"
+fi
+
+# look for config file
+if [ -z "$TF_CLI_CONFIG_FILE" ]
+then
+ if [ -r $TOP/.tf-control.tfrc ]
+ then
+ export TF_CLI_CONFIG_FILE=$TOP/.tf-control.tfrc
+ elif [ -r $HOME/.tf-control.tfrc ]
+ then
+ export TF_CLI_CONFIG_FILE=$HOME/.tf-control.tfrc
+ else
+ unset TF_CLI_CONFIG_FILE
+ fi
+fi
+
+jsondata=$($TERRAFORM output -json $MODULE)
echo "# $(date +%Y%m%d)"
for k in $VARS
do
diff --git a/data.tf b/data.tf
index c99f19d..16506e6 100644
--- a/data.tf
+++ b/data.tf
@@ -5,19 +5,3 @@ data "aws_arn" "current" {
}
data "aws_region" "current" {}
-
-# output "caller_account_id" {
-# value = data.aws_caller_identity.current.account_id
-# }
-#
-# output "account_caller_arn" {
-# value = data.aws_caller_identity.current.arn
-# }
-#
-# output "account_caller_arn_partition" {
-# value = data.aws_arn.current.partition
-# }
-#
-# output "account_region_name" {
-# value = data.aws_region.current.name
-# }
diff --git a/locals.tf b/locals.tf
new file mode 100644
index 0000000..24d3131
--- /dev/null
+++ b/locals.tf
@@ -0,0 +1,6 @@
+locals {
+ # account_id = var.account_id != "" ? var.account_id : data.aws_caller_identity.current.account_id
+ account_id = data.aws_caller_identity.current.account_id
+ account_environment = data.aws_arn.current.partition == "aws-us-gov" ? "gov" : "ew"
+ region = data.aws_region.current.name
+}
diff --git a/main.tf b/main.tf
index 218acf0..7ca369d 100644
--- a/main.tf
+++ b/main.tf
@@ -36,6 +36,7 @@
* # roles = [ ]
* # saml_role = [ "role-name" ]
* # create_access_keys = true
+* # inline_policies = [ { name = "my-policy", policy = data.aws_iam_policy_document.my-policy.json } ]
* }
* ```
*/
@@ -45,8 +46,9 @@ locals {
username = lower(var.username)
iam_username = var.iam_username != "" ? var.iam_username : "a-${local.username}"
email_address = var.email_address != "" ? lower(var.email_address) : module.user_email.search_result["attribute_value"][0]
- pgp_key_exists = var.pgp_key != "" ? 1 : 0
- generate_password = var.generate_password ? local.pgp_key_exists : 0
+ pgp_key_exists = var.pgp_key != "" || fileexists(var.pgp_key_file)
+ pgp_key = fileexists(var.pgp_key_file) ? file(var.pgp_key_file) : var.pgp_key
+ generate_password = var.generate_password && local.pgp_key_exists
user_password = local.generate_password == 1 ? element(concat(aws_iam_user_login_profile.user[*].encrypted_password, tolist([""])), 0) : ""
ap1 = var.service_account ? var.attached_policies : []
ap2 = [for arn in local.ap1 : {
@@ -70,15 +72,12 @@ locals {
}
tags = merge(
+ local.base_tags,
var.tags,
local.tags_username,
local.tags_email[local.email_address != "" ? "exists" : "not_exists"],
local.tags_service,
local.tags_reference[var.reference != "" ? "exists" : "not_exists"],
- {
- "boc:tf_module_version" = local._module_version
- "boc:created_by" = "terraform"
- },
)
}
@@ -87,17 +86,19 @@ locals {
#---
resource "aws_iam_user" "user" {
name = local.iam_username
- tags = local.tags
+ path = var.path
force_destroy = true
+
+ tags = local.tags
}
#---
# login profile (if genrate password)
#---
resource "aws_iam_user_login_profile" "user" {
- count = local.generate_password
+ count = local.generate_password ? 1 : 0
user = aws_iam_user.user.name
- pgp_key = var.pgp_key
+ pgp_key = local.generate_password ? local.pgp_key : null
password_reset_required = true
lifecycle {
@@ -139,3 +140,9 @@ resource "aws_iam_user_policy_attachment" "user_policy" {
policy_arn = each.value
}
+
+resource "aws_iam_user_policy" "user_policy" {
+ for_each = { for p in var.inline_policies : p.name => p.policy }
+ user = aws_iam_user.user.name
+ policy = each.value
+}
diff --git a/outputs.access_keys.tf b/outputs.access_keys.tf
new file mode 100644
index 0000000..33073de
--- /dev/null
+++ b/outputs.access_keys.tf
@@ -0,0 +1,20 @@
+## output "aws_access_key_id_prev" {
+## description = "AWS Access Key (previous)"
+## value = ""
+## }
+##
+## output "aws_secret_access_key_prev" {
+## description = "AWS Secret Access Key [encrypted] (previous)"
+## value = ""
+## }
+
+output "aws_access_key_id" {
+ description = "AWS Access Key (current)"
+ value = var.create_access_keys ? aws_iam_access_key.iam_access_key_v1[0].id : ""
+}
+
+output "aws_secret_access_key" {
+ description = "AWS Secret Access Key [encrypted] (current)"
+ value = var.create_access_keys ? aws_iam_access_key.iam_access_key_v1[0].encrypted_secret : ""
+}
+
diff --git a/templates/access_key.tpl b/templates/access_key.tpl
new file mode 100644
index 0000000..ef4acd1
--- /dev/null
+++ b/templates/access_key.tpl
@@ -0,0 +1,7 @@
+account: ${account_id}
+profile: ${profile}
+create_date: ${create_date}
+username: ${username}
+access_key_id: ${access_key_id}
+access_secret_key_encrypted: ${encrypted_secret}
+access_secret_key_encrypted:
diff --git a/templates/rotate-keys.tpl b/templates/rotate-keys.tpl
new file mode 100644
index 0000000..7e456a2
--- /dev/null
+++ b/templates/rotate-keys.tpl
@@ -0,0 +1,5 @@
+username = "${username}"
+region = "${region}"
+profile = "${profile}"
+service_profile = ""
+gpg_key_file = "%{ if pgp_key_file != ""}../../${pgp_key_file}%{ else }%{ endif }"
diff --git a/variables.tf b/variables.tf
index 08775a9..b5fbbda 100644
--- a/variables.tf
+++ b/variables.tf
@@ -58,13 +58,25 @@ variable "pgp_key" {
default = ""
}
+variable "pgp_key_file" {
+ description = "PGP key filename for encrypting the access key (supercedes pgp_key)"
+ type = string
+ default = ""
+}
+
# may be a sub-module by user after rotation
variable "create_access_keys" {
- description = "Set to 1 or true to create access keys (not implemented)"
+ description = "Flag to create access keys"
type = bool
default = false
}
+variable "profile" {
+ description = "AWS Profile Name, used generating key rotation file"
+ type = string
+ default = null
+}
+
variable "tags" {
description = "Additional tags to include on the objects"
type = map(string)
@@ -82,3 +94,21 @@ variable "attached_policies" {
type = list(string)
default = []
}
+
+variable "inline_policies" {
+ description = "List of IAM Policy Document objects to include in this role. Format is {name=name,policy=policy-json}"
+ type = list(object({ name = string, policy = string }))
+ default = []
+}
+
+variable "path" {
+ description = "IAM path"
+ type = string
+ default = "/"
+}
+
+variable "component_tags" {
+ description = "Additional tags for Components (role, policy)"
+ type = map(map(string))
+ default = { "user" = {}, "policy" = {} }
+}
diff --git a/version.tf b/version.tf
index 5624ad5..997b4d4 100644
--- a/version.tf
+++ b/version.tf
@@ -1,3 +1,4 @@
locals {
- _module_version = "2.5.1"
+ _module_name = "aws-iam-user"
+ _module_version = "2.7.0"
}