Skip to content

Commit

Permalink
Support for identity_client_id and certificate_information in `az…
Browse files Browse the repository at this point in the history
…urerm_api_management` (#12262)

Fix: #12260

Previously api mgmt service doesn't support user assigned identity in hostname_configuration, which caused we need separate steps for applying a key vault certificate in hostname_configuration. Now this could be solved.

certificate will return some basic certificate information (expiry, subject, thumbprint) for every certificate.

--- PASS: TestAccApiManagement_complete (2429.80s)
--- PASS: TestAccApiManagement_identityUserAssignedHostnameConfigurationsVersionedKeyVaultId (4682.85s)
  • Loading branch information
yupwei68 authored Aug 27, 2021
1 parent 2d5ebfa commit 24e0c9c
Show file tree
Hide file tree
Showing 4 changed files with 302 additions and 1 deletion.
80 changes: 80 additions & 0 deletions internal/services/apimanagement/api_management_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,21 @@ func resourceApiManagementService() *pluginsdk.Resource {
string(apimanagement.Root),
}, false),
},

"expiry": {
Type: pluginsdk.TypeString,
Computed: true,
},

"subject": {
Type: pluginsdk.TypeString,
Computed: true,
},

"thumbprint": {
Type: pluginsdk.TypeString,
Computed: true,
},
},
},
},
Expand Down Expand Up @@ -864,6 +879,8 @@ func resourceApiManagementServiceRead(d *pluginsdk.ResourceData, meta interface{
d.Set("client_certificate_enabled", props.EnableClientCertificate)
d.Set("gateway_disabled", props.DisableGateway)

d.Set("certificate", flattenAPIManagementCertificates(d, props.Certificates))

if resp.Sku != nil && resp.Sku.Name != "" {
if err := d.Set("security", flattenApiManagementSecurityCustomProperties(props.CustomProperties, resp.Sku.Name == apimanagement.SkuTypeConsumption)); err != nil {
return fmt.Errorf("setting `security`: %+v", err)
Expand Down Expand Up @@ -1075,6 +1092,10 @@ func expandApiManagementCommonHostnameConfiguration(input map[string]interface{}
output.NegotiateClientCertificate = utils.Bool(v.(bool))
}

if v, ok := input["ssl_keyvault_identity_client_id"].(string); ok && v != "" {
output.IdentityClientID = utils.String(v)
}

return output
}

Expand Down Expand Up @@ -1110,6 +1131,24 @@ func flattenApiManagementHostnameConfigurations(input *[]apimanagement.HostnameC
output["key_vault_id"] = *config.KeyVaultID
}

if config.IdentityClientID != nil {
output["ssl_keyvault_identity_client_id"] = *config.IdentityClientID
}

if config.Certificate != nil {
if config.Certificate.Expiry != nil && !config.Certificate.Expiry.IsZero() {
output["expiry"] = config.Certificate.Expiry.Format(time.RFC3339)
}

if config.Certificate.Thumbprint != nil {
output["thumbprint"] = *config.Certificate.Thumbprint
}

if config.Certificate.Subject != nil {
output["subject"] = *config.Certificate.Subject
}
}

var configType string
switch strings.ToLower(string(config.Type)) {
case strings.ToLower(string(apimanagement.HostnameTypeProxy)):
Expand Down Expand Up @@ -1750,3 +1789,44 @@ func flattenApiManagementTenantAccessSettings(input apimanagement.AccessInformat

return []interface{}{result}
}

func flattenAPIManagementCertificates(d *pluginsdk.ResourceData, inputs *[]apimanagement.CertificateConfiguration) []interface{} {
if inputs == nil || len(*inputs) == 0 {
return []interface{}{}
}

outputs := []interface{}{}
for i, input := range *inputs {
var expiry, subject, thumbprint, pwd, encodedCertificate string
if v, ok := d.GetOk(fmt.Sprintf("certificate.%d.certificate_password", i)); ok {
pwd = v.(string)
}

if v, ok := d.GetOk(fmt.Sprintf("certificate.%d.encoded_certificate", i)); ok {
encodedCertificate = v.(string)
}

if input.Certificate.Expiry != nil && !input.Certificate.Expiry.IsZero() {
expiry = input.Certificate.Expiry.Format(time.RFC3339)
}

if input.Certificate.Thumbprint != nil {
thumbprint = *input.Certificate.Thumbprint
}

if input.Certificate.Subject != nil {
subject = *input.Certificate.Subject
}

output := map[string]interface{}{
"certificate_password": pwd,
"encoded_certificate": encodedCertificate,
"store_name": string(input.StoreName),
"expiry": expiry,
"subject": subject,
"thumbprint": thumbprint,
}
outputs = append(outputs, output)
}
return outputs
}
177 changes: 176 additions & 1 deletion internal/services/apimanagement/api_management_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,43 @@ func TestAccApiManagement_complete(t *testing.T) {
Config: r.complete(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("certificate.0.expiry").Exists(),
check.That(data.ResourceName).Key("certificate.0.subject").Exists(),
check.That(data.ResourceName).Key("certificate.0.thumbprint").Exists(),
check.That(data.ResourceName).Key("certificate.1.expiry").Exists(),
check.That(data.ResourceName).Key("certificate.1.subject").Exists(),
check.That(data.ResourceName).Key("certificate.1.thumbprint").Exists(),
check.That(data.ResourceName).Key("certificate.2.expiry").Exists(),
check.That(data.ResourceName).Key("certificate.2.subject").Exists(),
check.That(data.ResourceName).Key("certificate.2.thumbprint").Exists(),
check.That(data.ResourceName).Key("certificate.3.expiry").Exists(),
check.That(data.ResourceName).Key("certificate.3.subject").Exists(),
check.That(data.ResourceName).Key("certificate.3.thumbprint").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.developer_portal.0.expiry").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.developer_portal.0.subject").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.developer_portal.0.thumbprint").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.portal.0.expiry").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.portal.0.subject").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.portal.0.thumbprint").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.proxy.0.expiry").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.proxy.0.subject").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.proxy.0.thumbprint").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.proxy.1.expiry").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.proxy.1.subject").Exists(),
check.That(data.ResourceName).Key("hostname_configuration.0.proxy.1.thumbprint").Exists(),
),
},
{
ResourceName: data.ResourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"certificate", // not returned from API, sensitive
"certificate.0.certificate_password",
"certificate.0.encoded_certificate",
"certificate.1.certificate_password",
"certificate.1.encoded_certificate",
"certificate.2.encoded_certificate",
"certificate.3.encoded_certificate",
"hostname_configuration.0.portal.0.certificate", // not returned from API, sensitive
"hostname_configuration.0.portal.0.certificate_password", // not returned from API, sensitive
"hostname_configuration.0.developer_portal.0.certificate", // not returned from API, sensitive
Expand Down Expand Up @@ -317,6 +346,21 @@ func TestAccApiManagement_identitySystemAssignedUpdateHostnameConfigurationsVers
})
}

