diff --git a/vault/provider.go b/vault/provider.go index 9791cf6d7..56caf3467 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -113,6 +113,8 @@ func Provider() terraform.ResourceProvider { "vault_aws_auth_backend_sts_role": awsAuthBackendSTSRoleResource(), "vault_aws_secret_backend": awsSecretBackendResource(), "vault_aws_secret_backend_role": awsSecretBackendRoleResource(), + "vault_azure_auth_backend_config": azureAuthBackendConfigResource(), + "vault_azure_auth_backend_role": azureAuthBackendRoleResource(), "vault_consul_secret_backend": consulSecretBackendResource(), "vault_database_secret_backend_connection": databaseSecretBackendConnectionResource(), "vault_database_secret_backend_role": databaseSecretBackendRoleResource(), diff --git a/vault/resource_azure_auth_backend_config.go b/vault/resource_azure_auth_backend_config.go new file mode 100644 index 000000000..0e55dd154 --- /dev/null +++ b/vault/resource_azure_auth_backend_config.go @@ -0,0 +1,156 @@ +package vault + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/vault/api" +) + +func azureAuthBackendConfigResource() *schema.Resource { + return &schema.Resource{ + Create: azureAuthBackendWrite, + Read: azureAuthBackendRead, + Update: azureAuthBackendWrite, + Delete: azureAuthBackendDelete, + Exists: azureAuthBackendExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "backend": { + Type: schema.TypeString, + Optional: true, + Description: "Unique name of the auth backend to configure.", + ForceNew: true, + Default: "azure", + // standardise on no beginning or trailing slashes + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + "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, + }, + "resource": { + Type: schema.TypeString, + Required: true, + Description: "The configured URL for the application registered in Azure Active Directory.", + }, + "environment": { + Type: schema.TypeString, + Optional: true, + Description: "The Azure cloud environment. Valid values: AzurePublicCloud, AzureUSGovernmentCloud, AzureChinaCloud, AzureGermanCloud.", + }, + }, + } +} + +func azureAuthBackendWrite(d *schema.ResourceData, meta interface{}) error { + config := meta.(*api.Client) + + // if backend comes from the config, it won't have the StateFunc + // applied yet, so we need to apply it again. + backend := d.Get("backend").(string) + tenantId := d.Get("tenant_id").(string) + clientId := d.Get("client_id").(string) + clientSecret := d.Get("client_secret").(string) + resource := d.Get("resource").(string) + environment := d.Get("environment").(string) + + path := azureAuthBackendConfigPath(backend) + + data := map[string]interface{}{ + "tenant_id": tenantId, + "client_id": clientId, + "client_secret": clientSecret, + "resource": resource, + "environment": environment, + } + + log.Printf("[DEBUG] Writing Azure auth backend config to %q", path) + _, err := config.Logical().Write(path, data) + if err != nil { + return fmt.Errorf("error writing to %q: %s", path, err) + } + log.Printf("[DEBUG] Wrote Azure auth backend config to %q", path) + + d.SetId(path) + + return azureAuthBackendRead(d, meta) +} + +func azureAuthBackendRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*api.Client) + + log.Printf("[DEBUG] Reading Azure auth backend config") + secret, err := config.Logical().Read(d.Id()) + if err != nil { + return fmt.Errorf("error reading Azure auth backend config from %q: %s", d.Id(), err) + } + log.Printf("[DEBUG] Read Azure auth backend config") + + if secret == nil { + log.Printf("[WARN] No info found at %q; removing from state.", d.Id()) + d.SetId("") + return nil + } + idPieces := strings.Split(d.Id(), "/") + if len(idPieces) != 3 { + return fmt.Errorf("expected %q to have 4 pieces, has %d", d.Id(), len(idPieces)) + } + d.Set("backend", idPieces[1]) + d.Set("tenant_id", secret.Data["tenant_id"]) + d.Set("client_id", secret.Data["client_id"]) + d.Set("client_secert", secret.Data["client_secret"]) + d.Set("resource", secret.Data["resource"]) + d.Set("environment", secret.Data["environment"]) + return nil +} + +func azureAuthBackendDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*api.Client) + + log.Printf("[DEBUG] Deleting Azure auth backend config from %q", d.Id()) + _, err := config.Logical().Delete(d.Id()) + if err != nil { + return fmt.Errorf("error deleting Azure auth backend config from %q: %s", d.Id(), err) + } + log.Printf("[DEBUG] Deleted Azure auth backend config from %q", d.Id()) + + return nil +} + +func azureAuthBackendExists(d *schema.ResourceData, meta interface{}) (bool, error) { + config := meta.(*api.Client) + + log.Printf("[DEBUG] Checking if Azure auth backend is configured at %q", d.Id()) + secret, err := config.Logical().Read(d.Id()) + if err != nil { + return true, fmt.Errorf("error checking if Azure auth backend is configured at %q: %s", d.Id(), err) + } + log.Printf("[DEBUG] Checked if Azure auth backend is configured at %q", d.Id()) + return secret != nil, nil +} + +func azureAuthBackendConfigPath(path string) string { + return "auth/" + strings.Trim(path, "/") + "/config" +} diff --git a/vault/resource_azure_auth_backend_config_test.go b/vault/resource_azure_auth_backend_config_test.go new file mode 100644 index 000000000..9a27ebb73 --- /dev/null +++ b/vault/resource_azure_auth_backend_config_test.go @@ -0,0 +1,149 @@ +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/vault/api" +) + +func TestAccAzureAuthBackendConfig_import(t *testing.T) { + backend := acctest.RandomWithPrefix("azure") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testProviders, + CheckDestroy: testAccCheckAzureAuthBackendConfigDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureAuthBackendConfig_basic(backend), + Check: testAccAzureAuthBackendConfigCheck_attrs(backend), + }, + { + ResourceName: "vault_azure_auth_backend_config.config", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"client_secret"}, + }, + }, + }) +} + +func TestAccAzureAuthBackendConfig_basic(t *testing.T) { + backend := acctest.RandomWithPrefix("azure") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckAzureAuthBackendConfigDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureAuthBackendConfig_basic(backend), + Check: testAccAzureAuthBackendConfigCheck_attrs(backend), + }, + { + Config: testAccAzureAuthBackendConfig_updated(backend), + Check: testAccAzureAuthBackendConfigCheck_attrs(backend), + }, + }, + }) +} + +func testAccCheckAzureAuthBackendConfigDestroy(s *terraform.State) error { + config := testProvider.Meta().(*api.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_azure_auth_backend_config" { + continue + } + secret, err := config.Logical().Read(rs.Primary.ID) + if err != nil { + return fmt.Errorf("error checking for Azure auth backend %q config: %s", rs.Primary.ID, err) + } + if secret != nil { + return fmt.Errorf("Azure auth backend %q still configured", rs.Primary.ID) + } + } + return nil +} + +func testAccAzureAuthBackendConfig_basic(backend string) string { + return fmt.Sprintf(` +resource "vault_auth_backend" "azure" { + type = "azure" + path = "%s" + description = "Test auth backend for Azure backend config" +} + +resource "vault_azure_auth_backend_config" "config" { + backend = "${vault_auth_backend.azure.path}" + tenant_id = "11111111-2222-3333-4444-555555555555" + client_id = "11111111-2222-3333-4444-555555555555" + client_secret = "12345678901234567890" + resource = "http://vault.hashicorp.com" +} +`, backend) +} + +func testAccAzureAuthBackendConfigCheck_attrs(backend string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceState := s.Modules[0].Resources["vault_azure_auth_backend_config.config"] + if resourceState == nil { + return fmt.Errorf("resource not found in state") + } + + instanceState := resourceState.Primary + if instanceState == nil { + return fmt.Errorf("resource has no primary instance") + } + + endpoint := instanceState.ID + + if endpoint != "auth/"+backend+"/config" { + return fmt.Errorf("expected ID to be %q, got %q", "auth/"+backend+"/config", endpoint) + } + + config := testProvider.Meta().(*api.Client) + resp, err := config.Logical().Read(endpoint) + if err != nil { + return fmt.Errorf("error reading back Azure auth config from %q: %s", endpoint, err) + } + if resp == nil { + return fmt.Errorf("Azure auth not configured at %q", endpoint) + } + attrs := map[string]string{ + "tenant_id": "tenant_id", + "client_id": "client_id", + //"client_secret": "client_secret", + "resource": "resource", + "environment": "environment", + } + for stateAttr, apiAttr := range attrs { + if resp.Data[apiAttr] == nil && instanceState.Attributes[stateAttr] == "" { + continue + } + if resp.Data[apiAttr] != instanceState.Attributes[stateAttr] { + return fmt.Errorf("expected %s (%s) of %q to be %q, got %q", apiAttr, stateAttr, endpoint, instanceState.Attributes[stateAttr], resp.Data[apiAttr]) + } + } + return nil + } +} + +func testAccAzureAuthBackendConfig_updated(backend string) string { + return fmt.Sprintf(` +resource "vault_auth_backend" "azure" { + path = "%s" + type = "azure" + description = "Test auth backend for Azure backend config" +} + +resource "vault_azure_auth_backend_config" "config" { + backend = "${vault_auth_backend.azure.path}" + tenant_id = "11111111-2222-3333-4444-555555555555" + client_id = "11111111-2222-3333-4444-555555555555" + client_secret = "12345678901234567890" + resource = "http://vault.hashicorp.com" +}`, backend) +} diff --git a/vault/resource_azure_auth_backend_role.go b/vault/resource_azure_auth_backend_role.go new file mode 100644 index 000000000..ba502d8d9 --- /dev/null +++ b/vault/resource_azure_auth_backend_role.go @@ -0,0 +1,432 @@ +package vault + +import ( + "encoding/json" + "fmt" + "log" + "regexp" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/vault/api" +) + +var ( + azureAuthBackendRoleBackendFromPathRegex = regexp.MustCompile("^auth/(.+)/role/.+$") + azureAuthBackendRoleNameFromPathRegex = regexp.MustCompile("^auth/.+/role/(.+)$") +) + +func azureAuthBackendRoleResource() *schema.Resource { + return &schema.Resource{ + Create: azureAuthBackendRoleCreate, + Read: azureAuthBackendRoleRead, + Update: azureAuthBackendRoleUpdate, + Delete: azureAuthBackendRoleDelete, + Exists: azureAuthBackendRoleExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "role": { + Type: schema.TypeString, + Required: true, + Description: "Name of the role.", + ForceNew: true, + }, + "bound_service_principal_ids": { + Type: schema.TypeList, + Optional: true, + Description: "The list of Service Principal IDs that login is restricted to.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "bound_group_ids": { + Type: schema.TypeList, + Optional: true, + Description: "The list of group ids that login is restricted to.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "bound_locations": { + Type: schema.TypeList, + Optional: true, + Description: "The list of locations that login is restricted to.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "bound_subscription_ids": { + Type: schema.TypeList, + Optional: true, + Description: "The list of subscription IDs that login is restricted to.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "bound_resource_groups": { + Type: schema.TypeList, + Optional: true, + Description: "The list of resource groups that login is restricted to.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "bound_scale_sets": { + Type: schema.TypeList, + Optional: true, + Description: "The list of scale set names that the login is restricted to.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "ttl": { + Type: schema.TypeInt, + Optional: true, + Description: "The TTL period of tokens issued using this role, provided as the number of seconds.", + }, + "max_ttl": { + Type: schema.TypeInt, + Optional: true, + Description: "The maximum allowed lifetime of tokens issued using this role, provided as the number of seconds.", + }, + "period": { + Type: schema.TypeInt, + Optional: true, + Description: "If set, indicates that the token generated using this role should never expire. The token should be renewed within the duration specified by this value. At each renewal, the token's TTL will be set to the value of this field. The maximum allowed lifetime of token issued using this role. Specified as a number of seconds.", + }, + "policies": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Policies to be set on tokens issued using this role.", + }, + "backend": { + Type: schema.TypeString, + Optional: true, + Description: "Unique name of the auth backend to configure.", + ForceNew: true, + Default: "azure", + // standardise on no beginning or trailing slashes + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + }, + } +} + +func azureAuthBackendRoleCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + backend := d.Get("backend").(string) + role := d.Get("role").(string) + + path := azureAuthBackendRolePath(backend, role) + + log.Printf("[DEBUG] Writing Azure auth backend role %q", path) + iPolicies := d.Get("policies").([]interface{}) + policies := make([]string, len(iPolicies)) + for i, iPolicy := range iPolicies { + policies[i] = iPolicy.(string) + } + + data := map[string]interface{}{} + + if v, ok := d.GetOk("ttl"); ok { + data["ttl"] = v.(int) + } + if v, ok := d.GetOk("max_ttl"); ok { + data["max_ttl"] = v.(int) + } + if v, ok := d.GetOk("period"); ok { + data["period"] = v.(int) + } + if len(policies) > 0 { + data["policies"] = policies + } + + if _, ok := d.GetOk("bound_service_principal_ids"); ok { + iSPI := d.Get("bound_service_principal_ids").([]interface{}) + bound_service_principal_ids := make([]string, len(iSPI)) + for i, iSP := range iSPI { + bound_service_principal_ids[i] = iSP.(string) + } + data["bound_service_principal_ids"] = bound_service_principal_ids + } + + if _, ok := d.GetOk("bound_group_ids"); ok { + iGI := d.Get("bound_group_ids").([]interface{}) + bound_group_ids := make([]string, len(iGI)) + for i, iG := range iGI { + bound_group_ids[i] = iG.(string) + } + data["bound_group_ids"] = bound_group_ids + } + + if _, ok := d.GetOk("bound_locations"); ok { + iLS := d.Get("bound_locations").([]interface{}) + bound_locations := make([]string, len(iLS)) + for i, iL := range iLS { + bound_locations[i] = iL.(string) + } + data["bound_locations"] = bound_locations + } + + if _, ok := d.GetOk("bound_subscription_ids"); ok { + iSI := d.Get("bound_subscription_ids").([]interface{}) + bound_subscription_ids := make([]string, len(iSI)) + for i, iS := range iSI { + bound_subscription_ids[i] = iS.(string) + } + data["bound_subscription_ids"] = bound_subscription_ids + } + + if _, ok := d.GetOk("bound_resource_groups"); ok { + iRGN := d.Get("bound_resource_groups").([]interface{}) + bound_resource_groups := make([]string, len(iRGN)) + for i, iRG := range iRGN { + bound_resource_groups[i] = iRG.(string) + } + data["bound_resource_groups"] = bound_resource_groups + } + + if _, ok := d.GetOk("bound_scale_sets"); ok { + iSS := d.Get("bound_scale_sets").([]interface{}) + bound_scale_sets := make([]string, len(iSS)) + for i, iS := range iSS { + bound_scale_sets[i] = iS.(string) + } + data["bound_scale_sets"] = bound_scale_sets + } + + d.SetId(path) + if _, err := client.Logical().Write(path, data); err != nil { + d.SetId("") + return fmt.Errorf("error writing Azure auth backend role %q: %s", path, err) + } + log.Printf("[DEBUG] Wrote Azure auth backend role %q", path) + + return azureAuthBackendRoleRead(d, meta) +} + +func azureAuthBackendRoleRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + backend, err := azureAuthBackendRoleBackendFromPath(path) + if err != nil { + return fmt.Errorf("invalid path %q for Azure auth backend role: %s", path, err) + } + + role, err := azureAuthBackendRoleNameFromPath(path) + if err != nil { + return fmt.Errorf("invalid path %q for Azure auth backend role: %s", path, err) + } + + log.Printf("[DEBUG] Reading Azure auth backend role %q", path) + resp, err := client.Logical().Read(path) + if err != nil { + return fmt.Errorf("error reading Azure auth backend role %q: %s", path, err) + } + log.Printf("[DEBUG] Read Azure auth backend role %q", path) + if resp == nil { + log.Printf("[WARN] Azure auth backend role %q not found, removing from state", path) + d.SetId("") + return nil + } + iPolicies := resp.Data["policies"].([]interface{}) + policies := make([]string, len(iPolicies)) + for i, iPolicy := range iPolicies { + policies[i] = iPolicy.(string) + } + + ttl, err := resp.Data["ttl"].(json.Number).Int64() + if err != nil { + return fmt.Errorf("expected ttl %q to be a number, isn't", resp.Data["ttl"]) + } + + maxTTL, err := resp.Data["max_ttl"].(json.Number).Int64() + if err != nil { + return fmt.Errorf("expected max_ttl %q to be a number, isn't", resp.Data["max_ttl"]) + } + + period, err := resp.Data["period"].(json.Number).Int64() + if err != nil { + return fmt.Errorf("expected period %q to be a number, isn't", resp.Data["period"]) + } + + d.Set("backend", backend) + d.Set("role", role) + + if _, ok := d.GetOk("bound_service_principal_ids"); ok { + d.Set("bound_service_principal_ids", resp.Data["bound_service_principal_ids"]) + } + + if _, ok := d.GetOk("bound_group_ids"); ok { + d.Set("bound_group_ids", resp.Data["bound_group_ids"]) + } + + if _, ok := d.GetOk("bound_locations"); ok { + d.Set("bound_locations", resp.Data["bound_locations"]) + } + + if _, ok := d.GetOk("bound_subscription_ids"); ok { + d.Set("bound_subscription_ids", resp.Data["bound_subscription_ids"]) + } + + if _, ok := d.GetOk("bound_resource_groups"); ok { + d.Set("bound_resource_groups", resp.Data["bound_resource_groups"]) + } + + if _, ok := d.GetOk("bound_scale_sets"); ok { + d.Set("bound_scale_sets", resp.Data["bound_scale_sets"]) + } + + d.Set("ttl", ttl) + d.Set("max_ttl", maxTTL) + d.Set("period", period) + d.Set("policies", policies) + + return nil +} + +func azureAuthBackendRoleUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + log.Printf("[DEBUG] Updating Azure auth backend role %q", path) + iPolicies := d.Get("policies").([]interface{}) + policies := make([]string, len(iPolicies)) + for i, iPolicy := range iPolicies { + policies[i] = iPolicy.(string) + } + + data := map[string]interface{}{} + if v, ok := d.GetOk("ttl"); ok { + data["ttl"] = v.(int) + } + if v, ok := d.GetOk("max_ttl"); ok { + data["max_ttl"] = v.(int) + } + if v, ok := d.GetOk("period"); ok { + data["period"] = v.(int) + } + if len(policies) > 0 { + data["policies"] = policies + } + if _, ok := d.GetOk("bound_service_principal_ids"); ok { + iSPI := d.Get("bound_service_principal_ids").([]interface{}) + bound_service_principal_ids := make([]string, len(iSPI)) + for i, iSP := range iSPI { + bound_service_principal_ids[i] = iSP.(string) + } + data["bound_service_principal_ids"] = bound_service_principal_ids + } + if _, ok := d.GetOk("bound_group_ids"); ok { + iGI := d.Get("bound_group_ids").([]interface{}) + bound_group_ids := make([]string, len(iGI)) + for i, iG := range iGI { + bound_group_ids[i] = iG.(string) + } + data["bound_group_ids"] = bound_group_ids + } + if _, ok := d.GetOk("bound_locations"); ok { + iLS := d.Get("bound_locations").([]interface{}) + bound_locations := make([]string, len(iLS)) + for i, iL := range iLS { + bound_locations[i] = iL.(string) + } + data["bound_locations"] = bound_locations + } + if _, ok := d.GetOk("bound_subscription_ids"); ok { + iSI := d.Get("bound_subscription_ids").([]interface{}) + bound_subscription_ids := make([]string, len(iSI)) + for i, iS := range iSI { + bound_subscription_ids[i] = iS.(string) + } + data["bound_subscription_ids"] = bound_subscription_ids + } + if _, ok := d.GetOk("bound_resource_groups"); ok { + iRGN := d.Get("bound_resource_groups").([]interface{}) + bound_resource_groups := make([]string, len(iRGN)) + for i, iRG := range iRGN { + bound_resource_groups[i] = iRG.(string) + } + data["bound_resource_groups"] = bound_resource_groups + } + if _, ok := d.GetOk("bound_scale_sets"); ok { + iSS := d.Get("bound_scale_sets").([]interface{}) + bound_scale_sets := make([]string, len(iSS)) + for i, iS := range iSS { + bound_scale_sets[i] = iS.(string) + } + data["bound_scale_sets"] = bound_scale_sets + } + log.Printf("[DEBUG] Updating role %q in Azure auth backend", path) + _, err := client.Logical().Write(path, data) + if err != nil { + return fmt.Errorf("Error updating Azure auth role %q: %s", path, err) + } + log.Printf("[DEBUG] Updated role %q to Azure auth backend", path) + + return azureAuthBackendRoleRead(d, meta) +} + +func azureAuthBackendRoleDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + log.Printf("[DEBUG] Deleting Azure auth backend role %q", path) + _, err := client.Logical().Delete(path) + if err != nil { + return fmt.Errorf("error deleting Azure auth backend role %q", path) + } + log.Printf("[DEBUG] Deleted Azure auth backend role %q", path) + + return nil +} + +func azureAuthBackendRoleExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client := meta.(*api.Client) + + path := d.Id() + log.Printf("[DEBUG] Checking if Azure auth backend role %q exists", path) + + resp, err := client.Logical().Read(path) + if err != nil { + return true, fmt.Errorf("error checking if Azure auth backend role %q exists: %s", path, err) + } + log.Printf("[DEBUG] Checked if Azure auth backend role %q exists", path) + + return resp != nil, nil +} + +func azureAuthBackendRolePath(backend, role string) string { + return "auth/" + strings.Trim(backend, "/") + "/role/" + strings.Trim(role, "/") +} + +func azureAuthBackendRoleNameFromPath(path string) (string, error) { + if !azureAuthBackendRoleNameFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no role found") + } + res := azureAuthBackendRoleNameFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for role", len(res)) + } + return res[1], nil +} + +func azureAuthBackendRoleBackendFromPath(path string) (string, error) { + if !azureAuthBackendRoleBackendFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no backend found") + } + res := azureAuthBackendRoleBackendFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for backend", len(res)) + } + return res[1], nil +} diff --git a/vault/resource_azure_auth_backend_role_test.go b/vault/resource_azure_auth_backend_role_test.go new file mode 100644 index 000000000..dcf33ac0f --- /dev/null +++ b/vault/resource_azure_auth_backend_role_test.go @@ -0,0 +1,233 @@ +package vault + +import ( + "encoding/json" + "fmt" + "strconv" + "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 TestAzureAuthBackendRole_basic(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-azure-backend") + name := acctest.RandomWithPrefix("tf-test-azure-role") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testProviders, + CheckDestroy: testAzureAuthBackendRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAzureAuthBackendRoleConfig_basic(backend, name), + Check: testAzureAuthBackendRoleCheck_attrs(backend, name), + }, + }, + }) +} + +func TestAzureAuthBackendRole(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-azure-backend") + name := acctest.RandomWithPrefix("tf-test-azure-role") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testProviders, + CheckDestroy: testAzureAuthBackendRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAzureAuthBackendRoleConfig(backend, name), + Check: testAzureAuthBackendRoleCheck_attrs(backend, name), + }, + }, + }) +} + +func testAzureAuthBackendRoleDestroy(s *terraform.State) error { + client := testProvider.Meta().(*api.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_azure_auth_backend_role" { + continue + } + secret, err := client.Logical().Read(rs.Primary.ID) + if err != nil { + return fmt.Errorf("Error checking for Azure auth backend role %q: %s", rs.Primary.ID, err) + } + if secret != nil { + return fmt.Errorf("Azure auth backend role %q still exists", rs.Primary.ID) + } + } + return nil +} + +func testAzureAuthBackendRoleCheck_attrs(backend, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceState := s.Modules[0].Resources["vault_azure_auth_backend_role.test"] + if resourceState == nil { + return fmt.Errorf("resource not found in state") + } + + instanceState := resourceState.Primary + if instanceState == nil { + return fmt.Errorf("resource has no primary instance") + } + + endpoint := "auth/" + strings.Trim(backend, "/") + "/role/" + name + if endpoint != instanceState.ID { + return fmt.Errorf("expected ID to be %q, got %q instead", endpoint, instanceState.ID) + } + + client := testProvider.Meta().(*api.Client) + authMounts, err := client.Sys().ListAuth() + if err != nil { + return err + } + authMount := authMounts[strings.Trim(backend, "/")+"/"] + + if authMount == nil { + return fmt.Errorf("auth mount %s not present", backend) + } + + if "azure" != authMount.Type { + return fmt.Errorf("incorrect mount type: %s", authMount.Type) + } + + resp, err := client.Logical().Read(instanceState.ID) + if err != nil { + return err + } + + attrs := map[string]string{ + "type": "role_type", + "ttl": "ttl", + "max_ttl": "max_ttl", + "period": "period", + "policies": "policies", + "bound_service_principal_ids": "bound_service_principal_ids", + "bound_group_ids": "bound_group_ids", + "bound_locations": "bound_locations", + "bound_subscription_ids": "bound_subscription_ids", + "bound_resource_groups": "bound_resource_groups", + "bound_scale_sets": "bound_scale_sets", + } + + for stateAttr, apiAttr := range attrs { + if resp.Data[apiAttr] == nil && instanceState.Attributes[stateAttr] == "" { + continue + } + var match bool + switch resp.Data[apiAttr].(type) { + case json.Number: + apiData, err := resp.Data[apiAttr].(json.Number).Int64() + if err != nil { + return fmt.Errorf("Expected API field %s to be an int, was %q", apiAttr, resp.Data[apiAttr]) + } + stateData, err := strconv.ParseInt(instanceState.Attributes[stateAttr], 10, 64) + if err != nil { + return fmt.Errorf("Expected state field %s to be an int, was %q", stateAttr, instanceState.Attributes[stateAttr]) + } + match = apiData == stateData + case bool: + if _, ok := resp.Data[apiAttr]; !ok && instanceState.Attributes[stateAttr] == "" { + match = true + } else { + stateData, err := strconv.ParseBool(instanceState.Attributes[stateAttr]) + if err != nil { + return fmt.Errorf("Expected state field %s to be a bool, was %q", stateAttr, instanceState.Attributes[stateAttr]) + } + match = resp.Data[apiAttr] == stateData + } + + case []interface{}: + apiData := resp.Data[apiAttr].([]interface{}) + length := instanceState.Attributes[stateAttr+".#"] + if length == "" { + if len(resp.Data[apiAttr].([]interface{})) != 0 { + return fmt.Errorf("Expected state field %s to have %d entries, had 0", stateAttr, len(apiData)) + } + match = true + } else { + count, err := strconv.Atoi(length) + if err != nil { + return fmt.Errorf("Expected %s.# to be a number, got %q", stateAttr, instanceState.Attributes[stateAttr+".#"]) + } + if count != len(apiData) { + return fmt.Errorf("Expected %s to have %d entries in state, has %d", stateAttr, len(apiData), count) + } + + for i := 0; i < count; i++ { + found := false + for stateKey, stateValue := range instanceState.Attributes { + if strings.HasPrefix(stateKey, stateAttr) { + if apiData[i] == stateValue { + found = true + } + } + } + if !found { + return fmt.Errorf("Expected item %d of %s (%s in state) of %q to be in state but wasn't", i, apiAttr, stateAttr, endpoint) + } + } + match = true + } + default: + match = resp.Data[apiAttr] == instanceState.Attributes[stateAttr] + + } + if !match { + return fmt.Errorf("Expected %s (%s in state) of %q to be %q, got %q", apiAttr, stateAttr, endpoint, instanceState.Attributes[stateAttr], resp.Data[apiAttr]) + } + + } + + return nil + } +} + +func testAzureAuthBackendRoleConfig_basic(backend, name string) string { + + return fmt.Sprintf(` + +resource "vault_auth_backend" "azure" { + path = "%s" + type = "azure" +} + +resource "vault_azure_auth_backend_role" "test" { + backend = "${vault_auth_backend.azure.path}" + role = "%s" + bound_service_principal_ids = ["foo"] + bound_resource_groups = ["bar"] + ttl = 300 + max_ttl = 600 + policies = ["policy_a", "policy_b"] +} +`, backend, name) + +} + +func testAzureAuthBackendRoleConfig(backend, name string) string { + + return fmt.Sprintf(` + +resource "vault_auth_backend" "azure" { + path = "%s" + type = "azure" +} + +resource "vault_azure_auth_backend_role" "test" { + backend = "${vault_auth_backend.azure.path}" + role = "%s" + ttl = 300 + max_ttl = 600 + policies = ["policy_a", "policy_b"] + bound_locations = ["west us"] + bound_resource_groups = ["test"] +} +`, backend, name) +} diff --git a/website/docs/r/azure_auth_backend_config.html.md b/website/docs/r/azure_auth_backend_config.html.md new file mode 100644 index 000000000..c78188c28 --- /dev/null +++ b/website/docs/r/azure_auth_backend_config.html.md @@ -0,0 +1,77 @@ +--- +layout: "vault" +page_title: "Vault: vault_azure_auth_backend_config resource" +sidebar_current: "docs-vault-resource-azure-auth-backend-config" +description: |- + Configures the Azure Auth Backend in Vault. +--- + +# vault\_azure\_auth\_backend\_config + +Configures the Azure Auth Backend in Vault. + +This resource sets the access key and secret key that Vault will use +when making API requests on behalf of an Azure Auth Backend. It can also +be used to override the URLs Vault uses when making those API requests. + +For more information, see the +[Vault docs](https://www.vaultproject.io/api/auth/azure/index.html#configure). + +~> **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_auth_backend" "example" { + type = "azure" +} + +resource "vault_azure_auth_backend_config" "example" { + backend = "${vault_auth_backend.example.path}" + tenant_id = "11111111-2222-3333-4444-555555555555" + client_id = "11111111-2222-3333-4444-555555555555" + client_secret = "01234567890123456789 + resource = "https://vault.hashicorp.com" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `tenant_id` - (Required) The tenant id for the Azure Active Directory + organization. + +* `resource` - (Required) The configured URL for the application registered in + Azure Active Directory. + +* `backend` - (Optional) The path the Azure auth backend being configured was + mounted at. Defaults to `azure`. + +* `client_id` - (Optional) The client id for credentials to query the Azure APIs. + Currently read permissions to query compute resources are required. + +* `client_secret` - (Optional) The client secret for credentials to query the + Azure APIs. + +* `environment` - (Optional) The Azure cloud environment. Valid values: + AzurePublicCloud, AzureUSGovernmentCloud, AzureChinaCloud, + AzureGermanCloud. Defaults to `AzurePublicCloud`. + + +## Attributes Reference + +No additional attributes are exported by this resource. + +## Import + +Azure auth backends can be imported using `auth/`, the `backend` path, and `/config` e.g. + +``` +$ terraform import vault_azure_auth_backend_config.example auth/azure/config +``` diff --git a/website/docs/r/azure_auth_backend_role.html.md b/website/docs/r/azure_auth_backend_role.html.md new file mode 100644 index 000000000..37df26212 --- /dev/null +++ b/website/docs/r/azure_auth_backend_role.html.md @@ -0,0 +1,91 @@ +--- +layout: "vault" +page_title: "Vault: vault_azure_auth_backend_role resource" +sidebar_current: "docs-vault-resource-azure-auth-backend-role" +description: |- + Manages Azure auth backend roles in Vault. +--- + +# vault\_azure\_auth\_backend\_role + +Manages an Azure auth backend role in a Vault server. Roles constrain the +instances or principals that can perform the login operation against the +backend. See the [Vault +documentation](https://www.vaultproject.io/docs/auth/azure.html) for more +information. + +## Example Usage + +```hcl +resource "vault_auth_backend" "azure" { + type = "azure" +} + +resource "vault_azure_auth_backend_role" "example" { + backend = "${vault_auth_backend.azure.path}" + role = "test-role" + bound_subscription_ids = ["11111111-2222-3333-4444-555555555555"] + bound_resource_groups = ["123456789012"] + ttl = 60 + max_ttl = 120 + policies = ["default", "dev", "prod"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `role` - (Required) The name of the role. + +* `bound_service_principal_ids` - (Optional) If set, defines a constraint on the + service principals that can perform the login operation that they should be posess + the ids specified by this field. + +* `bound_group_ids` - (Optional) If set, defines a constraint on the groups + that can perform the login operation that they should be using the group + ID specified by this field. + +* `bound_locations` - (Optional) If set, defines a constraint on the virtual machines + that can perform the login operation that the location in their identity + document must match the one specified by this field. + +* `bound_subscription_ids` - (Optional) If set, defines a constraint on the subscriptions + that can perform the login operation to ones which matches the value specified by this + field. + +* `bound_resource_groups` - (Optional) If set, defines a constraint on the virtual + machiness that can perform the login operation that they be associated with + the resource group that matches the value specified by this field. + +* `bound_scale_sets` - (Optional) If set, defines a constraint on the virtual + machines that can perform the login operation that they must match the scale set + specified by this field. + +* `ttl` - (Optional) The TTL period of tokens issued using this role, provided + as a number of seconds. + +* `max_ttl` - (Optional) The maximum allowed lifetime of tokens issued using + this role, provided as a number of seconds. + +* `period` - (Optional) If set, indicates that the token generated using this + role should never expire. The token should be renewed within the duration + specified by this value. At each renewal, the token's TTL will be set to the + value of this field. The maximum allowed lifetime of token issued using this + role. Specified as a number of seconds. + +* `policies` - (Optional) An array of strings specifying the policies to be set + on tokens issued using this role. + + +## Attributes Reference + +No additional attributes are exported by this resource. + +## Import + +Azure auth backend roles can be imported using `auth/`, the `backend` path, `/role/`, and the `role` name e.g. + +``` +$ terraform import vault_azure_auth_backend_role.example auth/azure/role/test-role +``` diff --git a/website/vault.erb b/website/vault.erb index dcf4059f9..9926bfbae 100644 --- a/website/vault.erb +++ b/website/vault.erb @@ -99,6 +99,14 @@ vault_aws_secret_backend_role + > + vault_azure_auth_backend_config + + + > + vault_azure_auth_backend_role + + > vault_cert_auth_backend_role