diff --git a/vault/provider.go b/vault/provider.go index ccf529513..77d5fcaf8 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 000000000..8a81ba42b --- /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 000000000..6a24d05f8 --- /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 000000000..98abd6737 --- /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.