diff --git a/Makefile b/Makefile index a8c0538..e7ecefd 100644 --- a/Makefile +++ b/Makefile @@ -9,4 +9,4 @@ codeartifact-repo-format: init cd ./codeartifact-repo && ../scripts/format.sh codeartifact-repo-verify: init - cd ./codeartifact-repo && terraform-docs -c ../.terraform-docs.yaml . && ../scripts/verify.sh \ No newline at end of file + cd ./codeartifact-repo && ../scripts/verify.sh \ No newline at end of file diff --git a/codeartifact-repo/README.md b/codeartifact-repo/README.md index ae0918a..a184d32 100644 --- a/codeartifact-repo/README.md +++ b/codeartifact-repo/README.md @@ -45,7 +45,7 @@ This module is intended to configure AWS CodeArtifact domains and repositories. | [domain\_policy\_document\_path](#input\_domain\_policy\_document\_path) | Path to IAM policy document applied to Codeartifact domain | `string` | `null` | no | | [encryption\_key\_arn](#input\_encryption\_key\_arn) | ARN of KMS key used for repository encryption. If not specified, and use\_default\_ecnryption\_key is false, creates new KMS key | `string` | `null` | no | | [repo\_region](#input\_repo\_region) | Region in which repository will be managed. If not specified, defaults to region configured for provider | `string` | `null` | no | -| [repositories](#input\_repositories) | List of repositories within Codeartifact domain |
list(object({
repository_name = string
description = optional(string, "")
region = optional(string, null)
domain_owner = optional(string, null)
upstream = optional(string, null)
external_connection = optional(string, null)
policy_document_path = optional(string, null)
}))
| `[]` | no | +| [repositories](#input\_repositories) | List of repositories within Codeartifact domain |
list(object({
repository_name = string
description = optional(string, "")
region = optional(string, null)
domain_owner = optional(string, null)
upstream = optional(string, null)
external_connection = optional(string, null)
policy_document_path = optional(string, null)
default_read_access_principals = optional(list(string), null)
default_write_access_principals = optional(list(string), null)
}))
| `[]` | no | | [tags](#input\_tags) | Tags to be applied to resources | `map(string)` | `{}` | no | | [use\_default\_ecnryption\_key](#input\_use\_default\_ecnryption\_key) | Whether to use default Codeartifact KMS key (defaults to true) | `bool` | `true` | no | @@ -54,8 +54,10 @@ This module is intended to configure AWS CodeArtifact domains and repositories. | Name | Description | |------|-------------| | [created\_repositories](#output\_created\_repositories) | A list of names of the created repositories. | +| [default\_sts\_policies](#output\_default\_sts\_policies) | Created STS policies | | [domain](#output\_domain) | Name of the CodeArtifact domain | | [domain\_owner](#output\_domain\_owner) | Owner account of the CodeArtifact domain | +| [policy\_documents](#output\_policy\_documents) | A map of repository names to their applied policy documents (if any). | ## Examples \ No newline at end of file diff --git a/codeartifact-repo/main.tf b/codeartifact-repo/main.tf index e788ffd..ec9081f 100644 --- a/codeartifact-repo/main.tf +++ b/codeartifact-repo/main.tf @@ -5,6 +5,25 @@ locals { should_create_kms_key = (!var.use_default_ecnryption_key && var.encryption_key_arn == null) ? true : false + + repo_read_access_principals = { + for repo in var.repositories : repo.repository_name => repo.default_read_access_principals if(repo.default_read_access_principals != null + && length(repo.default_read_access_principals) > 0) + } + repo_write_access_principals = { + for repo in var.repositories : repo.repository_name => repo.default_write_access_principals if(repo.default_write_access_principals != null + && length(repo.default_write_access_principals) > 0) + } + repo_final_policy_documents = data.aws_iam_policy_document.combined_default_policies + # repos_with_policy_files = [ + # for repo in var.repositories : repo.repository_name if repo.policy_document_path != null + # ] + all_sts_principals = { + for repo in var.repositories : repo.repository_name => distinct(concat( + repo.default_read_access_principals != null ? repo.default_read_access_principals : [], + repo.default_write_access_principals != null ? repo.default_write_access_principals : [] + )) + } } resource "aws_codeartifact_domain" "repo_domain" { @@ -47,11 +66,89 @@ resource "aws_codeartifact_repository" "repository" { tags = var.tags } +data "aws_iam_policy_document" "default_readonly_repo_policy" { + for_each = { for k, v in local.repo_read_access_principals : k => v } + statement { + effect = "Allow" + actions = [ + "codeartifact:DescribePackageVersion", + "codeartifact:DescribeRepository", + "codeartifact:GetPackageVersionReadme", + "codeartifact:GetRepositoryEndpoint", + "codeartifact:ListPackageVersionAssets", + "codeartifact:ListPackageVersionDependencies", + "codeartifact:ListPackageVersions", + "codeartifact:ListPackages", + "codeartifact:ReadFromRepository", + ] + resources = [aws_codeartifact_repository.repository[each.key].arn] + principals { + type = "AWS" + identifiers = each.value + } + } +} + +data "aws_iam_policy_document" "default_write_access_repo_policy" { + for_each = { for k, v in local.repo_write_access_principals : k => v } + statement { + effect = "Allow" + actions = [ + "codeartifact:DescribePackageVersion", + "codeartifact:DescribeRepository", + "codeartifact:GetPackageVersionReadme", + "codeartifact:GetRepositoryEndpoint", + "codeartifact:ListPackageVersionAssets", + "codeartifact:ListPackageVersionDependencies", + "codeartifact:ListPackageVersions", + "codeartifact:ListPackages", + "codeartifact:PublishPackageVersion", + "codeartifact:PutPackageMetadata", + "codeartifact:ReadFromRepository", + ] + resources = [aws_codeartifact_repository.repository[each.key].arn] + principals { + type = "AWS" + identifiers = each.value + } + } +} + +data "aws_iam_policy_document" "default_sts_policy" { + for_each = local.all_sts_principals + statement { + effect = "Allow" + actions = ["sts:GetServiceBearerToken"] + resources = [aws_codeartifact_repository.repository[each.key].arn] + principals { + type = "AWS" + identifiers = each.value + } + condition { + variable = "sts:AWSServiceName" + values = ["codeartifact.amazonaws.com"] + test = "StringEquals" + } + } +} + +data "aws_iam_policy_document" "combined_default_policies" { + for_each = { for repo in var.repositories : repo.repository_name => repo if contains(keys(local.repo_read_access_principals), repo.repository_name) || contains(keys(local.repo_write_access_principals), repo.repository_name) } + + source_policy_documents = compact( + [ + try(data.aws_iam_policy_document.default_readonly_repo_policy[each.key].json, null), + try(data.aws_iam_policy_document.default_write_access_repo_policy[each.key].json, null), + try(data.aws_iam_policy_document.default_sts_policy[each.key].json, null) + ] + ) +} + resource "aws_codeartifact_repository_permissions_policy" "repo_permissions_policy" { - for_each = { for repo in var.repositories : repo.repository_name => repo if repo.policy_document_path != null } + for_each = { for repo in var.repositories : repo.repository_name => repo if repo.policy_document_path != null || contains(keys(local.repo_final_policy_documents), repo.repository_name) } repository = aws_codeartifact_repository.repository[each.key].repository domain = aws_codeartifact_domain.repo_domain.domain - policy_document = file(each.value.policy_document_path) + policy_document = each.value.policy_document_path != null ? file(each.value.policy_document_path) : local.repo_final_policy_documents[each.key].json region = var.repo_region != null ? var.repo_region : null domain_owner = each.value.domain_owner != null ? each.value.domain_owner : null } diff --git a/codeartifact-repo/outputs.tf b/codeartifact-repo/outputs.tf index 3d90ab3..6c55eaf 100644 --- a/codeartifact-repo/outputs.tf +++ b/codeartifact-repo/outputs.tf @@ -15,3 +15,13 @@ output "created_repositories" { description = "A list of names of the created repositories." value = tolist(keys(aws_codeartifact_repository.repository)) } + +output "policy_documents" { + description = "A map of repository names to their applied policy documents (if any)." + value = { for repo_name, repo_policy in aws_codeartifact_repository_permissions_policy.repo_permissions_policy : repo_name => repo_policy.policy_document } +} + +output "default_sts_policies" { + description = "Created STS policies" + value = { for repo_name, sts_policy in data.aws_iam_policy_document.default_sts_policy : repo_name => sts_policy.json } +} \ No newline at end of file diff --git a/codeartifact-repo/tests/policies.tftest.hcl b/codeartifact-repo/tests/policies.tftest.hcl new file mode 100644 index 0000000..f12d8dc --- /dev/null +++ b/codeartifact-repo/tests/policies.tftest.hcl @@ -0,0 +1,137 @@ +// Copyright 2025 Bitshift +// SPDX-License-Identifier: MPL-2.0 + +mock_provider "aws" { + override_data { + target = data.aws_iam_policy_document.default_readonly_repo_policy + values = { + json = "{}" + } + } + override_data { + target = data.aws_iam_policy_document.default_write_access_repo_policy + values = { + json = "{}" + } + } + override_data { + target = data.aws_iam_policy_document.combined_default_policies + values = { + json = "{}" + } + } +} + +run "default_policy_should_be_created_when_principals_specified" { + command = plan + + variables { + domain_name = "test-domain" + repositories = [ + { + repository_name = "repo-with-policies" + default_read_access_principals = ["arn:aws:iam::123456789012:role/ReadRole"] + default_write_access_principals = ["arn:aws:iam::123456789012:role/WriteRole"] + } + ] + } + + assert { + condition = length(data.aws_iam_policy_document.default_readonly_repo_policy) == 1 + error_message = "Default read-only policy document was not created." + } + + assert { + condition = length(data.aws_iam_policy_document.default_write_access_repo_policy) == 1 + error_message = "Default write-access policy document was not created." + } + + assert { + condition = length(data.aws_iam_policy_document.default_sts_policy) == 1 + error_message = "Default STS policy document was not created." + } +} + +run "no_default_policy_when_no_principals" { + command = plan + + variables { + domain_name = "test-domain" + repositories = [ + { + repository_name = "repo-without-policies" + } + ] + } + + assert { + condition = length(data.aws_iam_policy_document.default_readonly_repo_policy) == 0 + error_message = "Default read-only policy document was created despite no principals being specified." + } + + assert { + condition = length(data.aws_iam_policy_document.default_write_access_repo_policy) == 0 + error_message = "Default write-access policy document was created despite no principals being specified." + } +} + +run "combined_policy_created_when_either_principal_specified" { + command = plan + + variables { + domain_name = "test-domain" + repositories = [ + { + repository_name = "repo-with-read-policy" + default_read_access_principals = ["arn:aws:iam::123456789012:role/ReadRole"] + }, + { + repository_name = "repo-with-write-policy" + default_write_access_principals = ["arn:aws:iam::123456789012:role/WriteRole"] + } + ] + } + + assert { + condition = length(data.aws_iam_policy_document.combined_default_policies) == 2 + error_message = "Combined default policies were not created correctly when either read or write principals were specified." + } +} + +run "no_combined_policy_when_no_principals" { + command = plan + + variables { + domain_name = "test-domain" + repositories = [ + { + repository_name = "repo-without-policies" + } + ] + } + + assert { + condition = length(data.aws_iam_policy_document.combined_default_policies) == 0 + error_message = "Combined default policy was created despite no principals being specified." + } +} + +run "policy_file_should_override_default_policy" { + command = plan + + variables { + domain_name = "test-domain" + repositories = [ + { + repository_name = "repo-with-policy-file" + policy_document_path = "tests/test-repo-policy.json" + default_read_access_principals = ["arn:aws:iam::123456789012:role/ReadRole"] + } + ] + } + + assert { + condition = length(aws_codeartifact_repository_permissions_policy.repo_permissions_policy) == 1 + error_message = "Repository permissions policy was not created when a policy document path was provided." + } +} \ No newline at end of file diff --git a/codeartifact-repo/variables.tf b/codeartifact-repo/variables.tf index 4a7b6a3..c5a8b1d 100644 --- a/codeartifact-repo/variables.tf +++ b/codeartifact-repo/variables.tf @@ -50,13 +50,15 @@ variable "domain_permissions_policy_revision" { variable "repositories" { type = list(object({ - repository_name = string - description = optional(string, "") - region = optional(string, null) - domain_owner = optional(string, null) - upstream = optional(string, null) - external_connection = optional(string, null) - policy_document_path = optional(string, null) + repository_name = string + description = optional(string, "") + region = optional(string, null) + domain_owner = optional(string, null) + upstream = optional(string, null) + external_connection = optional(string, null) + policy_document_path = optional(string, null) + default_read_access_principals = optional(list(string), null) + default_write_access_principals = optional(list(string), null) })) description = "List of repositories within Codeartifact domain" default = [] diff --git a/scripts/format.sh b/scripts/format.sh index 04d721f..fea120e 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -5,6 +5,7 @@ GOPATH=$(go env GOPATH) echo "Formatting files..." echo "Adding license headers..." +echo "dir: $(pwd)" $GOPATH/bin/addlicense -c 'Bitshift' -y 2025 -l mpl -s=only ./*.tf || exit 1 cd tests && $GOPATH/bin/addlicense -check -c 'Bitshift' -y 2025 -l mpl -s=only ./*.tftest.hcl || exit 1 cd .. @@ -14,3 +15,7 @@ echo "License headers added successfully" echo "Running Terraform format..." terraform fmt || exit 1 echo "Terraform formatting successfull" + +echo "Generating terraform docs..." +terraform-docs -c ../.terraform-docs.yaml . || exit 1 +echo "Terraform docs generated successfully"