diff --git a/.gitignore b/.gitignore index c8c00f29..b39dd807 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,5 @@ ui/libpeerconnection.log ui/npm-debug.log ui/testem.log tmp/ +.terraform +terraform.tfstate* diff --git a/Makefile b/Makefile index ffb9cc26..6e54b9bd 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ testcompile: fmtcheck generate go test -v -c -tags='$(BUILD_TAGS)' $$pkg -parallel=4 ; \ done -# test runs all tests +# test runs all unit tests test: fmtcheck generate @if [ "$(TEST)" = "./..." ]; then \ echo "ERROR: Set TEST to a specific package"; \ @@ -36,6 +36,10 @@ test: fmtcheck generate fi VAULT_ACC=1 go test -tags='$(BUILD_TAGS)' $(TEST) -v $(TESTARGS) -timeout 45m +# test-acceptance runs all acceptance tests +test-acceptance: + bats $(CURDIR)/tests/acceptance/basic.bats + # generate runs `go generate` to build the dynamically generated # source files. generate: diff --git a/README.md b/README.md index 4a688f5c..177fe2e3 100644 --- a/README.md +++ b/README.md @@ -122,3 +122,15 @@ You can also specify a `TESTARGS` variable to filter tests like so: $ make test TESTARGS='--run=TestConfig' ``` +Acceptance tests requires Azure access, and the following to be installed: +- [Docker](https://docs.docker.com/get-docker/) +- [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) +- [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) +- [bats](https://bats-core.readthedocs.io/en/stable) + +_You will need to be properly logged in to Azure with your subscription set. See +['Azure Provider: Authenticating using the Azure CLI'](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/azure_cli)_ +for more information. +```sh +$ make test-acceptance AZURE_TENANT_ID= +``` diff --git a/tests/acceptance/basic.bats b/tests/acceptance/basic.bats new file mode 100644 index 00000000..f6f2085c --- /dev/null +++ b/tests/acceptance/basic.bats @@ -0,0 +1,233 @@ +#!/usr/bin/env bats + +# based off of the "Vault Ecosystem - Testing Best Practices" Confluence page. + +REPO_ROOT="$(git rev-parse --show-toplevel)" +PLUGIN_NAME="${REPO_ROOT##*/}" +VAULT_IMAGE="${VAULT_IMAGE:-hashicorp/vault:1.9.0-rc1}" +CONTAINER_NAME='' +VAULT_TOKEN='root' + +TESTS_OUT_DIR="$(mktemp -d /tmp/${PLUGIN_NAME}.XXXXXXXXX)" +TESTS_OUT_FILE="${TESTS_OUT_DIR}/output.log" + +PLUGIN_TYPE='' +case ${PLUGIN_NAME} in + *-secrets-*) + PLUGIN_TYPE='secret' + ;; + *-auth-*) + PLUGIN_TYPE='auth' + ;; + *) + echo "could not determine plugin type from ${PLUGIN_NAME}" >&2 + exit 1 + ;; +esac + +if [[ -z "${AZURE_TENANT_ID}" ]]; then + echo "AZURE_TENANT_ID env var not set" >&2 + exit 1 +fi + + +if [[ -n "${WITH_DEV_PLUGIN}" ]]; then + PLUGIN=${REPO_ROOT}/bin/${PLUGIN_NAME} + PLUGIN_SHA256="$(sha256sum ${PLUGIN} | cut -d ' ' -f 1)" +fi + +setup(){ + { # Braces used to redirect all setup logs. + # 1. Configure Vault. + + export CONFIG_DIR="$(mktemp -d ${TESTS_OUT_DIR}/test-XXXXXXX)" + export CONTAINER_NAME="${CONFIG_DIR##*/}" + local PLUGIN_DIR="${CONFIG_DIR}/plugins" + mkdir -vp ${CONFIG_DIR}/{terraform,plugins} + echo "plugin_directory = \"/vault/config/plugins\"" > ${CONFIG_DIR}/vault.hcl + + cp -a ${REPO_ROOT}/tests/acceptance/terraform/*.tf ${CONFIG_DIR}/terraform/. + cat > ${CONFIG_DIR}/terraform/terraform.tfvars <> $TESTS_OUT_FILE +} + +teardown(){ + if [[ -n $SKIP_TEARDOWN ]]; then + echo "Skipping teardown" + return + fi + + { # Braces used to redirect all teardown logs. + + # If the test failed, print some debug output + if [[ "$BATS_ERROR_STATUS" -ne 0 ]]; then + docker logs "${CONTAINER_NAME?}" + fi + + # Teardown Vault configuration. + docker rm --force "${CONTAINER_NAME}" + + printenv | sort + + pushd ${CONFIG_DIR}/terraform + terraform apply -destroy -input=false -auto-approve + popd + + rm -rf "${CONFIG_DIR}" + + } >> $TESTS_OUT_FILE +} + +@test "Azure Secrets Engine - Legacy AAD" { + pushd ${CONFIG_DIR}/terraform + terraform init && terraform apply -input=false -auto-approve -var legacy_aad_resource_access=true + local tf_output=$(terraform output -json | tee ${CONFIG_DIR}/tf-output.json) + popd + + # TODO: remove this sleep, tests periodically fail if the credentials created during infrastructure + # provisioning are not considered valid by Azure. Need to find a way to poll for the creds status. + sleep 10 + + local client_id="$(echo ${tf_output} | jq -er .application_id.value)" + local client_secret="$(echo ${tf_output} | jq -er .application_password_value.value)" + local subscription_id="$(echo ${tf_output} | jq -er .subscription_id.value)" + local resource_group_name="$(echo ${tf_output} | jq -er .resource_group_name.value)" + local tenant_id="$(echo ${tf_output} | jq -er .tenant_id.value)" + + vault secrets enable azure + + vault write azure/config \ + subscription_id=${subscription_id} \ + tenant_id="${tenant_id}" \ + client_id="${client_id}" \ + client_secret="${client_secret}" + + local ttl=10 + vault write azure/roles/my-role ttl="${ttl}" azure_roles=-< /dev/null + do + if [[ "${tries}" -ge 10 ]]; then + echo "vault failed to remove service principal ${sp_id}, ttl=${ttl}" >&2 + exit 1 + fi + ((++tries)) + sleep .5 + done + +} >> $TESTS_OUT_FILE + +@test "Azure Secrets Engine - MS Graph" { + pushd ${CONFIG_DIR}/terraform + terraform init && terraform apply -input=false -auto-approve -var legacy_aad_resource_access=false + local tf_output=$(terraform output -json | tee ${CONFIG_DIR}/tf-output.json) + popd + + # TODO: remove this sleep, tests periodically fail if the credentials created during infrastructure + # provisioning are not considered valid by Azure. Need to find a way to poll for the creds status. + sleep 10 + + local client_id="$(echo ${tf_output} | jq -er .application_id.value)" + local client_secret="$(echo ${tf_output} | jq -er .application_password_value.value)" + local subscription_id="$(echo ${tf_output} | jq -er .subscription_id.value)" + local resource_group_name="$(echo ${tf_output} | jq -er .resource_group_name.value)" + local tenant_id="$(echo ${tf_output} | jq -er .tenant_id.value)" + + vault secrets enable azure + + vault write azure/config \ + use_microsoft_graph_api=true \ + subscription_id="${subscription_id}" \ + tenant_id="${tenant_id}" \ + client_id="${client_id}" \ + client_secret="${client_secret}" + + local ttl=10 + vault write azure/roles/my-role ttl="${ttl}" azure_roles=-< /dev/null + do + if [[ "${tries}" -ge 10 ]]; then + echo "vault failed to remove service principal ${sp_id}, ttl=${ttl}" >&2 + exit 1 + fi + ((++tries)) + sleep .5 + done + +} >> $TESTS_OUT_FILE diff --git a/tests/acceptance/terraform/main.tf b/tests/acceptance/terraform/main.tf new file mode 100644 index 00000000..48fe12ef --- /dev/null +++ b/tests/acceptance/terraform/main.tf @@ -0,0 +1,218 @@ +# Configure Terraform +terraform { + required_providers { + null = { + source = "hashicorp/null" + version = "3.1.0" + } + random = { + source = "hashicorp/random" + version = "3.1.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = "2.84.0" + } + azuread = { + source = "hashicorp/azuread" + version = "2.8.0" + } + time = { + source = "hashicorp/time" + version = "0.7.2" + } + } +} + +# Configure the Azure Active Directory Provider +provider "azuread" { + tenant_id = var.tenant_id +} + +# Configure the Azure Provider +provider "azurerm" { + features {} +} + +variable "tenant_id" { + description = "Tenant ID that should be used." +} + +variable "name_prefix" { + description = "Prefix all resources with name." + +} +variable "legacy_aad_resource_access" { + description = "Provision AD application with Azure Active Directory Graph API access" + type = bool + default = false +} + +resource "random_id" "name" { + byte_length = 8 + prefix = var.name_prefix +} + +locals { + name = random_id.name.dec +} + +data "azurerm_subscription" "primary" {} + +data "azurerm_client_config" "vault_azure_secrets" {} + +data "azuread_client_config" "current" {} + +resource "azuread_application" "vault_azure_secrets" { + display_name = local.name + identifier_uris = ["api://${local.name}"] + owners = [data.azuread_client_config.current.object_id] + sign_in_audience = "AzureADMyOrg" + + feature_tags { + enterprise = false + gallery = true + } + + + dynamic "required_resource_access" { + for_each = var.legacy_aad_resource_access ? [] : [""] + # Microsoft Graph + content { + resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph + + resource_access { + id = "df021288-bdef-4463-88db-98f22de89214" # User.Read.All + type = "Role" + } + + resource_access { + id = "b4e74841-8e56-480b-be8b-910348b18b4c" # User.ReadWrite + type = "Scope" + } + } + } + # Legacy Azure Active Directory Graph (AADG) + dynamic "required_resource_access" { + for_each = var.legacy_aad_resource_access ? [""] : [] + content { + resource_app_id = "00000002-0000-0000-c000-000000000000" + resource_access { + id = "311a71cc-e848-46a1-bdf8-97ff7156d8e6" + type = "Scope" + } + resource_access { + id = "970d6fa6-214a-4a9b-8513-08fad511e2fd" + type = "Scope" + } + resource_access { + id = "1cda74f2-2616-4834-b122-5cb1b07f8a59" + type = "Role" + } + resource_access { + id = "78c8a3c8-a07e-4b9e-af1b-b5ccab50a175" + type = "Role" + } + } + } +} + +resource "time_rotating" "vault_azure_secrets" { + rotation_days = 7 +} + +resource "azuread_application_password" "vault_azure_secrets" { + application_object_id = azuread_application.vault_azure_secrets.object_id + rotate_when_changed = { + rotation = time_rotating.vault_azure_secrets.id + } +} + +resource "azuread_service_principal" "vault_azure_secrets" { + description = local.name + application_id = azuread_application.vault_azure_secrets.application_id + app_role_assignment_required = false + owners = [data.azuread_client_config.current.object_id] +} + +resource "azuread_group" "vault_azure_secrets" { + display_name = local.name + owners = [data.azuread_client_config.current.object_id] + security_enabled = true + + members = [ + azuread_service_principal.vault_azure_secrets.object_id + ] +} + +resource "azurerm_resource_group" "vault_azure_secrets" { + name = local.name + location = "East US" +} + +data "azurerm_role_definition" "builtin" { + name = "Owner" +} + +resource "azurerm_role_assignment" "vault_azure_secrets" { + scope = azurerm_resource_group.vault_azure_secrets.id + principal_id = azuread_service_principal.vault_azure_secrets.object_id + role_definition_id = "/subscriptions/${data.azurerm_subscription.primary.subscription_id}${data.azurerm_role_definition.builtin.id}" +} + +resource "null_resource" "admin_consent" { + depends_on = [ + azuread_application.vault_azure_secrets, + azurerm_role_assignment.vault_azure_secrets, + ] + provisioner "local-exec" { + command = < /dev/null && exit 0 + sleep .5 +done +exit 1 +HERE + } +} + +output "application_id" { + value = azuread_application.vault_azure_secrets.application_id +} + +output "application_password_value" { + sensitive = true + value = azuread_application_password.vault_azure_secrets.value +} + +output "application_password_id" { + value = azuread_application_password.vault_azure_secrets.id +} + +output "role_assignment_principal_type" { + value = azurerm_role_assignment.vault_azure_secrets.principal_type +} + +output "role_assignment_principal_id" { + value = azurerm_role_assignment.vault_azure_secrets.principal_id +} + +output "service_principal_id" { + value = azuread_service_principal.vault_azure_secrets.id +} + +output "resource_group_name" { + value = azurerm_resource_group.vault_azure_secrets.name +} + +output "tenant_id" { + value = var.tenant_id +} + +output "subscription_id" { + value = data.azurerm_subscription.primary.subscription_id +} + +output "name" { + value = local.name +}