func TestAccApiManagement_identityUserAssignedHostnameConfigurationsKeyVaultId(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_api_management", "test")
r := ApiManagementResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.identityUserAssignedHostnameConfigurationsKeyVaultId(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
})
}

func TestAccApiManagement_consumption(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_api_management", "test")
r := ApiManagementResource{}
Expand Down Expand Up @@ -1653,6 +1697,137 @@ resource "azurerm_api_management" "test" {
`, r.identitySystemAssignedUpdateHostnameConfigurationsTemplate(data), data.RandomInteger)
}

func (ApiManagementResource) identityUserAssignedHostnameConfigurationsKeyVaultId(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]s"
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",
"Purge",
]
secret_permissions = [
"Delete",
"Get",
"List",
"Purge",
]
}
resource "azurerm_user_assigned_identity" "test" {
name = "acctestUAI-%[1]d"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
}
resource "azurerm_key_vault_access_policy" "test2" {
key_vault_id = azurerm_key_vault.test.id
tenant_id = azurerm_user_assigned_identity.test.tenant_id
object_id = azurerm_user_assigned_identity.test.principal_id
secret_permissions = [
"Get",
"List",
]
}
resource "azurerm_key_vault_certificate" "test" {
depends_on = [azurerm_key_vault_access_policy.test]
name = "acctestKVCert-%[1]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-%[1]d"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
publisher_name = "pub1"
publisher_email = "[email protected]"
sku_name = "Developer_1"
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
ssl_keyvault_identity_client_id = azurerm_user_assigned_identity.test.client_id
}
}
identity {
type = "UserAssigned"
identity_ids = [
azurerm_user_assigned_identity.test.id,
]
}
depends_on = [azurerm_key_vault_access_policy.test2]
}
`, data.RandomInteger, data.Locations.Primary, data.RandomString)
}

func (ApiManagementResource) consumption(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
Expand Down
21 changes: 21 additions & 0 deletions internal/services/apimanagement/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ func apiManagementResourceHostnameSchema() map[string]*pluginsdk.Schema {
Optional: true,
Default: false,
},

"ssl_keyvault_identity_client_id": {
Type: pluginsdk.TypeString,
Optional: true,
ValidateFunc: validation.IsUUID,
},

"expiry": {
Type: pluginsdk.TypeString,
Computed: true,
},

"subject": {
Type: pluginsdk.TypeString,
Computed: true,
},

"thumbprint": {
Type: pluginsdk.TypeString,
Computed: true,
},
}
}

Expand Down
25 changes: 25 additions & 0 deletions website/docs/r/api_management.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ A `management`, `portal`, `developer_portal` and `scm` block supports the follow

* `negotiate_client_certificate` - (Optional) Should Client Certificate Negotiation be enabled for this Hostname? Defaults to `false`.

* `ssl_keyvault_identity_client_id` - (Optional) The client id of the System or User Assigned Managed identity generated by Azure AD, which has `GET` access to the keyVault containing the SSL certificate.

-> **NOTE:** If User Assigned Managed identity is used in this field, please assign User Assigned Managed identity to the `azurerm_api_management` as well.

---

A `policy` block supports the following:
Expand Down Expand Up @@ -373,6 +377,27 @@ A `tenant_access` block exports the following:

* `secondary_key` - Secondary access key for the tenant access information contract.

---

The `certificate` block exports the following:

* `expiry` - The expiration date of the certificate in RFC3339 format: `2000-01-02T03:04:05Z`.

* `thumbprint` - The thumbprint of the certificate.

* `subject` - The subject of the certificate.

---

The `hostname_configuration` block exports the following:

* `expiry` - The expiration date of the certificate in RFC3339 format: `2000-01-02T03:04:05Z`.

* `thumbprint` - The thumbprint of the certificate.

* `subject` - The subject of the certificate.


## Timeouts

The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions:
Expand Down

0 comments on commit 24e0c9c

Please sign in to comment.