From 0b4101167881c37ec6963147c10fb73e07563e92 Mon Sep 17 00:00:00 2001
From: petems
Date: Fri, 2 Aug 2019 19:17:41 +0100
Subject: [PATCH] Add Azure Secret Backend resource
---
vault/provider.go | 4 +
vault/resource_azure_secret_backend.go | 183 ++++++++++++++++++++
vault/resource_azure_secret_backend_test.go | 69 ++++++++
website/docs/r/azure_secret_backend.html.md | 46 +++++
4 files changed, 302 insertions(+)
create mode 100644 vault/resource_azure_secret_backend.go
create mode 100644 vault/resource_azure_secret_backend_test.go
create mode 100644 website/docs/r/azure_secret_backend.html.md
diff --git a/vault/provider.go b/vault/provider.go
index ccf5295135..77d5fcaf81 100644
--- a/vault/provider.go
+++ b/vault/provider.go
@@ -240,6 +240,10 @@ var (
Resource: awsSecretBackendRoleResource(),
PathInventory: []string{"/aws/roles/{name}"},
},
+ "vault_azure_secret_backend": {
+ Resource: azureSecretBackendResource(),
+ PathInventory: []string{"/azure/config"},
+ },
"vault_azure_auth_backend_config": {
Resource: azureAuthBackendConfigResource(),
PathInventory: []string{"/auth/azure/config"},
diff --git a/vault/resource_azure_secret_backend.go b/vault/resource_azure_secret_backend.go
new file mode 100644
index 0000000000..8a81ba42bc
--- /dev/null
+++ b/vault/resource_azure_secret_backend.go
@@ -0,0 +1,183 @@
+package vault
+
+import (
+ "fmt"
+ "log"
+ "strings"
+
+ "github.com/hashicorp/terraform/helper/schema"
+ "github.com/hashicorp/vault/api"
+)
+
+func azureSecretBackendResource() *schema.Resource {
+ return &schema.Resource{
+ Create: azureSecretBackendCreate,
+ Read: azureSecretBackendRead,
+ Update: azureSecretBackendCreate,
+ Delete: azureSecretBackendDelete,
+ Exists: azureSecretBackendExists,
+ Importer: &schema.ResourceImporter{
+ State: schema.ImportStatePassthrough,
+ },
+
+ Schema: map[string]*schema.Schema{
+ "path": {
+ Type: schema.TypeString,
+ Optional: true,
+ ForceNew: true,
+ Default: "azure",
+ Description: "Path to mount the backend at.",
+ ValidateFunc: func(v interface{}, k string) (ws []string, errs []error) {
+ value := v.(string)
+ if strings.HasSuffix(value, "/") {
+ errs = append(errs, fmt.Errorf("path cannot end in '/'"))
+ }
+ return
+ },
+ DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
+ return old+"/" == new || new+"/" == old
+ },
+ },
+ "description": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "Human-friendly description of the mount for the backend.",
+ },
+ "subscription_id": {
+ Type: schema.TypeString,
+ ForceNew: true,
+ Required: true,
+ Sensitive: true,
+ Description: "The subscription id for the Azure Active Directory.",
+ },
+ "tenant_id": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The tenant id for the Azure Active Directory organization.",
+ Sensitive: true,
+ },
+ "client_id": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The client id for credentials to query the Azure APIs. Currently read permissions to query compute resources are required.",
+ Sensitive: true,
+ },
+ "client_secret": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The client secret for credentials to query the Azure APIs",
+ Sensitive: true,
+ },
+ "environment": {
+ Type: schema.TypeString,
+ Optional: true,
+ Default: "AzurePublicCloud",
+ Description: "The Azure cloud environment. Valid values: AzurePublicCloud, AzureUSGovernmentCloud, AzureChinaCloud, AzureGermanCloud.",
+ },
+ },
+ }
+}
+
+func azureSecretBackendCreate(d *schema.ResourceData, meta interface{}) error {
+ client := meta.(*api.Client)
+
+ path := d.Get("path").(string)
+ description := d.Get("description").(string)
+ tenantID := d.Get("tenant_id").(string)
+ clientID := d.Get("client_id").(string)
+ clientSecret := d.Get("client_secret").(string)
+ environment := d.Get("environment").(string)
+ subscriptionID := d.Get("subscription_id").(string)
+
+ configPath := azureSecretBackendPath(path)
+
+ data := map[string]interface{}{
+ "tenant_id": tenantID,
+ "client_id": clientID,
+ "client_secret": clientSecret,
+ "environment": environment,
+ "subscription_id": subscriptionID,
+ }
+
+ d.Partial(true)
+ log.Printf("[DEBUG] Mounting Azure backend at %q", path)
+ err := client.Sys().Mount(path, &api.MountInput{
+ Type: "azure",
+ Description: description,
+ Config: api.MountConfigInput{},
+ })
+ if err != nil {
+ return fmt.Errorf("error mounting to %q: %s", path, err)
+ }
+ log.Printf("[DEBUG] Mounted Azure backend at %q", path)
+ d.SetId(path)
+
+ d.SetPartial("path")
+
+ log.Printf("[DEBUG] Writing Azure configuration to %q", configPath)
+ if _, err := client.Logical().Write(configPath, data); err != nil {
+ return fmt.Errorf("error writing Azure configuration for %q: %s", path, err)
+ }
+ log.Printf("[DEBUG] Wrote Azure configuration to %q", configPath)
+ d.Partial(false)
+
+ return azureSecretBackendRead(d, meta)
+}
+
+func azureSecretBackendRead(d *schema.ResourceData, meta interface{}) error {
+ client := meta.(*api.Client)
+
+ path := d.Id()
+
+ log.Printf("[DEBUG] Reading Azure backend mount %q from Vault", path)
+ mounts, err := client.Sys().ListMounts()
+ if err != nil {
+ return fmt.Errorf("error reading mount %q: %s", path, err)
+ }
+ log.Printf("[DEBUG] Read Azure backend mount %q from Vault", path)
+
+ // the API always returns the path with a trailing slash, so let's make
+ // sure we always specify it as a trailing slash.
+ mount, ok := mounts[strings.Trim(path, "/")+"/"]
+ if !ok {
+ log.Printf("[WARN] Mount %q not found, removing backend from state.", path)
+ d.SetId("")
+ return nil
+ }
+
+ d.Set("path", path)
+ d.Set("description", mount.Description)
+
+ return nil
+}
+
+func azureSecretBackendDelete(d *schema.ResourceData, meta interface{}) error {
+ client := meta.(*api.Client)
+
+ path := d.Id()
+
+ log.Printf("[DEBUG] Unmounting Azure backend %q", path)
+ err := client.Sys().Unmount(path)
+ if err != nil {
+ return fmt.Errorf("error unmounting Azure backend from %q: %s", path, err)
+ }
+ log.Printf("[DEBUG] Unmounted Azure backend %q", path)
+ return nil
+}
+
+func azureSecretBackendExists(d *schema.ResourceData, meta interface{}) (bool, error) {
+ client := meta.(*api.Client)
+ path := d.Id()
+ log.Printf("[DEBUG] Checking if Azure backend exists at %q", path)
+ mounts, err := client.Sys().ListMounts()
+ if err != nil {
+ return true, fmt.Errorf("error retrieving list of mounts: %s", err)
+ }
+ log.Printf("[DEBUG] Checked if Azure backend exists at %q", path)
+ _, ok := mounts[strings.Trim(path, "/")+"/"]
+ return ok, nil
+}
+
+func azureSecretBackendPath(path string) string {
+ return strings.Trim(path, "/") + "/config"
+}
diff --git a/vault/resource_azure_secret_backend_test.go b/vault/resource_azure_secret_backend_test.go
new file mode 100644
index 0000000000..6a24d05f8a
--- /dev/null
+++ b/vault/resource_azure_secret_backend_test.go
@@ -0,0 +1,69 @@
+package vault
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/hashicorp/terraform/helper/acctest"
+ "github.com/hashicorp/terraform/helper/resource"
+ "github.com/hashicorp/terraform/terraform"
+ "github.com/hashicorp/vault/api"
+)
+
+func TestAzureSecretBackend(t *testing.T) {
+ path := acctest.RandomWithPrefix("tf-test-azure")
+ resource.Test(t, resource.TestCase{
+ Providers: testProviders,
+ PreCheck: func() { testAccPreCheck(t) },
+ CheckDestroy: testAccAzureSecretBackendCheckDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config: testAzureSecretBackend_initialConfig(path),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("vault_azure_secret_backend.test", "path", path),
+ resource.TestCheckResourceAttr("vault_azure_secret_backend.test", "subscription_id", "11111111-2222-3333-4444-111111111111"),
+ resource.TestCheckResourceAttr("vault_azure_secret_backend.test", "tenant_id", "11111111-2222-3333-4444-222222222222"),
+ resource.TestCheckResourceAttr("vault_azure_secret_backend.test", "client_id", "11111111-2222-3333-4444-333333333333"),
+ resource.TestCheckResourceAttr("vault_azure_secret_backend.test", "client_secret", "12345678901234567890"),
+ resource.TestCheckResourceAttr("vault_azure_secret_backend.test", "environment", "AzurePublicCloud"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccAzureSecretBackendCheckDestroy(s *terraform.State) error {
+ client := testProvider.Meta().(*api.Client)
+
+ mounts, err := client.Sys().ListMounts()
+ if err != nil {
+ return err
+ }
+
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "vault_azure_secret_backend" {
+ continue
+ }
+ for path, mount := range mounts {
+ path = strings.Trim(path, "/")
+ rsPath := strings.Trim(rs.Primary.Attributes["path"], "/")
+ if mount.Type == "azure" && path == rsPath {
+ return fmt.Errorf("Mount %q still exists", path)
+ }
+ }
+ }
+ return nil
+}
+
+func testAzureSecretBackend_initialConfig(path string) string {
+ return fmt.Sprintf(`
+resource "vault_azure_secret_backend" "test" {
+ path = "%s"
+ subscription_id = "11111111-2222-3333-4444-111111111111"
+ tenant_id = "11111111-2222-3333-4444-222222222222"
+ client_id = "11111111-2222-3333-4444-333333333333"
+ client_secret = "12345678901234567890"
+ environment = "AzurePublicCloud"
+}`, path)
+}
diff --git a/website/docs/r/azure_secret_backend.html.md b/website/docs/r/azure_secret_backend.html.md
new file mode 100644
index 0000000000..98abd67371
--- /dev/null
+++ b/website/docs/r/azure_secret_backend.html.md
@@ -0,0 +1,46 @@
+---
+layout: "vault"
+page_title: "Vault: vault_azure_secret_backend resource"
+sidebar_current: "docs-vault-resource-azure-secret-backend"
+description: |-
+ Creates an azure secret backend for Vault.
+---
+
+# vault\_azure\_secret\_backend
+
+Creates an Azure Secret Backend for Vault.
+
+The Azure secrets engine dynamically generates Azure service principals and role assignments. Vault roles can be mapped to one or more Azure roles, providing a simple, flexible way to manage the permissions granted to generated service principals.
+
+~> **Important** All data provided in the resource configuration will be
+written in cleartext to state and plan files generated by Terraform, and
+will appear in the console output when Terraform runs. Protect these
+artifacts accordingly. See
+[the main provider documentation](../index.html)
+for more details.
+
+## Example Usage
+
+```hcl
+resource "vault_azure_secret_backend" "azure" {
+ subscription_id = "11111111-2222-3333-4444-111111111111"
+ tenant_id = "11111111-2222-3333-4444-222222222222"
+ client_id = "11111111-2222-3333-4444-333333333333"
+ client_secret = "12345678901234567890"
+ environment = "AzurePublicCloud"
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+- `subscription_id` (`string: `) - The subscription id for the Azure Active Directory.
+- `tenant_id` (`string: `) - The tenant id for the Azure Active Directory.
+- `client_id` (`string:""`) - The OAuth2 client id to connect to Azure.
+- `client_secret` (`string:""`) - The OAuth2 client secret to connect to Azure.
+- `environment` (`string:""`) - The Azure environment.
+
+## Attributes Reference
+
+No additional attributes are exported by this resource.