diff --git a/azurerm/helpers/azure/key_vault_child.go b/azurerm/helpers/azure/key_vault_child.go index 386a4e7e8950..d352eda26aae 100644 --- a/azurerm/helpers/azure/key_vault_child.go +++ b/azurerm/helpers/azure/key_vault_child.go @@ -42,6 +42,38 @@ func ParseKeyVaultChildID(id string) (*KeyVaultChildID, error) { return &childId, nil } +func ParseKeyVaultChildIDVersionOptional(id string) (*KeyVaultChildID, error) { + // example: https://tharvey-keyvault.vault.azure.net/type/bird/fdf067c93bbb4b22bff4d8b7a9a56217 + idURL, err := url.ParseRequestURI(id) + if err != nil { + return nil, fmt.Errorf("Cannot parse Azure KeyVault Child Id: %s", err) + } + + path := idURL.Path + + path = strings.TrimPrefix(path, "/") + path = strings.TrimSuffix(path, "/") + + components := strings.Split(path, "/") + + if len(components) != 2 && len(components) != 3 { + return nil, fmt.Errorf("Azure KeyVault Child Id should have 2 or 3 segments, got %d: '%s'", len(components), path) + } + + version := "" + if len(components) == 3 { + version = components[2] + } + + childId := KeyVaultChildID{ + KeyVaultBaseUrl: fmt.Sprintf("%s://%s/", idURL.Scheme, idURL.Host), + Name: components[1], + Version: version, + } + + return &childId, nil +} + func ValidateKeyVaultChildName(v interface{}, k string) (warnings []string, errors []error) { value := v.(string) @@ -72,3 +104,24 @@ func ValidateKeyVaultChildId(i interface{}, k string) (warnings []string, errors return warnings, errors } + +// Unfortunately this can't (easily) go in the Validate package +// since there's a circular reference on this package +func ValidateKeyVaultChildIdVersionOptional(i interface{}, k string) (warnings []string, errors []error) { + if warnings, errors = validation.StringIsNotEmpty(i, k); len(errors) > 0 { + return warnings, errors + } + + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("Expected %s to be a string!", k)) + return warnings, errors + } + + if _, err := ParseKeyVaultChildIDVersionOptional(v); err != nil { + errors = append(errors, fmt.Errorf("Error parsing Key Vault Child ID: %s", err)) + return warnings, errors + } + + return warnings, errors +} diff --git a/azurerm/helpers/azure/key_vault_child_test.go b/azurerm/helpers/azure/key_vault_child_test.go index b8b1e8998aaf..2e5951cc7659 100644 --- a/azurerm/helpers/azure/key_vault_child_test.go +++ b/azurerm/helpers/azure/key_vault_child_test.go @@ -55,6 +55,59 @@ func TestAccAzureRMValidateKeyVaultChildID(t *testing.T) { } } +func TestAccAzureRMValidateKeyVaultChildIDVersionOptional(t *testing.T) { + cases := []struct { + Input string + ExpectError bool + }{ + { + Input: "", + ExpectError: true, + }, + { + Input: "https://my-keyvault.vault.azure.net/secrets", + ExpectError: true, + }, + { + Input: "https://my-keyvault.vault.azure.net/secrets/bird", + ExpectError: false, + }, + { + Input: "https://my-keyvault.vault.azure.net/secrets/bird/fdf067c93bbb4b22bff4d8b7a9a56217", + ExpectError: false, + }, + { + Input: "https://my-keyvault.vault.azure.net/certificates/hello/world", + ExpectError: false, + }, + { + Input: "https://my-keyvault.vault.azure.net/keys/castle/1492", + ExpectError: false, + }, + { + Input: "https://my-keyvault.vault.azure.net/secrets/bird/fdf067c93bbb4b22bff4d8b7a9a56217/XXX", + ExpectError: true, + }, + } + + for _, tc := range cases { + warnings, err := ValidateKeyVaultChildIdVersionOptional(tc.Input, "example") + if err != nil { + if !tc.ExpectError { + t.Fatalf("Got error for input %q: %+v", tc.Input, err) + } + + return + } + + if tc.ExpectError && len(warnings) == 0 { + t.Fatalf("Got no errors for input %q but expected some", tc.Input) + } else if !tc.ExpectError && len(warnings) > 0 { + t.Fatalf("Got %d errors for input %q when didn't expect any", len(warnings), tc.Input) + } + } +} + func TestAccAzureRMKeyVaultChild_parseID(t *testing.T) { cases := []struct { Input string @@ -134,6 +187,90 @@ func TestAccAzureRMKeyVaultChild_parseID(t *testing.T) { } } +func TestAccAzureRMKeyVaultChild_parseIDVersionOptional(t *testing.T) { + cases := []struct { + Input string + Expected KeyVaultChildID + ExpectError bool + }{ + { + Input: "", + ExpectError: true, + }, + { + Input: "https://my-keyvault.vault.azure.net/secrets", + ExpectError: true, + }, + { + Input: "https://my-keyvault.vault.azure.net/secrets/bird", + ExpectError: false, + Expected: KeyVaultChildID{ + Name: "bird", + KeyVaultBaseUrl: "https://my-keyvault.vault.azure.net/", + Version: "", + }, + }, + { + Input: "https://my-keyvault.vault.azure.net/secrets/bird/fdf067c93bbb4b22bff4d8b7a9a56217", + ExpectError: false, + Expected: KeyVaultChildID{ + Name: "bird", + KeyVaultBaseUrl: "https://my-keyvault.vault.azure.net/", + Version: "fdf067c93bbb4b22bff4d8b7a9a56217", + }, + }, + { + Input: "https://my-keyvault.vault.azure.net/certificates/hello/world", + ExpectError: false, + Expected: KeyVaultChildID{ + Name: "hello", + KeyVaultBaseUrl: "https://my-keyvault.vault.azure.net/", + Version: "world", + }, + }, + { + Input: "https://my-keyvault.vault.azure.net/keys/castle/1492", + ExpectError: false, + Expected: KeyVaultChildID{ + Name: "castle", + KeyVaultBaseUrl: "https://my-keyvault.vault.azure.net/", + Version: "1492", + }, + }, + { + Input: "https://my-keyvault.vault.azure.net/secrets/bird/fdf067c93bbb4b22bff4d8b7a9a56217/XXX", + ExpectError: true, + }, + } + + for _, tc := range cases { + secretId, err := ParseKeyVaultChildIDVersionOptional(tc.Input) + if err != nil { + if !tc.ExpectError { + t.Fatalf("Got error for ID '%s': %+v", tc.Input, err) + } + + return + } + + if secretId == nil { + t.Fatalf("Expected a SecretID to be parsed for ID '%s', got nil.", tc.Input) + } + + if tc.Expected.KeyVaultBaseUrl != secretId.KeyVaultBaseUrl { + t.Fatalf("Expected 'KeyVaultBaseUrl' to be '%s', got '%s' for ID '%s'", tc.Expected.KeyVaultBaseUrl, secretId.KeyVaultBaseUrl, tc.Input) + } + + if tc.Expected.Name != secretId.Name { + t.Fatalf("Expected 'Version' to be '%s', got '%s' for ID '%s'", tc.Expected.Name, secretId.Name, tc.Input) + } + + if tc.Expected.Version != secretId.Version { + t.Fatalf("Expected 'Version' to be '%s', got '%s' for ID '%s'", tc.Expected.Version, secretId.Version, tc.Input) + } + } +} + func TestAccAzureRMKeyVaultChild_validateName(t *testing.T) { cases := []struct { Input string diff --git a/azurerm/internal/services/apimanagement/api_management_resource.go b/azurerm/internal/services/apimanagement/api_management_resource.go index d75305d26b80..75c373d17bb6 100644 --- a/azurerm/internal/services/apimanagement/api_management_resource.go +++ b/azurerm/internal/services/apimanagement/api_management_resource.go @@ -1139,7 +1139,7 @@ func apiManagementResourceHostnameSchema(schemaName string) map[string]*schema.S "key_vault_id": { Type: schema.TypeString, Optional: true, - ValidateFunc: azure.ValidateKeyVaultChildId, + ValidateFunc: azure.ValidateKeyVaultChildIdVersionOptional, ConflictsWith: []string{ fmt.Sprintf("hostname_configuration.0.%s.0.certificate", schemaName), fmt.Sprintf("hostname_configuration.0.%s.0.certificate_password", schemaName), diff --git a/azurerm/internal/services/apimanagement/tests/api_management_resource_test.go b/azurerm/internal/services/apimanagement/tests/api_management_resource_test.go index 959578090643..09ccdcf49f66 100644 --- a/azurerm/internal/services/apimanagement/tests/api_management_resource_test.go +++ b/azurerm/internal/services/apimanagement/tests/api_management_resource_test.go @@ -191,6 +191,58 @@ func TestAccAzureRMApiManagement_virtualNetworkInternal(t *testing.T) { }) } +func TestAccAzureRMApiManagement_identitySystemAssignedUpdateHostnameConfigurationsVersionedKeyVaultId(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identitySystemAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagement_identitySystemAssignedUpdateHostnameConfigurationsVersionedKeyVaultId(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identitySystemAssignedUpdateHostnameConfigurationsVersionlessKeyVaultId(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identitySystemAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagement_identitySystemAssignedUpdateHostnameConfigurationsVersionlessKeyVaultId(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + func testCheckAzureRMApiManagementDestroy(s *terraform.State) error { conn := acceptance.AzureProvider.Meta().(*clients.Client).ApiManagement.ServiceClient ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext @@ -889,21 +941,17 @@ func testAccAzureRMApiManagement_identitySystemAssigned(data acceptance.TestData provider "azurerm" { features {} } - resource "azurerm_resource_group" "test" { name = "acctestRG-%d" location = "%s" } - resource "azurerm_api_management" "test" { name = "acctestAM-%d" location = azurerm_resource_group.test.location resource_group_name = azurerm_resource_group.test.name publisher_name = "pub1" publisher_email = "pub1@email.com" - - sku_name = "Developer_1" - + sku_name = "Developer_1" identity { type = "SystemAssigned" } @@ -973,3 +1021,223 @@ resource "azurerm_api_management" "test" { } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } + +func testAccAzureRMApiManagement_identitySystemAssignedUpdateHostnameConfigurationsVersionlessKeyVaultId(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} +data "azurerm_client_config" "current" {} +resource "azurerm_key_vault" "test" { + name = "acctestKV-%[3]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" +} +resource "azurerm_key_vault_access_policy" "test" { + key_vault_id = azurerm_key_vault.test.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + certificate_permissions = [ + "Create", + "Delete", + "Deleteissuers", + "Get", + "Getissuers", + "Import", + "List", + "Listissuers", + "Managecontacts", + "Manageissuers", + "Setissuers", + "Update", + ] + secret_permissions = [ + "Delete", + "Get", + "List", + "Purge", + ] +} +resource "azurerm_key_vault_access_policy" "test2" { + key_vault_id = azurerm_key_vault.test.id + tenant_id = azurerm_api_management.test.identity[0].tenant_id + object_id = azurerm_api_management.test.identity[0].principal_id + secret_permissions = [ + "Get", + "List", + ] +} +resource "azurerm_key_vault_certificate" "test" { + depends_on = [azurerm_key_vault_access_policy.test] + name = "acctestKVCert-%[3]d" + key_vault_id = azurerm_key_vault.test.id + certificate_policy { + issuer_parameters { + name = "Self" + } + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = true + } + secret_properties { + content_type = "application/x-pkcs12" + } + x509_certificate_properties { + # Server Authentication = 1.3.6.1.5.5.7.3.1 + # Client Authentication = 1.3.6.1.5.5.7.3.2 + extended_key_usage = ["1.3.6.1.5.5.7.3.1"] + key_usage = [ + "cRLSign", + "dataEncipherment", + "digitalSignature", + "keyAgreement", + "keyCertSign", + "keyEncipherment", + ] + subject_alternative_names { + dns_names = ["api.terraform.io"] + } + subject = "CN=api.terraform.io" + validity_in_months = 1 + } + } +} +resource "azurerm_api_management" "test" { + name = "acctestAM-%[3]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + publisher_name = "pub1" + publisher_email = "pub1@email.com" + sku_name = "Developer_1" + identity { + type = "SystemAssigned" + } + hostname_configuration { + proxy { + host_name = "api.terraform.io" + key_vault_id = "${azurerm_key_vault.test.vault_uri}secrets/${azurerm_key_vault_certificate.test.name}" + default_ssl_binding = true + negotiate_client_certificate = false + } + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + +func testAccAzureRMApiManagement_identitySystemAssignedUpdateHostnameConfigurationsVersionedKeyVaultId(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} +data "azurerm_client_config" "current" {} +resource "azurerm_key_vault" "test" { + name = "acctestKV-%[3]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" +} +resource "azurerm_key_vault_access_policy" "test" { + key_vault_id = azurerm_key_vault.test.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + certificate_permissions = [ + "Create", + "Delete", + "Deleteissuers", + "Get", + "Getissuers", + "Import", + "List", + "Listissuers", + "Managecontacts", + "Manageissuers", + "Setissuers", + "Update", + ] + secret_permissions = [ + "Delete", + "Get", + "List", + "Purge", + ] +} +resource "azurerm_key_vault_access_policy" "test2" { + key_vault_id = azurerm_key_vault.test.id + tenant_id = azurerm_api_management.test.identity[0].tenant_id + object_id = azurerm_api_management.test.identity[0].principal_id + secret_permissions = [ + "Get", + "List", + ] +} +resource "azurerm_key_vault_certificate" "test" { + depends_on = [azurerm_key_vault_access_policy.test] + name = "acctestKVCert-%[3]d" + key_vault_id = azurerm_key_vault.test.id + certificate_policy { + issuer_parameters { + name = "Self" + } + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = true + } + secret_properties { + content_type = "application/x-pkcs12" + } + x509_certificate_properties { + # Server Authentication = 1.3.6.1.5.5.7.3.1 + # Client Authentication = 1.3.6.1.5.5.7.3.2 + extended_key_usage = ["1.3.6.1.5.5.7.3.1"] + key_usage = [ + "cRLSign", + "dataEncipherment", + "digitalSignature", + "keyAgreement", + "keyCertSign", + "keyEncipherment", + ] + subject_alternative_names { + dns_names = ["api.terraform.io"] + } + subject = "CN=api.terraform.io" + validity_in_months = 1 + } + } +} +resource "azurerm_api_management" "test" { + name = "acctestAM-%[3]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + publisher_name = "pub1" + publisher_email = "pub1@email.com" + sku_name = "Developer_1" + identity { + type = "SystemAssigned" + } + hostname_configuration { + proxy { + host_name = "api.terraform.io" + key_vault_id = azurerm_key_vault_certificate.test.secret_id + default_ssl_binding = true + negotiate_client_certificate = false + } + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +}