diff --git a/README.md b/README.md
index 383a3887..c9d7b25e 100644
--- a/README.md
+++ b/README.md
@@ -111,7 +111,7 @@ Using a [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overvie
module "s3_bucket" {
source = "cloudposse/s3-bucket/aws"
# Cloud Posse recommends pinning every module to a specific version
- # version = "x.x.x"
+ # version = "x.x.x"
acl = "private"
enabled = true
user_enabled = true
@@ -129,7 +129,7 @@ Using [grants](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html
module "s3_bucket" {
source = "cloudposse/s3-bucket/aws"
# Cloud Posse recommends pinning every module to a specific version
- # version = "x.x.x"
+ # version = "x.x.x"
acl = ""
enabled = true
user_enabled = true
@@ -156,6 +156,39 @@ module "s3_bucket" {
}
```
+Allowing specific principal ARNs to perform actions on the bucket:
+
+```hcl
+module "s3_bucket" {
+ source = "cloudposse/s3-bucket/aws"
+ # Cloud Posse recommends pinning every module to a specific version
+ # version = "x.x.x"
+ acl = "private"
+ enabled = true
+ user_enabled = true
+ versioning_enabled = false
+ allowed_bucket_actions = ["s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation"]
+ name = "app"
+ stage = "test"
+ namespace = "eg"
+
+ privileged_principal_arns = {
+ "arn:aws:iam::123456789012:role/principal1" = ["prefix1/", "prefix2/"]
+ "arn:aws:iam::123456789012:role/principal2" = [""]
+ }
+ privileged_principal_actions = [
+ "s3:PutObject",
+ "s3:PutObjectAcl",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ "s3:ListBucket",
+ "s3:ListBucketMultipartUploads",
+ "s3:GetBucketLocation",
+ "s3:AbortMultipartUpload"
+ ]
+}
+```
+
@@ -244,6 +277,8 @@ Available targets:
| [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no |
| [object\_lock\_configuration](#input\_object\_lock\_configuration) | A configuration for S3 object locking. With S3 Object Lock, you can store objects using a `write once, read many` (WORM) model. Object Lock can help prevent objects from being deleted or overwritten for a fixed amount of time or indefinitely. |
object({
mode = string # Valid values are GOVERNANCE and COMPLIANCE.
days = number
years = number
})
| `null` | no |
| [policy](#input\_policy) | A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy | `string` | `""` | no |
+| [privileged\_principal\_actions](#input\_privileged\_principal\_actions) | List of actions to permit `privileged_principal_arns` to perform on bucket and bucket prefixes (see `privileged_principal_arns`) | `list(string)` | `[]` | no |
+| [privileged\_principal\_arns](#input\_privileged\_principal\_arns) | (Optional) Map of IAM Principal ARNs to lists of S3 path prefixes to grant `privileged_principal_actions` permissions.
Resource list will include the bucket itself along with all the prefixes. Prefixes should not begin with '/'. | `map(list(string))` | `{}` | no |
| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no |
| [replication\_rules](#input\_replication\_rules) | DEPRECATED: Use s3\_replication\_rules instead. | `list(any)` | `null` | no |
| [restrict\_public\_buckets](#input\_restrict\_public\_buckets) | Set to `false` to disable the restricting of making the bucket public | `bool` | `true` | no |
@@ -427,8 +462,8 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply
### Contributors
-| [![Erik Osterman][osterman_avatar]][osterman_homepage]
[Erik Osterman][osterman_homepage] | [![Andriy Knysh][aknysh_avatar]][aknysh_homepage]
[Andriy Knysh][aknysh_homepage] | [![Maxim Mironenko][maximmi_avatar]][maximmi_homepage]
[Maxim Mironenko][maximmi_homepage] | [![Josh Myers][joshmyers_avatar]][joshmyers_homepage]
[Josh Myers][joshmyers_homepage] |
-|---|---|---|---|
+| [![Erik Osterman][osterman_avatar]][osterman_homepage]
[Erik Osterman][osterman_homepage] | [![Andriy Knysh][aknysh_avatar]][aknysh_homepage]
[Andriy Knysh][aknysh_homepage] | [![Maxim Mironenko][maximmi_avatar]][maximmi_homepage]
[Maxim Mironenko][maximmi_homepage] | [![Josh Myers][joshmyers_avatar]][joshmyers_homepage]
[Josh Myers][joshmyers_homepage] | [![Yonatan Koren][korenyoni_avatar]][korenyoni_homepage]
[Yonatan Koren][korenyoni_homepage] |
+|---|---|---|---|---|
[osterman_homepage]: https://github.com/osterman
@@ -439,6 +474,8 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply
[maximmi_avatar]: https://img.cloudposse.com/150x150/https://github.com/maximmi.png
[joshmyers_homepage]: https://github.com/joshmyers
[joshmyers_avatar]: https://img.cloudposse.com/150x150/https://github.com/joshmyers.png
+ [korenyoni_homepage]: https://github.com/korenyoni
+ [korenyoni_avatar]: https://img.cloudposse.com/150x150/https://github.com/korenyoni.png
[![README Footer][readme_footer_img]][readme_footer_link]
[![Beacon][beacon]][website]
diff --git a/README.yaml b/README.yaml
index 7f6a8e73..2d29189f 100644
--- a/README.yaml
+++ b/README.yaml
@@ -88,7 +88,7 @@ usage: |-
module "s3_bucket" {
source = "cloudposse/s3-bucket/aws"
# Cloud Posse recommends pinning every module to a specific version
- # version = "x.x.x"
+ # version = "x.x.x"
acl = "private"
enabled = true
user_enabled = true
@@ -106,7 +106,7 @@ usage: |-
module "s3_bucket" {
source = "cloudposse/s3-bucket/aws"
# Cloud Posse recommends pinning every module to a specific version
- # version = "x.x.x"
+ # version = "x.x.x"
acl = ""
enabled = true
user_enabled = true
@@ -133,6 +133,39 @@ usage: |-
}
```
+ Allowing specific principal ARNs to perform actions on the bucket:
+
+ ```hcl
+ module "s3_bucket" {
+ source = "cloudposse/s3-bucket/aws"
+ # Cloud Posse recommends pinning every module to a specific version
+ # version = "x.x.x"
+ acl = "private"
+ enabled = true
+ user_enabled = true
+ versioning_enabled = false
+ allowed_bucket_actions = ["s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation"]
+ name = "app"
+ stage = "test"
+ namespace = "eg"
+
+ privileged_principal_arns = {
+ "arn:aws:iam::123456789012:role/principal1" = ["prefix1/", "prefix2/"]
+ "arn:aws:iam::123456789012:role/principal2" = [""]
+ }
+ privileged_principal_actions = [
+ "s3:PutObject",
+ "s3:PutObjectAcl",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ "s3:ListBucket",
+ "s3:ListBucketMultipartUploads",
+ "s3:GetBucketLocation",
+ "s3:AbortMultipartUpload"
+ ]
+ }
+ ```
+
include:
- "docs/targets.md"
- "docs/terraform.md"
@@ -147,3 +180,5 @@ contributors:
github: "maximmi"
- name: "Josh Myers"
github: "joshmyers"
+ - name: "Yonatan Koren"
+ github: "korenyoni"
diff --git a/docs/terraform.md b/docs/terraform.md
index d38c3c49..12f25ddd 100644
--- a/docs/terraform.md
+++ b/docs/terraform.md
@@ -68,6 +68,8 @@
| [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no |
| [object\_lock\_configuration](#input\_object\_lock\_configuration) | A configuration for S3 object locking. With S3 Object Lock, you can store objects using a `write once, read many` (WORM) model. Object Lock can help prevent objects from being deleted or overwritten for a fixed amount of time or indefinitely. | object({
mode = string # Valid values are GOVERNANCE and COMPLIANCE.
days = number
years = number
})
| `null` | no |
| [policy](#input\_policy) | A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy | `string` | `""` | no |
+| [privileged\_principal\_actions](#input\_privileged\_principal\_actions) | List of actions to permit `privileged_principal_arns` to perform on bucket and bucket prefixes (see `privileged_principal_arns`) | `list(string)` | `[]` | no |
+| [privileged\_principal\_arns](#input\_privileged\_principal\_arns) | (Optional) Map of IAM Principal ARNs to lists of S3 path prefixes to grant `privileged_principal_actions` permissions.
Resource list will include the bucket itself along with all the prefixes. Prefixes should not begin with '/'. | `map(list(string))` | `{}` | no |
| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no |
| [replication\_rules](#input\_replication\_rules) | DEPRECATED: Use s3\_replication\_rules instead. | `list(any)` | `null` | no |
| [restrict\_public\_buckets](#input\_restrict\_public\_buckets) | Set to `false` to disable the restricting of making the bucket public | `bool` | `true` | no |
diff --git a/examples/complete/fixtures.us-east-2.tfvars b/examples/complete/fixtures.us-east-2.tfvars
index d9022d6b..39768da2 100644
--- a/examples/complete/fixtures.us-east-2.tfvars
+++ b/examples/complete/fixtures.us-east-2.tfvars
@@ -12,8 +12,19 @@ acl = "private"
force_destroy = true
+user_enabled = true
+
versioning_enabled = false
allow_encrypted_uploads_only = true
-allowed_bucket_actions = ["s3:PutObject", "s3:PutObjectAcl", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:GetBucketLocation", "s3:AbortMultipartUpload"]
+allowed_bucket_actions = [
+ "s3:PutObject",
+ "s3:PutObjectAcl",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ "s3:ListBucket",
+ "s3:ListBucketMultipartUploads",
+ "s3:GetBucketLocation",
+ "s3:AbortMultipartUpload"
+]
diff --git a/examples/complete/grants.us-east-2.tfvars b/examples/complete/grants.us-east-2.tfvars
index 3df51ded..ae61c275 100644
--- a/examples/complete/grants.us-east-2.tfvars
+++ b/examples/complete/grants.us-east-2.tfvars
@@ -23,4 +23,13 @@ versioning_enabled = false
allow_encrypted_uploads_only = true
-allowed_bucket_actions = ["s3:PutObject", "s3:PutObjectAcl", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:GetBucketLocation", "s3:AbortMultipartUpload"]
+allowed_bucket_actions = [
+ "s3:PutObject",
+ "s3:PutObjectAcl",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ "s3:ListBucket",
+ "s3:ListBucketMultipartUploads",
+ "s3:GetBucketLocation",
+ "s3:AbortMultipartUpload"
+]
diff --git a/examples/complete/lifecycle.us-east-2.tfvars b/examples/complete/lifecycle.us-east-2.tfvars
index cf8f6434..c1037646 100644
--- a/examples/complete/lifecycle.us-east-2.tfvars
+++ b/examples/complete/lifecycle.us-east-2.tfvars
@@ -57,4 +57,13 @@ versioning_enabled = false
allow_encrypted_uploads_only = true
-allowed_bucket_actions = ["s3:PutObject", "s3:PutObjectAcl", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:GetBucketLocation", "s3:AbortMultipartUpload"]
+allowed_bucket_actions = [
+ "s3:PutObject",
+ "s3:PutObjectAcl",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ "s3:ListBucket",
+ "s3:ListBucketMultipartUploads",
+ "s3:GetBucketLocation",
+ "s3:AbortMultipartUpload"
+]
diff --git a/examples/complete/main.tf b/examples/complete/main.tf
index 8f7b8d80..31ac8d81 100644
--- a/examples/complete/main.tf
+++ b/examples/complete/main.tf
@@ -1,15 +1,3 @@
-locals {
- replication_enabled = length(var.s3_replication_rules) > 0
- extra_rule = local.replication_enabled ? {
- id = "replication-test-explicit-bucket"
- status = "Enabled"
- prefix = "/extra"
- priority = 5
- destination_bucket = module.s3_bucket_replication_target_extra[0].bucket_arn
- } : null
- s3_replication_rules = local.replication_enabled ? concat(var.s3_replication_rules, [local.extra_rule]) : null
-}
-
provider "aws" {
region = var.region
}
@@ -17,7 +5,7 @@ provider "aws" {
module "s3_bucket" {
source = "../../"
- user_enabled = true
+ user_enabled = var.user_enabled
acl = var.acl
force_destroy = var.force_destroy
grants = var.grants
@@ -30,36 +18,8 @@ module "s3_bucket" {
s3_replication_enabled = local.replication_enabled
s3_replica_bucket_arn = join("", module.s3_bucket_replication_target.*.bucket_arn)
s3_replication_rules = local.s3_replication_rules
+ privileged_principal_actions = var.privileged_principal_actions
+ privileged_principal_arns = local.privileged_principal_arns
context = module.this.context
}
-
-module "s3_bucket_replication_target" {
- count = local.replication_enabled ? 1 : 0
-
- source = "../../"
-
- user_enabled = true
- acl = "private"
- force_destroy = true
- versioning_enabled = true
- s3_replication_source_roles = [module.s3_bucket.replication_role_arn]
-
- attributes = ["target"]
- context = module.this.context
-}
-
-module "s3_bucket_replication_target_extra" {
- count = local.replication_enabled ? 1 : 0
-
- source = "../../"
-
- user_enabled = true
- acl = "private"
- force_destroy = true
- versioning_enabled = true
- s3_replication_source_roles = [module.s3_bucket.replication_role_arn]
-
- attributes = ["target", "extra"]
- context = module.this.context
-}
diff --git a/examples/complete/privileged-principals.tf b/examples/complete/privileged-principals.tf
new file mode 100644
index 00000000..7a9d5905
--- /dev/null
+++ b/examples/complete/privileged-principals.tf
@@ -0,0 +1,88 @@
+locals {
+ account_id = data.aws_caller_identity.current.account_id
+ principal_names = [
+ "arn:aws:iam::${local.account_id}:role/${join("", module.deployment_principal_label.*.id)}",
+ "arn:aws:iam::${local.account_id}:role/${join("", module.additional_deployment_principal_label.*.id)}"
+ ]
+ privileged_principal_arns = var.privileged_principal_enabled ? {
+ (local.principal_names[0]) = [""]
+ (local.principal_names[1]) = ["prefix1/", "prefix2/"]
+ } : {}
+}
+
+data "aws_caller_identity" "current" {}
+
+data "aws_iam_policy_document" "deployment_assume_role" {
+ count = var.privileged_principal_enabled ? 1 : 0
+
+ statement {
+ actions = ["sts:AssumeRole"]
+ effect = "Allow"
+ principals {
+ identifiers = ["ec2.amazonaws.com"] # example: this role can be used in an IAM Instance Profile
+ type = "Service"
+ }
+ }
+}
+
+data "aws_iam_policy_document" "deployment_iam_policy" {
+ count = var.privileged_principal_enabled ? 1 : 0
+
+ statement {
+ actions = var.privileged_principal_actions
+ effect = "Allow"
+ resources = ["arn:aws:s3:::${module.this.id}*"]
+ }
+}
+
+resource "aws_iam_policy" "deployment_iam_policy" {
+ count = var.privileged_principal_enabled ? 1 : 0
+
+ policy = join("", data.aws_iam_policy_document.deployment_iam_policy.*.json)
+}
+
+module "deployment_principal_label" {
+ source = "cloudposse/label/null"
+ version = "0.24.1"
+ enabled = var.privileged_principal_enabled
+
+ attributes = ["deployment"]
+
+ context = module.this.context
+}
+
+resource "aws_iam_role" "deployment_iam_role" {
+ count = var.privileged_principal_enabled ? 1 : 0
+
+ name = join("", module.deployment_principal_label.*.id)
+ assume_role_policy = join("", data.aws_iam_policy_document.deployment_assume_role.*.json)
+
+ tags = module.deployment_principal_label.tags
+}
+
+
+module "additional_deployment_principal_label" {
+ source = "cloudposse/label/null"
+ version = "0.24.1"
+ enabled = var.privileged_principal_enabled
+
+ attributes = ["deployment", "additional"]
+
+ context = module.this.context
+}
+
+resource "aws_iam_role" "additional_deployment_iam_role" {
+ count = var.privileged_principal_enabled ? 1 : 0
+
+ name = join("", module.additional_deployment_principal_label.*.id)
+ assume_role_policy = join("", data.aws_iam_policy_document.deployment_assume_role.*.json)
+
+ tags = module.additional_deployment_principal_label.tags
+}
+
+resource "aws_iam_role_policy_attachment" "additional_deployment_role_attachment" {
+ count = var.privileged_principal_enabled ? 1 : 0
+
+ policy_arn = join("", aws_iam_policy.deployment_iam_policy.*.arn)
+ role = join("", aws_iam_role.deployment_iam_role.*.name)
+}
diff --git a/examples/complete/privileged-principals.us-east-2.tfvars b/examples/complete/privileged-principals.us-east-2.tfvars
new file mode 100644
index 00000000..e2421ee0
--- /dev/null
+++ b/examples/complete/privileged-principals.us-east-2.tfvars
@@ -0,0 +1,32 @@
+enabled = true
+
+region = "us-east-2"
+
+namespace = "eg"
+
+stage = "test"
+
+name = "s3-principals-test" # s3-privileged-principals-test will hit the name length limit
+
+acl = "private"
+
+force_destroy = true
+
+allow_encrypted_uploads_only = true
+
+privileged_principal_actions = [
+ "s3:PutObject",
+ "s3:PutObjectAcl",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ "s3:ListBucket",
+ "s3:ListBucketMultipartUploads",
+ "s3:GetBucketLocation",
+ "s3:AbortMultipartUpload"
+]
+
+privileged_principal_enabled = true
+
+versioning_enabled = false
+
+user_enabled = false
diff --git a/examples/complete/replication.tf b/examples/complete/replication.tf
new file mode 100644
index 00000000..3fe2037b
--- /dev/null
+++ b/examples/complete/replication.tf
@@ -0,0 +1,41 @@
+locals {
+ replication_enabled = length(var.s3_replication_rules) > 0
+ extra_rule = local.replication_enabled ? {
+ id = "replication-test-explicit-bucket"
+ status = "Enabled"
+ prefix = "/extra"
+ priority = 5
+ destination_bucket = module.s3_bucket_replication_target_extra[0].bucket_arn
+ } : null
+ s3_replication_rules = local.replication_enabled ? concat(var.s3_replication_rules, [local.extra_rule]) : null
+}
+
+module "s3_bucket_replication_target" {
+ count = local.replication_enabled ? 1 : 0
+
+ source = "../../"
+
+ user_enabled = true
+ acl = "private"
+ force_destroy = true
+ versioning_enabled = true
+ s3_replication_source_roles = [module.s3_bucket.replication_role_arn]
+
+ attributes = ["target"]
+ context = module.this.context
+}
+
+module "s3_bucket_replication_target_extra" {
+ count = local.replication_enabled ? 1 : 0
+
+ source = "../../"
+
+ user_enabled = true
+ acl = "private"
+ force_destroy = true
+ versioning_enabled = true
+ s3_replication_source_roles = [module.s3_bucket.replication_role_arn]
+
+ attributes = ["target", "extra"]
+ context = module.this.context
+}
diff --git a/examples/complete/variables.tf b/examples/complete/variables.tf
index b5fc93c0..74ceb5a9 100644
--- a/examples/complete/variables.tf
+++ b/examples/complete/variables.tf
@@ -237,4 +237,16 @@ variable "object_lock_configuration" {
})
default = null
description = "A configuration for S3 object locking. With S3 Object Lock, you can store objects using a `write once, read many` (WORM) model. Object Lock can help prevent objects from being deleted or overwritten for a fixed amount of time or indefinitely."
-}
\ No newline at end of file
+}
+
+variable "privileged_principal_enabled" {
+ type = bool
+ default = false
+ description = "Whether or not to allow Privileged Principals to perform actions on the bucket"
+}
+
+variable "privileged_principal_actions" {
+ type = list(string)
+ default = []
+ description = "List of actions to permit `privileged_principal_arns` to perform on bucket and bucket prefixes (see `privileged_principal_arns`)"
+}
diff --git a/main.tf b/main.tf
index 7155bda1..ab007152 100644
--- a/main.tf
+++ b/main.tf
@@ -322,6 +322,7 @@ data "aws_iam_policy_document" "bucket_policy" {
}
}
}
+
dynamic "statement" {
for_each = length(var.s3_replication_source_roles) > 0 ? [1] : []
@@ -335,6 +336,23 @@ data "aws_iam_policy_document" "bucket_policy" {
}
}
}
+
+ dynamic "statement" {
+ for_each = keys(var.privileged_principal_arns)
+
+ content {
+ sid = "AllowPrivilegedPrincipal[${statement.key}]" # add indices to Sid
+ actions = var.privileged_principal_actions
+ resources = distinct(flatten([
+ "arn:${data.aws_partition.current.partition}:s3:::${join("", aws_s3_bucket.default.*.id)}",
+ formatlist("arn:${data.aws_partition.current.partition}:s3:::${join("", aws_s3_bucket.default.*.id)}/%s*", var.privileged_principal_arns[statement.value]),
+ ]))
+ principals {
+ type = "AWS"
+ identifiers = [statement.value]
+ }
+ }
+ }
}
data "aws_iam_policy_document" "aggregated_policy" {
diff --git a/outputs.tf b/outputs.tf
index 31681bb7..b3e9c62c 100644
--- a/outputs.tf
+++ b/outputs.tf
@@ -1,30 +1,30 @@
output "bucket_domain_name" {
- value = module.this.enabled ? join("", aws_s3_bucket.default.*.bucket_domain_name) : ""
+ value = local.enabled ? join("", aws_s3_bucket.default.*.bucket_domain_name) : ""
description = "FQDN of bucket"
}
output "bucket_regional_domain_name" {
- value = module.this.enabled ? join("", aws_s3_bucket.default.*.bucket_regional_domain_name) : ""
+ value = local.enabled ? join("", aws_s3_bucket.default.*.bucket_regional_domain_name) : ""
description = "The bucket region-specific domain name"
}
output "bucket_id" {
- value = module.this.enabled ? join("", aws_s3_bucket.default.*.id) : ""
+ value = local.enabled ? join("", aws_s3_bucket.default.*.id) : ""
description = "Bucket Name (aka ID)"
}
output "bucket_arn" {
- value = module.this.enabled ? join("", aws_s3_bucket.default.*.arn) : ""
+ value = local.enabled ? join("", aws_s3_bucket.default.*.arn) : ""
description = "Bucket ARN"
}
output "bucket_region" {
- value = module.this.enabled ? join("", aws_s3_bucket.default.*.region) : ""
+ value = local.enabled ? join("", aws_s3_bucket.default.*.region) : ""
description = "Bucket region"
}
output "enabled" {
- value = module.this.enabled
+ value = local.enabled
description = "Is module enabled"
}
@@ -49,7 +49,7 @@ output "user_unique_id" {
}
output "replication_role_arn" {
- value = module.this.enabled && local.replication_enabled ? join("", aws_iam_role.replication.*.arn) : ""
+ value = local.enabled && local.replication_enabled ? join("", aws_iam_role.replication.*.arn) : ""
description = "The ARN of the replication IAM Role"
}
diff --git a/test/src/examples_complete_test.go b/test/src/examples_complete_test.go
index 4450a978..99fb3768 100644
--- a/test/src/examples_complete_test.go
+++ b/test/src/examples_complete_test.go
@@ -1,11 +1,15 @@
package test
import (
+ "bytes"
+ "encoding/json"
"math/rand"
"strconv"
+ "strings"
"testing"
"time"
+ "github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/terraform"
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
"github.com/stretchr/testify/assert"
@@ -58,7 +62,7 @@ func TestExamplesComplete(t *testing.T) {
// Test the Terraform module in examples/complete using Terratest for grants.
func TestExamplesCompleteWithGrants(t *testing.T) {
t.Parallel()
- rand.Seed(time.Now().UnixNano()+1)
+ rand.Seed(time.Now().UnixNano() + 1)
attributes := []string{strconv.Itoa(rand.Intn(100000))}
rootFolder := "../../"
@@ -84,13 +88,6 @@ func TestExamplesCompleteWithGrants(t *testing.T) {
// This will run `terraform init` and `terraform apply` and fail the test if there are any errors
terraform.InitAndApply(t, terraformOptions)
- // Run `terraform output` to get the value of an output variable
- userName := terraform.Output(t, terraformOptions, "user_name")
-
- expectedUserName := "eg-test-s3-grants-test-" + attributes[0]
- // Verify we're getting back the outputs we expect
- assert.Equal(t, expectedUserName, userName)
-
// Run `terraform output` to get the value of an output variable
s3BucketId := terraform.Output(t, terraformOptions, "bucket_id")
@@ -102,7 +99,7 @@ func TestExamplesCompleteWithGrants(t *testing.T) {
// Test the Terraform module in examples/complete using Terratest for grants.
func TestExamplesCompleteWithObjectLock(t *testing.T) {
t.Parallel()
- rand.Seed(time.Now().UnixNano()+2)
+ rand.Seed(time.Now().UnixNano() + 2)
attributes := []string{strconv.Itoa(rand.Intn(100000))}
rootFolder := "../../"
@@ -128,14 +125,6 @@ func TestExamplesCompleteWithObjectLock(t *testing.T) {
// This will run `terraform init` and `terraform apply` and fail the test if there are any errors
terraform.InitAndApply(t, terraformOptions)
- // Run `terraform output` to get the value of an output variable
- userName := terraform.Output(t, terraformOptions, "user_name")
-
- expectedUserName := "eg-test-s3-object-lock-test-" + attributes[0]
-
- // Verify we're getting back the outputs we expect
- assert.Equal(t, expectedUserName, userName)
-
// Run `terraform output` to get the value of an output variable
s3BucketId := terraform.Output(t, terraformOptions, "bucket_id")
expectedS3BucketId := "eg-test-s3-object-lock-test-" + attributes[0]
@@ -145,7 +134,7 @@ func TestExamplesCompleteWithObjectLock(t *testing.T) {
func TestExamplesCompleteWithLifecycleRules(t *testing.T) {
t.Parallel()
- rand.Seed(time.Now().UnixNano()+3)
+ rand.Seed(time.Now().UnixNano() + 3)
attributes := []string{strconv.Itoa(rand.Intn(100000))}
rootFolder := "../../"
@@ -171,13 +160,6 @@ func TestExamplesCompleteWithLifecycleRules(t *testing.T) {
// This will run `terraform init` and `terraform apply` and fail the test if there are any errors
terraform.InitAndApply(t, terraformOptions)
- // Run `terraform output` to get the value of an output variable
- userName := terraform.Output(t, terraformOptions, "user_name")
-
- expectedUserName := "eg-test-s3-lifecycle-test-" + attributes[0]
- // Verify we're getting back the outputs we expect
- assert.Equal(t, expectedUserName, userName)
-
// Run `terraform output` to get the value of an output variable
s3BucketId := terraform.Output(t, terraformOptions, "bucket_id")
@@ -186,10 +168,9 @@ func TestExamplesCompleteWithLifecycleRules(t *testing.T) {
assert.Equal(t, expectedS3BucketId, s3BucketId)
}
-
func TestExamplesCompleteWithReplication(t *testing.T) {
t.Parallel()
- rand.Seed(time.Now().UnixNano()+4)
+ rand.Seed(time.Now().UnixNano() + 4)
attributes := []string{strconv.Itoa(rand.Intn(100000))}
rootFolder := "../../"
@@ -206,7 +187,7 @@ func TestExamplesCompleteWithReplication(t *testing.T) {
VarFiles: varFiles,
Vars: map[string]interface{}{
"attributes": attributes,
- "enabled": "true",
+ "enabled": "true",
},
}
@@ -216,13 +197,6 @@ func TestExamplesCompleteWithReplication(t *testing.T) {
// This will run `terraform init` and `terraform apply` and fail the test if there are any errors
terraform.InitAndApply(t, terraformOptions)
- // Run `terraform output` to get the value of an output variable
- userName := terraform.Output(t, terraformOptions, "user_name")
-
- expectedUserName := "eg-test-s3-replication-test-" + attributes[0]
- // Verify we're getting back the outputs we expect
- assert.Equal(t, expectedUserName, userName)
-
// Run `terraform output` to get the value of an output variable
s3BucketId := terraform.Output(t, terraformOptions, "bucket_id")
@@ -245,9 +219,117 @@ func TestExamplesCompleteWithReplication(t *testing.T) {
}
+func TestExamplesCompleteWithPrivilegedPrincipals(t *testing.T) {
+ t.Parallel()
+ rand.Seed(time.Now().UnixNano() + 5)
+
+ awsRegion := "us-east-2"
+ attributes := []string{strconv.Itoa(rand.Intn(100000))}
+ rootFolder := "../../"
+ terraformFolderRelativeToRoot := "examples/complete"
+ varFiles := []string{"privileged-principals.us-east-2.tfvars"}
+
+ tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot)
+
+ terraformOptions := &terraform.Options{
+ // The path to where our Terraform code is located
+ TerraformDir: tempTestFolder,
+ Upgrade: true,
+ // Variables to pass to our Terraform code using -var-file options
+ VarFiles: varFiles,
+ Vars: map[string]interface{}{
+ "attributes": attributes,
+ "enabled": "true",
+ },
+ }
+
+ // At the end of the test, run `terraform destroy` to clean up any resources that were created
+ defer terraform.Destroy(t, terraformOptions)
+
+ // This will run `terraform init` and `terraform apply` and fail the test if there are any errors
+ terraform.InitAndApply(t, terraformOptions)
+
+ // Run `terraform output` to get the value of an output variable
+ s3BucketId := terraform.Output(t, terraformOptions, "bucket_id")
+
+ expectedS3BucketId := "eg-test-s3-principals-test-" + attributes[0]
+ // Verify we're getting back the outputs we expect
+ assert.Equal(t, expectedS3BucketId, s3BucketId)
+
+ // Run `terraform output` to get the value of an output variable
+ bucketID := terraform.Output(t, terraformOptions, "bucket_id")
+
+ // Verify that our Bucket has a policy attached
+ aws.AssertS3BucketPolicyExists(t, awsRegion, bucketID)
+
+ // Verify that our bucket's bucket policy contains the expected statements allowing actions made by privileged principals
+ bucketPolicy := aws.GetS3BucketPolicy(t, awsRegion, bucketID)
+ expectedBucketPolicyStatementsTemplate := `
+ [{
+ "Sid": "AllowPrivilegedPrincipal[0]",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam::AWS_ACCOUNT_ID:role/eg-test-s3-principals-test-RANDOM_ID-deployment"
+ },
+ "Action": [
+ "s3:PutObjectAcl",
+ "s3:PutObject",
+ "s3:ListBucketMultipartUploads",
+ "s3:ListBucket",
+ "s3:GetObject",
+ "s3:GetBucketLocation",
+ "s3:DeleteObject",
+ "s3:AbortMultipartUpload"
+ ],
+ "Resource": [
+ "arn:aws:s3:::eg-test-s3-principals-test-RANDOM_ID/*",
+ "arn:aws:s3:::eg-test-s3-principals-test-RANDOM_ID"
+ ]
+ },
+ {
+ "Sid": "AllowPrivilegedPrincipal[1]",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam::AWS_ACCOUNT_ID:role/eg-test-s3-principals-test-RANDOM_ID-deployment-additional"
+ },
+ "Action": [
+ "s3:PutObjectAcl",
+ "s3:PutObject",
+ "s3:ListBucketMultipartUploads",
+ "s3:ListBucket",
+ "s3:GetObject",
+ "s3:GetBucketLocation",
+ "s3:DeleteObject",
+ "s3:AbortMultipartUpload"
+ ],
+ "Resource": [
+ "arn:aws:s3:::eg-test-s3-principals-test-RANDOM_ID/prefix2/*",
+ "arn:aws:s3:::eg-test-s3-principals-test-RANDOM_ID/prefix1/*",
+ "arn:aws:s3:::eg-test-s3-principals-test-RANDOM_ID"
+ ]
+ }]
+ `
+ expectedBucketPolicyStatements := strings.ReplaceAll(
+ strings.ReplaceAll(
+ expectedBucketPolicyStatementsTemplate,
+ "AWS_ACCOUNT_ID",
+ aws.GetAccountId(t),
+ ),
+ "RANDOM_ID",
+ attributes[0],
+ )
+ expectedBucketPolicyStatementsJSON := new(bytes.Buffer)
+ err := json.Compact(expectedBucketPolicyStatementsJSON, []byte(expectedBucketPolicyStatements))
+ if err != nil {
+ t.Errorf("Unexpected error when compacting JSON: %v.", err)
+ }
+ expectedBucketPolicySnippet := strings.Trim(expectedBucketPolicyStatementsJSON.String(), "[]")
+ assert.Contains(t, bucketPolicy, expectedBucketPolicySnippet)
+}
+
func TestExamplesCompleteDisabled(t *testing.T) {
t.Parallel()
- rand.Seed(time.Now().UnixNano()+5)
+ rand.Seed(time.Now().UnixNano() + 6)
attributes := []string{strconv.Itoa(rand.Intn(100000))}
rootFolder := "../../"
@@ -264,7 +346,7 @@ func TestExamplesCompleteDisabled(t *testing.T) {
VarFiles: varFiles,
Vars: map[string]interface{}{
"attributes": attributes,
- "enabled": "false",
+ "enabled": "false",
},
}
diff --git a/variables.tf b/variables.tf
index 9a1a3366..3692d76a 100644
--- a/variables.tf
+++ b/variables.tf
@@ -257,3 +257,18 @@ variable "website_inputs" {
description = "Specifies the static website hosting configuration object."
}
+
+variable "privileged_principal_arns" {
+ type = map(list(string))
+ default = {}
+ description = <<-EOT
+ (Optional) Map of IAM Principal ARNs to lists of S3 path prefixes to grant `privileged_principal_actions` permissions.
+ Resource list will include the bucket itself along with all the prefixes. Prefixes should not begin with '/'.
+ EOT
+}
+
+variable "privileged_principal_actions" {
+ type = list(string)
+ default = []
+ description = "List of actions to permit `privileged_principal_arns` to perform on bucket and bucket prefixes (see `privileged_principal_arns`)"
+}