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"