Skip to content

Commit

Permalink
New resource: azurerm_iothub_shared_access_policy (#3009)
Browse files Browse the repository at this point in the history
(resolves #2201)
  • Loading branch information
maxbog authored and katbyte committed Apr 9, 2019
1 parent d61190c commit 72f11a3
Show file tree
Hide file tree
Showing 6 changed files with 686 additions and 1 deletion.
1 change: 1 addition & 0 deletions azurerm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ func Provider() terraform.ResourceProvider {
"azurerm_image": resourceArmImage(),
"azurerm_iothub_consumer_group": resourceArmIotHubConsumerGroup(),
"azurerm_iothub": resourceArmIotHub(),
"azurerm_iothub_shared_access_policy": resourceArmIotHubSharedAccessPolicy(),
"azurerm_key_vault_access_policy": resourceArmKeyVaultAccessPolicy(),
"azurerm_key_vault_certificate": resourceArmKeyVaultCertificate(),
"azurerm_key_vault_key": resourceArmKeyVaultKey(),
Expand Down
8 changes: 8 additions & 0 deletions azurerm/resource_arm_iothub.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

var iothubResourceName = "azurerm_iothub"

func suppressIfTypeIsNot(t string) schema.SchemaDiffSuppressFunc {
return func(k, old, new string, d *schema.ResourceData) bool {
path := strings.Split(k, ".")
Expand Down Expand Up @@ -364,6 +366,9 @@ func resourceArmIotHubCreateUpdate(d *schema.ResourceData, meta interface{}) err
name := d.Get("name").(string)
resourceGroup := d.Get("resource_group_name").(string)

azureRMLockByName(name, iothubResourceName)
defer azureRMUnlockByName(name, iothubResourceName)

if requireResourcesToBeImported && d.IsNewResource() {
existing, err := client.Get(ctx, resourceGroup, name)
if err != nil {
Expand Down Expand Up @@ -540,6 +545,9 @@ func resourceArmIotHubDelete(d *schema.ResourceData, meta interface{}) error {
name := id.Path["IotHubs"]
resourceGroup := id.ResourceGroup

azureRMLockByName(name, iothubResourceName)
defer azureRMUnlockByName(name, iothubResourceName)

future, err := client.Delete(ctx, resourceGroup, name)
if err != nil {
if response.WasNotFound(future.Response()) {
Expand Down
355 changes: 355 additions & 0 deletions azurerm/resource_arm_iothub_shared_access_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
package azurerm

import (
"fmt"
"log"
"regexp"
"strings"

"github.com/Azure/azure-sdk-for-go/services/preview/iothub/mgmt/2018-12-01-preview/devices"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

func resourceArmIotHubSharedAccessPolicy() *schema.Resource {
return &schema.Resource{
Create: resourceArmIotHubSharedAccessPolicyCreateUpdate,
Read: resourceArmIotHubSharedAccessPolicyRead,
Update: resourceArmIotHubSharedAccessPolicyCreateUpdate,
Delete: resourceArmIotHubSharedAccessPolicyDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringMatch(regexp.MustCompile(`[a-zA-Z0-9!._-]{1,64}`), ""+
"The shared access policy key name must not be empty, and must not exceed 64 characters in length. The shared access policy key name can only contain alphanumeric characters, exclamation marks, periods, underscores and hyphens."),
},

"resource_group_name": resourceGroupNameSchema(),

"iothub_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.IoTHubName,
},

"registry_read": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},

"registry_write": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},

"service_connect": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},

"device_connect": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},

"primary_key": {
Type: schema.TypeString,
Sensitive: true,
Computed: true,
},

"primary_connection_string": {
Type: schema.TypeString,
Sensitive: true,
Computed: true,
},

"secondary_key": {
Type: schema.TypeString,
Sensitive: true,
Computed: true,
},

"secondary_connection_string": {
Type: schema.TypeString,
Sensitive: true,
Computed: true,
},
},
CustomizeDiff: iothubSharedAccessPolicyCustomizeDiff,
}
}

func iothubSharedAccessPolicyCustomizeDiff(d *schema.ResourceDiff, _ interface{}) (err error) {
registryRead, hasRegistryRead := d.GetOk("registry_read")
registryWrite, hasRegistryWrite := d.GetOk("registry_write")
serviceConnect, hasServieConnect := d.GetOk("service_connect")
deviceConnect, hasDeviceConnect := d.GetOk("device_connect")

if !hasRegistryRead && !hasRegistryWrite && !hasServieConnect && !hasDeviceConnect {
return fmt.Errorf("One of `registry_read`, `registry_write`, `service_connect` or `device_connect` properties must be set")
}

if !registryRead.(bool) && !registryWrite.(bool) && !serviceConnect.(bool) && !deviceConnect.(bool) {
err = multierror.Append(err, fmt.Errorf("At least one of `registry_read`, `registry_write`, `service_connect` or `device_connect` properties must be set to true"))
}

if registryWrite.(bool) && !registryRead.(bool) {
err = multierror.Append(err, fmt.Errorf("If `registry_write` is set to true, `registry_read` must also be set to true"))
}

return
}

func resourceArmIotHubSharedAccessPolicyCreateUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).iothubResourceClient
ctx := meta.(*ArmClient).StopContext

iothubName := d.Get("iothub_name").(string)
resourceGroup := d.Get("resource_group_name").(string)

azureRMLockByName(iothubName, iothubResourceName)
defer azureRMUnlockByName(iothubName, iothubResourceName)

iothub, err := client.Get(ctx, resourceGroup, iothubName)
if err != nil {
if utils.ResponseWasNotFound(iothub.Response) {
return fmt.Errorf("IotHub %q (Resource Group %q) was not found", iothubName, resourceGroup)
}

return fmt.Errorf("Error loading IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err)
}

keyName := d.Get("name").(string)

resourceId := fmt.Sprintf("%s/IotHubKeys/%s", *iothub.ID, keyName)

expandedAccessPolicy := devices.SharedAccessSignatureAuthorizationRule{
KeyName: &keyName,
Rights: devices.AccessRights(expandAccessRights(d)),
}

accessPolicies := make([]devices.SharedAccessSignatureAuthorizationRule, 0)

alreadyExists := false
for accessPolicyIterator, err := client.ListKeysComplete(ctx, resourceGroup, iothubName); accessPolicyIterator.NotDone(); err = accessPolicyIterator.NextWithContext(ctx) {
if err != nil {
return fmt.Errorf("Error loading Shared Access Profiles of IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err)
}
existingAccessPolicy := accessPolicyIterator.Value()

if strings.EqualFold(*existingAccessPolicy.KeyName, keyName) {
if d.IsNewResource() && requireResourcesToBeImported {
return tf.ImportAsExistsError("azurerm_iothub_shared_access_policy", resourceId)
}
accessPolicies = append(accessPolicies, expandedAccessPolicy)
alreadyExists = true
} else {
accessPolicies = append(accessPolicies, existingAccessPolicy)
}
}

if d.IsNewResource() {
accessPolicies = append(accessPolicies, expandedAccessPolicy)
} else if !alreadyExists {
return fmt.Errorf("Unable to find Shared Access Policy %q defined for IotHub %q (Resource Group %q)", keyName, iothubName, resourceGroup)
}

iothub.Properties.AuthorizationPolicies = &accessPolicies

future, err := client.CreateOrUpdate(ctx, resourceGroup, iothubName, iothub, "")
if err != nil {
return fmt.Errorf("Error updating IotHub %q (Resource Group %q) with Shared Access Profile %q: %+v", iothubName, resourceGroup, keyName, err)
}

if err = future.WaitForCompletionRef(ctx, client.Client); err != nil {
return fmt.Errorf("Error waiting for IotHub %q (Resource Group %q) to finish updating Shared Access Profile %q: %+v", iothubName, resourceGroup, keyName, err)
}

d.SetId(resourceId)

return resourceArmIotHubSharedAccessPolicyRead(d, meta)
}

func resourceArmIotHubSharedAccessPolicyRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).iothubResourceClient
ctx := meta.(*ArmClient).StopContext

parsedIothubSAPId, err := parseAzureResourceID(d.Id())

if err != nil {
return err
}

resourceGroup := parsedIothubSAPId.ResourceGroup
iothubName := parsedIothubSAPId.Path["IotHubs"]
keyName := parsedIothubSAPId.Path["IotHubKeys"]

accessPolicy, err := client.GetKeysForKeyName(ctx, resourceGroup, iothubName, keyName)
if err != nil {
if utils.ResponseWasNotFound(accessPolicy.Response) {
log.Printf("[DEBUG] Shared Access Policy %q was not found on IotHub %q (Resource Group %q) - removing from state", keyName, iothubName, resourceGroup)
d.SetId("")
return nil
}

return fmt.Errorf("Error loading IotHub Shared Access Policy %q (Resource Group %q): %+v", iothubName, resourceGroup, err)
}

iothub, err := client.Get(ctx, resourceGroup, iothubName)
if err != nil {
return fmt.Errorf("Error loading IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err)
}

d.Set("name", keyName)
d.Set("iothub_name", iothubName)
d.Set("resource_group_name", resourceGroup)

d.Set("primary_key", accessPolicy.PrimaryKey)
if err := d.Set("primary_connection_string", getSharedAccessPolicyConnectionString(*iothub.Properties.HostName, keyName, *accessPolicy.PrimaryKey)); err != nil {
return fmt.Errorf("error setting `primary_connection_string`: %v", err)
}
d.Set("secondary_key", accessPolicy.SecondaryKey)
if err := d.Set("secondary_connection_string", getSharedAccessPolicyConnectionString(*iothub.Properties.HostName, keyName, *accessPolicy.SecondaryKey)); err != nil {
return fmt.Errorf("error setting `secondary_connection_string`: %v", err)
}

rights := flattenAccessRights(accessPolicy.Rights)
d.Set("registry_read", rights.registryRead)
d.Set("registry_write", rights.registryWrite)
d.Set("device_connect", rights.deviceConnect)
d.Set("service_connect", rights.serviceConnect)

return nil
}

func resourceArmIotHubSharedAccessPolicyDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).iothubResourceClient
ctx := meta.(*ArmClient).StopContext

parsedIothubSAPId, err := parseAzureResourceID(d.Id())

if err != nil {
return err
}

resourceGroup := parsedIothubSAPId.ResourceGroup
iothubName := parsedIothubSAPId.Path["IotHubs"]
keyName := parsedIothubSAPId.Path["IotHubKeys"]

azureRMLockByName(iothubName, iothubResourceName)
defer azureRMUnlockByName(iothubName, iothubResourceName)

iothub, err := client.Get(ctx, resourceGroup, iothubName)
if err != nil {
if utils.ResponseWasNotFound(iothub.Response) {
return fmt.Errorf("IotHub %q (Resource Group %q) was not found", iothubName, resourceGroup)
}

return fmt.Errorf("Error loading IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err)
}

accessPolicies := make([]devices.SharedAccessSignatureAuthorizationRule, 0)

for accessPolicyIterator, err := client.ListKeysComplete(ctx, resourceGroup, iothubName); accessPolicyIterator.NotDone(); err = accessPolicyIterator.NextWithContext(ctx) {
if err != nil {
return fmt.Errorf("Error loading Shared Access Profiles of IotHub %q (Resource Group %q): %+v", iothubName, resourceGroup, err)
}
existingAccessPolicy := accessPolicyIterator.Value()

if !strings.EqualFold(*existingAccessPolicy.KeyName, keyName) {
accessPolicies = append(accessPolicies, existingAccessPolicy)
}
}

iothub.Properties.AuthorizationPolicies = &accessPolicies

future, err := client.CreateOrUpdate(ctx, resourceGroup, iothubName, iothub, "")
if err != nil {
return fmt.Errorf("Error updating IotHub %q (Resource Group %q) with Shared Access Profile %q: %+v", iothubName, resourceGroup, keyName, err)
}

if err = future.WaitForCompletionRef(ctx, client.Client); err != nil {
return fmt.Errorf("Error waiting for IotHub %q (Resource Group %q) to finish updating Shared Access Profile %q: %+v", iothubName, resourceGroup, keyName, err)
}

return nil
}

type accessRights struct {
registryRead bool
registryWrite bool
serviceConnect bool
deviceConnect bool
}

func expandAccessRights(d *schema.ResourceData) string {

var possibleAccessRights = []struct {
schema string
right string
}{
{"registry_read", "RegistryRead"},
{"registry_write", "RegistryWrite"},
{"service_connect", "ServiceConnect"},
{"device_connect", "DeviceConnect"},
}
actualRights := make([]string, 0)
// iteration order is important here, so we cannot use a map
for _, possibleRight := range possibleAccessRights {
if d.Get(possibleRight.schema).(bool) {
actualRights = append(actualRights, possibleRight.right)
}
}
strRights := strings.Join(actualRights, ", ")
return strRights
}

func flattenAccessRights(r devices.AccessRights) accessRights {
rights := accessRights{
registryRead: false,
registryWrite: false,
deviceConnect: false,
serviceConnect: false,
}

actualAccessRights := strings.Split(string(r), ",")

for _, right := range actualAccessRights {
switch strings.ToLower(strings.Trim(right, " ")) {
case "registrywrite":
rights.registryWrite = true
// RegistryWrite implies RegistryRead.
// What's more, creating a Access Policy with both RegistryRead and RegistryWrite
// only really sets RegistryWrite permission, which then also implies RedistryRead
fallthrough
case "registryread":
rights.registryRead = true
case "deviceconnect":
rights.deviceConnect = true
case "serviceconnect":
rights.serviceConnect = true
}
}

return rights
}

func getSharedAccessPolicyConnectionString(iothubHostName string, keyName string, key string) string {
return fmt.Sprintf("HostName=%s;SharedAccessKeyName=%s;SharedAccessKey=%s", iothubHostName, keyName, key)
}
Loading

0 comments on commit 72f11a3

Please sign in to comment.