Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
cd ./codeartifact-repo && ../scripts/verify.sh
4 changes: 3 additions & 1 deletion codeartifact-repo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ This module is intended to configure AWS CodeArtifact domains and repositories.
| <a name="input_domain_policy_document_path"></a> [domain\_policy\_document\_path](#input\_domain\_policy\_document\_path) | Path to IAM policy document applied to Codeartifact domain | `string` | `null` | no |
| <a name="input_encryption_key_arn"></a> [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 |
| <a name="input_repo_region"></a> [repo\_region](#input\_repo\_region) | Region in which repository will be managed. If not specified, defaults to region configured for provider | `string` | `null` | no |
| <a name="input_repositories"></a> [repositories](#input\_repositories) | List of repositories within Codeartifact domain | <pre>list(object({<br/> repository_name = string<br/> description = optional(string, "")<br/> region = optional(string, null)<br/> domain_owner = optional(string, null)<br/> upstream = optional(string, null)<br/> external_connection = optional(string, null)<br/> policy_document_path = optional(string, null)<br/> }))</pre> | `[]` | no |
| <a name="input_repositories"></a> [repositories](#input\_repositories) | List of repositories within Codeartifact domain | <pre>list(object({<br/> repository_name = string<br/> description = optional(string, "")<br/> region = optional(string, null)<br/> domain_owner = optional(string, null)<br/> upstream = optional(string, null)<br/> external_connection = optional(string, null)<br/> policy_document_path = optional(string, null)<br/> default_read_access_principals = optional(list(string), null)<br/> default_write_access_principals = optional(list(string), null)<br/> }))</pre> | `[]` | no |
| <a name="input_tags"></a> [tags](#input\_tags) | Tags to be applied to resources | `map(string)` | `{}` | no |
| <a name="input_use_default_ecnryption_key"></a> [use\_default\_ecnryption\_key](#input\_use\_default\_ecnryption\_key) | Whether to use default Codeartifact KMS key (defaults to true) | `bool` | `true` | no |

Expand All @@ -54,8 +54,10 @@ This module is intended to configure AWS CodeArtifact domains and repositories.
| Name | Description |
|------|-------------|
| <a name="output_created_repositories"></a> [created\_repositories](#output\_created\_repositories) | A list of names of the created repositories. |
| <a name="output_default_sts_policies"></a> [default\_sts\_policies](#output\_default\_sts\_policies) | Created STS policies |
| <a name="output_domain"></a> [domain](#output\_domain) | Name of the CodeArtifact domain |
| <a name="output_domain_owner"></a> [domain\_owner](#output\_domain\_owner) | Owner account of the CodeArtifact domain |
| <a name="output_policy_documents"></a> [policy\_documents](#output\_policy\_documents) | A map of repository names to their applied policy documents (if any). |

## Examples
<!-- END_TF_DOCS -->
101 changes: 99 additions & 2 deletions codeartifact-repo/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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
}
Expand Down
10 changes: 10 additions & 0 deletions codeartifact-repo/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
137 changes: 137 additions & 0 deletions codeartifact-repo/tests/policies.tftest.hcl
Original file line number Diff line number Diff line change
@@ -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."
}
}
16 changes: 9 additions & 7 deletions codeartifact-repo/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
5 changes: 5 additions & 0 deletions scripts/format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ..
Expand All @@ -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"
Loading