diff --git a/internal/services/managedhsm/client/helpers.go b/internal/services/managedhsm/client/helpers.go new file mode 100644 index 000000000000..112c6e5c6417 --- /dev/null +++ b/internal/services/managedhsm/client/helpers.go @@ -0,0 +1,169 @@ +package client + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/keyvault/2023-07-01/managedhsms" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" +) + +var ( + cache = map[string]managedHSMDetail{} + keysmith = &sync.RWMutex{} + lock = map[string]*sync.RWMutex{} +) + +type managedHSMDetail struct { + managedHSMId managedhsms.ManagedHSMId + dataPlaneBaseUri string +} + +func (c *Client) AddToCache(managedHsmId managedhsms.ManagedHSMId, dataPlaneUri string) { + cacheKey := c.cacheKeyForManagedHSM(managedHsmId.ManagedHSMName) + keysmith.Lock() + cache[cacheKey] = managedHSMDetail{ + managedHSMId: managedHsmId, + dataPlaneBaseUri: dataPlaneUri, + } + keysmith.Unlock() +} + +func (c *Client) BaseUriForManagedHSM(ctx context.Context, managedHsmId managedhsms.ManagedHSMId) (*string, error) { + cacheKey := c.cacheKeyForManagedHSM(managedHsmId.ManagedHSMName) + keysmith.Lock() + if lock[cacheKey] == nil { + lock[cacheKey] = &sync.RWMutex{} + } + keysmith.Unlock() + lock[cacheKey].Lock() + defer lock[cacheKey].Unlock() + + if v, ok := cache[cacheKey]; ok { + return &v.dataPlaneBaseUri, nil + } + + resp, err := c.ManagedHsmClient.Get(ctx, managedHsmId) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return nil, fmt.Errorf("%s was not found", managedHsmId) + } + return nil, fmt.Errorf("retrieving %s: %+v", managedHsmId, err) + } + + dataPlaneUri := "" + if model := resp.Model; model != nil { + if model.Properties.HsmUri != nil { + dataPlaneUri = *model.Properties.HsmUri + } + } + if dataPlaneUri == "" { + return nil, fmt.Errorf("retrieving %s: `properties.HsmUri` was nil", managedHsmId) + } + + c.AddToCache(managedHsmId, dataPlaneUri) + return &dataPlaneUri, nil +} + +func (c *Client) Purge(managedHSMId managedhsms.ManagedHSMId) { + cacheKey := c.cacheKeyForManagedHSM(managedHSMId.ManagedHSMName) + keysmith.Lock() + if lock[cacheKey] == nil { + lock[cacheKey] = &sync.RWMutex{} + } + keysmith.Unlock() + lock[cacheKey].Lock() + delete(cache, cacheKey) + lock[cacheKey].Unlock() +} + +func (c *Client) ManagedHSMIDFromBaseUrl(ctx context.Context, subscriptionId commonids.SubscriptionId, managedHsmBaseUrl string, domainSuffix *string) (*managedhsms.ManagedHSMId, error) { + endpoint, err := parse.ManagedHSMEndpoint(managedHsmBaseUrl, domainSuffix) + if err != nil { + return nil, err + } + + cacheKey := c.cacheKeyForManagedHSM(endpoint.ManagedHSMName) + keysmith.Lock() + if lock[cacheKey] == nil { + lock[cacheKey] = &sync.RWMutex{} + } + keysmith.Unlock() + lock[cacheKey].Lock() + defer lock[cacheKey].Unlock() + + // Check the cache to determine if we have an entry for this Managed HSM + if v, ok := cache[cacheKey]; ok { + return &v.managedHSMId, nil + } + + // Populate the cache if not found + if err = c.populateCache(ctx, subscriptionId); err != nil { + return nil, err + } + + // Now that the cache has been repopulated, check if we have the Managed HSM or not + if v, ok := cache[cacheKey]; ok { + return &v.managedHSMId, nil + } + + // We haven't found it, but Data Sources and Resources need to handle this error separately + return nil, nil +} + +func (c *Client) cacheKeyForManagedHSM(name string) string { + return strings.ToLower(name) +} + +func (c *Client) populateCache(ctx context.Context, subscriptionId commonids.SubscriptionId) error { + // Pull out the list of Managed HSMs available within the Subscription to re-populate the cache + // + // Whilst we've historically used the Resources API to query the single Managed HSM in question + // this endpoint has caching related issues - and whilst the ResourceGraph API has been suggested + // as an alternative that fixes this, we've seen similar caching issues there. + // Therefore, we're falling back on querying all the Managed HSMs within the specified Subscription, which + // comes from the `KeyVault` Resource Provider rather than the `Resources` Resource Provider - which + // is an approach we've used previously, but now with better caching. + // + // Whilst querying ALL Managed HSMs within a Subscription IS excessive where only a single Managed HSM + // is used - having the cache populated (one-time, per Provider launch) should alleviate problems + // in Terraform Configurations defining a large number of Managed HSM related items. + // + // Note that we will only populate the cache on demand where HSM resources are actually being managed, to avoid + // the overhead in configurations where HSM is not used. + // + // Finally, it's worth noting that we intentionally List ALL the Managed HSMs within a Subscription + // to be able to cache ALL of them - prior to looking up the specific Managed HSM we're interested + // in from the freshly populated cache. + // This fixes an issue in the previous implementation where the Cache was being repeatedly semi-populated + // until the specified Managed HSM was found, at which point we skipped populating the cache, which + // affected both the `Resources` API implementation: + // https://github.com/hashicorp/terraform-provider-azurerm/blob/3e88e5e74e12577d785f10298281b1b3c172254f/internal/services/keyvault/client/helpers.go#L133-L173 + // and the `ListBySubscription` endpoint: + // https://github.com/hashicorp/terraform-provider-azurerm/blob/a5e728dc62e832e74d7bb0f40a79af0ae5a79e1e/azurerm/helpers/azure/key_vault.go#L42-L89 + + opts := managedhsms.DefaultListBySubscriptionOperationOptions() + results, err := c.ManagedHsmClient.ListBySubscriptionComplete(ctx, subscriptionId, opts) + if err != nil { + return fmt.Errorf("listing the Managed HSMs within %s: %+v", subscriptionId, err) + } + for _, item := range results.Items { + if item.Id == nil || item.Properties.HsmUri == nil { + continue + } + + // Populate the Managed HSM into the cache + managedHsm, err := managedhsms.ParseManagedHSMIDInsensitively(*item.Id) + if err != nil { + return fmt.Errorf("parsing %q as a Managed HSM ID: %+v", *item.Id, err) + } + dataPlaneUri := *item.Properties.HsmUri + c.AddToCache(*managedHsm, dataPlaneUri) + } + + return nil +} diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource.go index 51c3c2122396..79d19a835423 100644 --- a/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource.go +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource.go @@ -179,14 +179,13 @@ func resourceKeyVaultManagedHardwareSecurityModule() *pluginsdk.Resource { } func resourceArmKeyVaultManagedHardwareSecurityModuleCreate(d *pluginsdk.ResourceData, meta interface{}) error { - kvClient := meta.(*clients.Client).ManagedHSMs - hsmClient := kvClient.ManagedHsmClient + client := meta.(*clients.Client).ManagedHSMs subscriptionId := meta.(*clients.Client).Account.SubscriptionId ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) defer cancel() id := managedhsms.NewManagedHSMID(subscriptionId, d.Get("resource_group_name").(string), d.Get("name").(string)) - existing, err := hsmClient.Get(ctx, id) + existing, err := client.ManagedHsmClient.Get(ctx, id) if err != nil { if !response.WasNotFound(existing.HttpResponse) { return fmt.Errorf("checking for presence of existing %s: %+v", id, err) @@ -221,20 +220,33 @@ func resourceArmKeyVaultManagedHardwareSecurityModuleCreate(d *pluginsdk.Resourc hsm.Properties.TenantId = pointer.To(tenantId) } - if err := hsmClient.CreateOrUpdateThenPoll(ctx, id, hsm); err != nil { + if err := client.ManagedHsmClient.CreateOrUpdateThenPoll(ctx, id, hsm); err != nil { return fmt.Errorf("creating %s: %+v", id, err) } d.SetId(id.ID()) + dataPlaneUri := "" + resp, err := client.ManagedHsmClient.Get(ctx, id) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", id, err) + } + if model := resp.Model; model != nil && model.Properties != nil && model.Properties.HsmUri != nil { + dataPlaneUri = *model.Properties.HsmUri + } + if dataPlaneUri == "" { + return fmt.Errorf("retrieving %s: `properties.HsmUri` was nil", id) + } + client.AddToCache(id, dataPlaneUri) + // security domain download to activate this module if ok := d.HasChange("security_domain_key_vault_certificate_ids"); ok { // get hsm uri - resp, err := hsmClient.Get(ctx, id) + resp, err := client.ManagedHsmClient.Get(ctx, id) if err != nil || resp.Model == nil || resp.Model.Properties == nil || resp.Model.Properties.HsmUri == nil { return fmt.Errorf("got nil HSMUri for %s: %+v", id, err) } - encData, err := securityDomainDownload(ctx, kvClient, *resp.Model.Properties.HsmUri, d.Get("security_domain_key_vault_certificate_ids").([]interface{}), d.Get("security_domain_quorum").(int)) + encData, err := securityDomainDownload(ctx, client, *resp.Model.Properties.HsmUri, d.Get("security_domain_key_vault_certificate_ids").([]interface{}), d.Get("security_domain_quorum").(int)) if err != nil { return fmt.Errorf("downloading security domain for %q: %+v", id, err) } @@ -358,7 +370,7 @@ func resourceArmKeyVaultManagedHardwareSecurityModuleRead(d *pluginsdk.ResourceD } func resourceArmKeyVaultManagedHardwareSecurityModuleDelete(d *pluginsdk.ResourceData, meta interface{}) error { - hsmClient := meta.(*clients.Client).ManagedHSMs.ManagedHsmClient + client := meta.(*clients.Client).ManagedHSMs ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) defer cancel() @@ -367,8 +379,8 @@ func resourceArmKeyVaultManagedHardwareSecurityModuleDelete(d *pluginsdk.Resourc return err } - // We need to grab the keyvault hsm to see if purge protection is enabled prior to deletion - resp, err := hsmClient.Get(ctx, *id) + // We need to grab the managed hsm to see if purge protection is enabled prior to deletion + resp, err := client.ManagedHsmClient.Get(ctx, *id) if err != nil { return fmt.Errorf("retrieving %s: %+v", id, err) } @@ -382,7 +394,7 @@ func resourceArmKeyVaultManagedHardwareSecurityModuleDelete(d *pluginsdk.Resourc } } - if err := hsmClient.DeleteThenPoll(ctx, *id); err != nil { + if err := client.ManagedHsmClient.DeleteThenPoll(ctx, *id); err != nil { return fmt.Errorf("deleting %s: %+v", id, err) } @@ -397,16 +409,18 @@ func resourceArmKeyVaultManagedHardwareSecurityModuleDelete(d *pluginsdk.Resourc // try to purge again if managed HSM still exists after 1 minute // for API issue: https://github.com/Azure/azure-rest-api-specs/issues/27138 purgeId := managedhsms.NewDeletedManagedHSMID(id.SubscriptionId, loc, id.ManagedHSMName) - if _, err := hsmClient.PurgeDeleted(ctx, purgeId); err != nil { + if _, err := client.ManagedHsmClient.PurgeDeleted(ctx, purgeId); err != nil { return fmt.Errorf("purging %s: %+v", id, err) } - purgePoller := custompollers.NewHSMPurgePoller(hsmClient, purgeId) + purgePoller := custompollers.NewHSMPurgePoller(client.ManagedHsmClient, purgeId) poller := pollers.NewPoller(purgePoller, time.Second*30, pollers.DefaultNumberOfDroppedConnectionsToAllow) if err := poller.PollUntilDone(ctx); err != nil { return fmt.Errorf("waiting for %s to be purged: %+v", id, err) } + client.Purge(*id) + return nil } diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource_test.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource_test.go index c9e602d10eb6..a6dc3686491e 100644 --- a/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource_test.go +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource_test.go @@ -19,17 +19,36 @@ import ( type KeyVaultManagedHardwareSecurityModuleResource struct{} func TestAccKeyVaultManagedHardwareSecurityModule(t *testing.T) { - // NOTE: this is a combined test rather than separate split out tests due to - // Azure only being able provision against one instance at a time + // @manicminer: these tests are sequential due to low service limits for Managed HSM + // (max 5 instances per subscription as of 2024-04-23) + // and to try and maintain a little headroom for cleanup after failed tests acceptance.RunTestsInSequence(t, map[string]map[string]func(t *testing.T){ + "dataSource": { + "basic": testAccDataSourceKeyVaultManagedHardwareSecurityModule_basic, + }, "resource": { - "data_source": testAccDataSourceKeyVaultManagedHardwareSecurityModule_basic, - "basic": testAccKeyVaultManagedHardwareSecurityModule_basic, - "update": testAccKeyVaultManagedHardwareSecurityModule_updateAndRequiresImport, - "complete": testAccKeyVaultManagedHardwareSecurityModule_complete, - "download": testAccKeyVaultManagedHardwareSecurityModule_download, - "role_define": testAccKeyVaultManagedHardwareSecurityModule_roleDefinition, - "role_assign": testAccKeyVaultManagedHardwareSecurityModule_roleAssignment, + "basic": testAccKeyVaultManagedHardwareSecurityModule_basic, + "update": testAccKeyVaultManagedHardwareSecurityModule_updateAndRequiresImport, + "complete": testAccKeyVaultManagedHardwareSecurityModule_complete, + "download": testAccKeyVaultManagedHardwareSecurityModule_download, + }, + "roleAssignments": { + "builtInRole": testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_builtInRole, + "customRole": testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_customRole, + + // TODO: uses `vault_base_url`, these 2 can be removed in 4.0 + "legacyBuiltInRole": testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_legacyBuiltInRole, + "legacyCustomRole": testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_legacyCustomRole, + }, + "roleDefinitions": { + "basic": testAccKeyVaultManagedHardwareSecurityModuleRoleDefinition_basic, + + // TODO: uses `vault_base_url`, this can be removed in 4.0 + "legacyWithUpdate": testAccKeyVaultManagedHardwareSecurityModuleRoleDefinition_legacyWithUpdate, + }, + "roleDefinitionDataSource": { + "basic": testAccDataSourceKeyVaultManagedHardwareSecurityModuleRoleDefinition_basic, + "legacy": testAccDataSourceKeyVaultManagedHardwareSecurityModuleRoleDefinition_legacy, }, }) } @@ -78,50 +97,6 @@ func testAccKeyVaultManagedHardwareSecurityModule_download(t *testing.T) { }) } -func testAccKeyVaultManagedHardwareSecurityModule_roleDefinition(t *testing.T) { - data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_role_definition", "test") - r := KeyVaultMHSMRoleDefinitionResource{} - - data.ResourceSequentialTest(t, r, []acceptance.TestStep{ - { - Config: r.withRoleDefinition(data), - Check: acceptance.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - ), - }, - data.ImportStep(), - { - Config: r.withRoleDefinitionUpdate(data), - Check: acceptance.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - ), - }, - data.ImportStep(), - }) -} - -func testAccKeyVaultManagedHardwareSecurityModule_roleAssignment(t *testing.T) { - data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_role_assignment", "test") - r := KeyVaultManagedHSMRoleAssignmentResource{} - - data.ResourceSequentialTest(t, r, []acceptance.TestStep{ - { - Config: r.withRoleAssignment(data), - Check: acceptance.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - ), - }, - data.ImportStep(), - { - Config: r.withBuiltInRoleAssignment(data), - Check: acceptance.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - ), - }, - data.ImportStep(), - }) -} - func testAccKeyVaultManagedHardwareSecurityModule_updateAndRequiresImport(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module", "test") r := KeyVaultManagedHardwareSecurityModuleResource{} diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_assignment_resource.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_assignment_resource.go index 8bac6a5ed44e..53682e22ed2b 100644 --- a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_assignment_resource.go +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_assignment_resource.go @@ -11,9 +11,14 @@ import ( "time" "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" "github.com/hashicorp/go-azure-sdk/resource-manager/authorization/2022-04-01/roledefinitions" + "github.com/hashicorp/go-azure-sdk/resource-manager/keyvault/2023-07-01/managedhsms" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/locks" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" @@ -23,26 +28,37 @@ import ( ) type KeyVaultManagedHSMRoleAssignmentModel struct { - VaultBaseUrl string `tfschema:"vault_base_url"` + ManagedHSMID string `tfschema:"managed_hsm_id"` Name string `tfschema:"name"` Scope string `tfschema:"scope"` RoleDefinitionId string `tfschema:"role_definition_id"` PrincipalId string `tfschema:"principal_id"` ResourceId string `tfschema:"resource_id"` + + // TODO: remove in v4.0 + VaultBaseUrl string `tfschema:"vault_base_url"` } -type KeyVaultManagedHSMRoleAssignmentResource struct{} +var _ sdk.ResourceWithStateMigration = KeyVaultManagedHSMRoleAssignmentResource{} -var _ sdk.Resource = KeyVaultManagedHSMRoleAssignmentResource{} +type KeyVaultManagedHSMRoleAssignmentResource struct{} -func (m KeyVaultManagedHSMRoleAssignmentResource) Arguments() map[string]*pluginsdk.Schema { - return map[string]*pluginsdk.Schema{ - "vault_base_url": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringIsNotEmpty, - }, +func (r KeyVaultManagedHSMRoleAssignmentResource) Arguments() map[string]*pluginsdk.Schema { + s := map[string]*pluginsdk.Schema{ + "managed_hsm_id": func() *pluginsdk.Schema { + s := &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ForceNew: true, + ValidateFunc: managedhsms.ValidateManagedHSMID, + } + if features.FourPointOhBeta() { + s.Required = true + } else { + s.Optional = true + s.Computed = true + } + return s + }(), "name": { Type: pluginsdk.TypeString, @@ -72,9 +88,20 @@ func (m KeyVaultManagedHSMRoleAssignmentResource) Arguments() map[string]*plugin ValidateFunc: validation.StringIsNotEmpty, }, } + if !features.FourPointOhBeta() { + s["vault_base_url"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + Deprecated: "The field `vault_base_url` has been deprecated in favour of `managed_hsm_id` and will be removed in 4.0 of the Azure Provider", + ValidateFunc: validation.StringIsNotEmpty, + } + } + return s } -func (m KeyVaultManagedHSMRoleAssignmentResource) Attributes() map[string]*pluginsdk.Schema { +func (r KeyVaultManagedHSMRoleAssignmentResource) Attributes() map[string]*pluginsdk.Schema { return map[string]*pluginsdk.Schema{ "resource_id": { Type: pluginsdk.TypeString, @@ -83,119 +110,232 @@ func (m KeyVaultManagedHSMRoleAssignmentResource) Attributes() map[string]*plugi } } -func (m KeyVaultManagedHSMRoleAssignmentResource) ModelObject() interface{} { +func (r KeyVaultManagedHSMRoleAssignmentResource) StateUpgraders() sdk.StateUpgradeData { + return sdk.StateUpgradeData{ + SchemaVersion: 1, + Upgraders: map[int]pluginsdk.StateUpgrade{ + 0: migration.ManagedHSMRoleAssignmentV0ToV1{}, + }, + } +} + +func (r KeyVaultManagedHSMRoleAssignmentResource) ModelObject() interface{} { return &KeyVaultManagedHSMRoleAssignmentModel{} } -func (m KeyVaultManagedHSMRoleAssignmentResource) ResourceType() string { +func (r KeyVaultManagedHSMRoleAssignmentResource) ResourceType() string { return "azurerm_key_vault_managed_hardware_security_module_role_assignment" } -func (m KeyVaultManagedHSMRoleAssignmentResource) Create() sdk.ResourceFunc { +func (r KeyVaultManagedHSMRoleAssignmentResource) Create() sdk.ResourceFunc { return sdk.ResourceFunc{ Timeout: 30 * time.Minute, - Func: func(ctx context.Context, meta sdk.ResourceMetaData) (err error) { - client := meta.Client.ManagedHSMs.DataPlaneRoleAssignmentsClient + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ManagedHSMs.DataPlaneRoleAssignmentsClient + domainSuffix, ok := metadata.Client.Account.Environment.ManagedHSM.DomainSuffix() + if !ok { + return fmt.Errorf("could not determine Managed HSM domain suffix for environment %q", metadata.Client.Account.Environment.Name) + } - var model KeyVaultManagedHSMRoleAssignmentModel - if err := meta.Decode(&model); err != nil { + var config KeyVaultManagedHSMRoleAssignmentModel + if err := metadata.Decode(&config); err != nil { return err } - locks.ByName(model.VaultBaseUrl, "azurerm_key_vault_managed_hardware_security_module") - defer locks.UnlockByName(model.VaultBaseUrl, "azurerm_key_vault_managed_hardware_security_module") + var managedHsmId *managedhsms.ManagedHSMId + var endpoint *parse.ManagedHSMDataPlaneEndpoint + var err error + if config.ManagedHSMID != "" { + managedHsmId, err = managedhsms.ParseManagedHSMID(config.ManagedHSMID) + if err != nil { + return err + } + baseUri, err := metadata.Client.ManagedHSMs.BaseUriForManagedHSM(ctx, *managedHsmId) + if err != nil { + return fmt.Errorf("determining the Data Plane Endpoint for %s: %+v", *managedHsmId, err) + } + if baseUri == nil { + return fmt.Errorf("unable to determine the Data Plane Endpoint for %q", *managedHsmId) + } + endpoint, err = parse.ManagedHSMEndpoint(*baseUri, domainSuffix) + if err != nil { + return fmt.Errorf("parsing the Data Plane Endpoint %q: %+v", *endpoint, err) + } + } - id, err := parse.NewManagedHSMRoleAssignmentID(model.VaultBaseUrl, model.Scope, model.Name) - if err != nil { - return err + if managedHsmId == nil && !features.FourPointOhBeta() { + endpoint, err = parse.ManagedHSMEndpoint(config.VaultBaseUrl, domainSuffix) + if err != nil { + return fmt.Errorf("parsing the Data Plane Endpoint %q: %+v", *endpoint, err) + } + subscriptionId := commonids.NewSubscriptionID(metadata.Client.Account.SubscriptionId) + managedHsmId, err = metadata.Client.ManagedHSMs.ManagedHSMIDFromBaseUrl(ctx, subscriptionId, endpoint.BaseURI(), domainSuffix) + if err != nil { + return fmt.Errorf("determining the Managed HSM ID for %q: %+v", endpoint.BaseURI(), err) + } + if managedHsmId == nil { + return fmt.Errorf("unable to determine the Resource Manager ID") + } } - existing, err := client.Get(ctx, model.VaultBaseUrl, model.Scope, model.Name) + locks.ByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") + defer locks.UnlockByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") + + id := parse.NewManagedHSMDataPlaneRoleAssignmentID(endpoint.ManagedHSMName, endpoint.DomainSuffix, config.Scope, config.Name) + existing, err := client.Get(ctx, endpoint.BaseURI(), config.Scope, config.Name) if !utils.ResponseWasNotFound(existing.Response) { if err != nil { return fmt.Errorf("retrieving %s: %v", id.ID(), err) } - return meta.ResourceRequiresImport(m.ResourceType(), id) + return metadata.ResourceRequiresImport(r.ResourceType(), id) } var param keyvault.RoleAssignmentCreateParameters param.Properties = &keyvault.RoleAssignmentProperties{ - PrincipalID: pointer.FromString(model.PrincipalId), - // the role definition id may has '/' prefix, but the api doesn't accept it - RoleDefinitionID: pointer.FromString(strings.TrimPrefix(model.RoleDefinitionId, "/")), + PrincipalID: pointer.FromString(config.PrincipalId), + // the role definition id may have '/' prefix, but the api doesn't accept it + RoleDefinitionID: pointer.FromString(strings.TrimPrefix(config.RoleDefinitionId, "/")), } - if _, err = client.Create(ctx, model.VaultBaseUrl, model.Scope, model.Name, param); err != nil { + + //nolint:misspell + // TODO: @manicminer: when migrating to go-azure-sdk, the SDK should auto-retry on 400 responses with code "BadParameter" and message "Unkown role definition" (note the misspelling) + + if _, err = client.Create(ctx, endpoint.BaseURI(), config.Scope, config.Name, param); err != nil { return fmt.Errorf("creating %s: %v", id.ID(), err) } - meta.SetID(id) + metadata.SetID(id) return nil }, } } -func (m KeyVaultManagedHSMRoleAssignmentResource) Read() sdk.ResourceFunc { +func (r KeyVaultManagedHSMRoleAssignmentResource) Read() sdk.ResourceFunc { return sdk.ResourceFunc{ Timeout: 5 * time.Minute, - Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { - client := meta.Client.ManagedHSMs.DataPlaneRoleAssignmentsClient + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ManagedHSMs.DataPlaneRoleAssignmentsClient + domainSuffix, ok := metadata.Client.Account.Environment.ManagedHSM.DomainSuffix() + if !ok { + return fmt.Errorf("could not determine Managed HSM domain suffix for environment %q", metadata.Client.Account.Environment.Name) + } - id, err := parse.ManagedHSMRoleAssignmentID(meta.ResourceData.Id()) + id, err := parse.ManagedHSMDataPlaneRoleAssignmentID(metadata.ResourceData.Id(), domainSuffix) if err != nil { return err } - result, err := client.Get(ctx, id.VaultBaseUrl, id.Scope, id.Name) + subscriptionId := commonids.NewSubscriptionID(metadata.Client.Account.SubscriptionId) + resourceManagerId, err := metadata.Client.ManagedHSMs.ManagedHSMIDFromBaseUrl(ctx, subscriptionId, id.BaseURI(), domainSuffix) if err != nil { - if utils.ResponseWasNotFound(result.Response) { - return meta.MarkAsGone(id) + return fmt.Errorf("determining Resource Manager ID for %q: %+v", id, err) + } + if resourceManagerId == nil { + return fmt.Errorf("unable to determine the Resource Manager ID for %s", id) + } + + resp, err := client.Get(ctx, id.BaseURI(), id.Scope, id.RoleAssignmentName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return metadata.MarkAsGone(id) } - return err + + return fmt.Errorf("retrieving %s: %+v", id, err) } - var model KeyVaultManagedHSMRoleAssignmentModel - if err := meta.Decode(&model); err != nil { - return err + model := KeyVaultManagedHSMRoleAssignmentModel{ + ManagedHSMID: resourceManagerId.ID(), + Name: id.RoleAssignmentName, + Scope: id.Scope, } - prop := result.Properties - model.Name = pointer.From(result.Name) - model.VaultBaseUrl = id.VaultBaseUrl - model.Scope = id.Scope - model.PrincipalId = pointer.ToString(prop.PrincipalID) - model.ResourceId = pointer.ToString(result.ID) - if roleID, err := roledefinitions.ParseScopedRoleDefinitionIDInsensitively(pointer.ToString(prop.RoleDefinitionID)); err != nil { - return fmt.Errorf("parsing role definition id: %v", err) - } else { - model.RoleDefinitionId = roleID.ID() + if !features.FourPointOhBeta() { + model.VaultBaseUrl = id.BaseURI() } - return meta.Encode(&model) + if props := resp.Properties; props != nil { + model.PrincipalId = pointer.From(props.PrincipalID) + model.ResourceId = pointer.From(resp.ID) // TODO: verify if we should deprecate this + + if roleDefinitionId := pointer.From(props.RoleDefinitionID); roleDefinitionId != "" { + parsed, err := roledefinitions.ParseScopedRoleDefinitionIDInsensitively(roleDefinitionId) + if err != nil { + return fmt.Errorf("parsing role definition id %q: %v", roleDefinitionId, err) + } + + model.RoleDefinitionId = parsed.ID() + } + } + + return metadata.Encode(&model) }, } } -func (m KeyVaultManagedHSMRoleAssignmentResource) Delete() sdk.ResourceFunc { +func (r KeyVaultManagedHSMRoleAssignmentResource) Delete() sdk.ResourceFunc { return sdk.ResourceFunc{ Timeout: 10 * time.Minute, - Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { - id, err := parse.ManagedHSMRoleAssignmentID(meta.ResourceData.Id()) + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ManagedHSMs.DataPlaneRoleAssignmentsClient + + domainSuffix, ok := metadata.Client.Account.Environment.ManagedHSM.DomainSuffix() + if !ok { + return fmt.Errorf("could not determine Managed HSM domain suffix for environment %q", metadata.Client.Account.Environment.Name) + } + + id, err := parse.ManagedHSMDataPlaneRoleAssignmentID(metadata.ResourceData.Id(), domainSuffix) if err != nil { return err } - meta.Logger.Infof("deleting %s", id) + subscriptionId := commonids.NewSubscriptionID(metadata.Client.Account.SubscriptionId) + managedHsmId, err := metadata.Client.ManagedHSMs.ManagedHSMIDFromBaseUrl(ctx, subscriptionId, id.BaseURI(), domainSuffix) + if err != nil { + return fmt.Errorf("determining the Managed HSM ID from the Base URI %q: %+v", id.BaseURI(), err) + } + if managedHsmId == nil { + return fmt.Errorf("unable to determine the Managed HSM ID from the Base URI %q: %+v", id.BaseURI(), err) + } + + locks.ByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") + defer locks.UnlockByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") - locks.ByName(id.VaultBaseUrl, "azurerm_key_vault_managed_hardware_security_module") - defer locks.UnlockByName(id.VaultBaseUrl, "azurerm_key_vault_managed_hardware_security_module") - if _, err := meta.Client.ManagedHSMs.DataPlaneRoleAssignmentsClient.Delete(ctx, id.VaultBaseUrl, id.Scope, id.Name); err != nil { + if _, err := client.Delete(ctx, id.BaseURI(), id.Scope, id.RoleAssignmentName); err != nil { return fmt.Errorf("deleting %s: %v", id.ID(), err) } + + deadline, ok := ctx.Deadline() + if !ok { + return fmt.Errorf("internal-error: context has no deadline") + } + stateConf := &pluginsdk.StateChangeConf{ + Pending: []string{"InProgress"}, + Target: []string{"NotFound"}, + Refresh: func() (interface{}, string, error) { + result, err := client.Get(ctx, id.BaseURI(), id.Scope, id.RoleAssignmentName) + if err != nil { + if response.WasNotFound(result.Response.Response) { + return result, "NotFound", nil + } + + return nil, "Error", err + } + + return result, "InProgress", nil + }, + ContinuousTargetOccurence: 5, + PollInterval: 5 * time.Second, + Timeout: time.Until(deadline), + } + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for deletion of %s: %+v", id, err) + } + return nil }, } } -func (m KeyVaultManagedHSMRoleAssignmentResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { - return validate.ManagedHSMRoleAssignmentId +func (r KeyVaultManagedHSMRoleAssignmentResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.ManagedHSMDataPlaneRoleAssignmentID } diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_assignment_resource_test.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_assignment_resource_test.go index 6d635a332092..65f1a2fec541 100644 --- a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_assignment_resource_test.go +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_assignment_resource_test.go @@ -6,9 +6,12 @@ package managedhsm_test import ( "context" "fmt" + "testing" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/utils" @@ -16,25 +19,118 @@ import ( type KeyVaultManagedHSMRoleAssignmentResource struct{} +// NOTE: all fields within the Role Assignment are ForceNew, therefore any Update tests aren't going to do much.. + +func testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_builtInRole(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_role_assignment", "test") + r := KeyVaultManagedHSMRoleAssignmentResource{} + + data.ResourceSequentialTest(t, r, []acceptance.TestStep{ + { + Config: r.builtInRole(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_customRole(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_role_assignment", "test") + r := KeyVaultManagedHSMRoleAssignmentResource{} + + data.ResourceSequentialTest(t, r, []acceptance.TestStep{ + { + Config: r.customRole(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_legacyBuiltInRole(t *testing.T) { + if features.FourPointOhBeta() { + t.Skipf("This test isn't applicable in 4.0") + } + + data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_role_assignment", "test") + r := KeyVaultManagedHSMRoleAssignmentResource{} + + data.ResourceSequentialTest(t, r, []acceptance.TestStep{ + { + Config: r.legacyBuiltInRole(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_legacyCustomRole(t *testing.T) { + if features.FourPointOhBeta() { + t.Skipf("This test isn't applicable in 4.0") + } + + data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_role_assignment", "test") + r := KeyVaultManagedHSMRoleAssignmentResource{} + + data.ResourceSequentialTest(t, r, []acceptance.TestStep{ + { + Config: r.legacyCustomRole(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + // real test nested in TestAccKeyVaultManagedHardwareSecurityModule, only provide Exists logic here -func (k KeyVaultManagedHSMRoleAssignmentResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { - id, err := parse.ManagedHSMRoleAssignmentID(state.ID) +func (r KeyVaultManagedHSMRoleAssignmentResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + domainSuffix, ok := client.Account.Environment.ManagedHSM.DomainSuffix() + if !ok { + return nil, fmt.Errorf("this Environment doesn't specify the Domain Suffix for Managed HSM") + } + id, err := parse.ManagedHSMDataPlaneRoleAssignmentID(state.ID, domainSuffix) if err != nil { return nil, err } - resp, err := client.ManagedHSMs.DataPlaneRoleAssignmentsClient.Get(ctx, id.VaultBaseUrl, id.Scope, id.Name) + resp, err := client.ManagedHSMs.DataPlaneRoleAssignmentsClient.Get(ctx, id.BaseURI(), id.Scope, id.RoleAssignmentName) if err != nil { - return nil, fmt.Errorf("retrieving Type %s: %+v", id, err) + return nil, fmt.Errorf("retrieving %s: %+v", id, err) } return utils.Bool(resp.Properties != nil), nil } -func (k KeyVaultManagedHSMRoleAssignmentResource) withRoleAssignment(data acceptance.TestData) string { - roleDef := KeyVaultMHSMRoleDefinitionResource{}.withRoleDefinition(data) - +func (r KeyVaultManagedHSMRoleAssignmentResource) builtInRole(data acceptance.TestData) string { return fmt.Sprintf(` +%s +locals { + assignmentOfficerName = "706c03c7-69ad-33e5-2796-b3380d3a6e1a" +} + +data "azurerm_key_vault_managed_hardware_security_module_role_definition" "officer" { + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + name = "515eb02d-2335-4d2d-92f2-b1cbdf9c3778" +} + +resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "test" { + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + name = local.assignmentOfficerName + scope = "/keys" + role_definition_id = data.azurerm_key_vault_managed_hardware_security_module_role_definition.officer.resource_manager_id + principal_id = data.azurerm_client_config.current.object_id +} +`, KeyVaultManagedHardwareSecurityModuleResource{}.download(data, 3)) +} +func (r KeyVaultManagedHSMRoleAssignmentResource) customRole(data acceptance.TestData) string { + return fmt.Sprintf(` %s locals { @@ -42,21 +138,17 @@ locals { } resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "test" { - vault_base_url = azurerm_key_vault_managed_hardware_security_module.test.hsm_uri + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id name = local.assignmentTestName scope = "/keys" role_definition_id = azurerm_key_vault_managed_hardware_security_module_role_definition.test.resource_manager_id principal_id = data.azurerm_client_config.current.object_id } -`, roleDef) +`, KeyVaultMHSMRoleDefinitionResource{}.basic(data)) } -func (k KeyVaultManagedHSMRoleAssignmentResource) withBuiltInRoleAssignment(data acceptance.TestData) string { - roleDef := k.withRoleAssignment(data) - +func (r KeyVaultManagedHSMRoleAssignmentResource) legacyBuiltInRole(data acceptance.TestData) string { return fmt.Sprintf(` - - %s locals { @@ -68,12 +160,30 @@ data "azurerm_key_vault_managed_hardware_security_module_role_definition" "offic name = "515eb02d-2335-4d2d-92f2-b1cbdf9c3778" } -resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "officer" { +resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "test" { vault_base_url = azurerm_key_vault_managed_hardware_security_module.test.hsm_uri name = local.assignmentOfficerName scope = "/keys" role_definition_id = data.azurerm_key_vault_managed_hardware_security_module_role_definition.officer.resource_manager_id principal_id = data.azurerm_client_config.current.object_id } -`, roleDef) +`, KeyVaultManagedHardwareSecurityModuleResource{}.download(data, 3)) +} + +func (r KeyVaultManagedHSMRoleAssignmentResource) legacyCustomRole(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +locals { + assignmentTestName = "1e243909-064c-6ac3-84e9-1c8bf8d6ad52" +} + +resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "test" { + vault_base_url = azurerm_key_vault_managed_hardware_security_module.test.hsm_uri + name = local.assignmentTestName + scope = "/keys" + role_definition_id = azurerm_key_vault_managed_hardware_security_module_role_definition.test.resource_manager_id + principal_id = data.azurerm_client_config.current.object_id +} +`, KeyVaultMHSMRoleDefinitionResource{}.basic(data)) } diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_data_source.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_data_source.go index 0b47ab6ea8a0..75acbe62090e 100644 --- a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_data_source.go +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_data_source.go @@ -9,23 +9,30 @@ import ( "time" "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" "github.com/hashicorp/go-azure-sdk/resource-manager/authorization/2022-04-01/roledefinitions" + "github.com/hashicorp/go-azure-sdk/resource-manager/keyvault/2023-07-01/managedhsms" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" "github.com/hashicorp/terraform-provider-azurerm/utils" + "github.com/tombuildsstuff/kermit/sdk/keyvault/7.4/keyvault" ) type KeyVaultMHSMRoleDefinitionDataSourceModel struct { + ManagedHSMID string `tfschema:"managed_hsm_id"` Name string `tfschema:"name"` RoleName string `tfschema:"role_name"` - VaultBaseUrl string `tfschema:"vault_base_url"` Description string `tfschema:"description"` AssignableScopes []string `tfschema:"assignable_scopes"` Permission []Permission `tfschema:"permission"` RoleType string `tfschema:"role_type"` ResourceManagerId string `tfschema:"resource_manager_id"` + + // TODO: remove in v4.0 + VaultBaseUrl string `tfschema:"vault_base_url"` } type KeyvaultMHSMRoleDefinitionDataSource struct{} @@ -33,19 +40,38 @@ type KeyvaultMHSMRoleDefinitionDataSource struct{} var _ sdk.DataSource = KeyvaultMHSMRoleDefinitionDataSource{} func (k KeyvaultMHSMRoleDefinitionDataSource) Arguments() map[string]*pluginsdk.Schema { - return map[string]*pluginsdk.Schema{ + s := map[string]*pluginsdk.Schema{ "name": { Type: pluginsdk.TypeString, Required: true, ValidateFunc: validation.IsUUID, }, - "vault_base_url": { + "managed_hsm_id": func() *pluginsdk.Schema { + s := &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: managedhsms.ValidateManagedHSMID, + } + if features.FourPointOhBeta() { + s.Required = true + } else { + s.Optional = true + s.Computed = true + } + return s + }(), + } + + if !features.FourPointOhBeta() { + s["vault_base_url"] = &pluginsdk.Schema{ Type: pluginsdk.TypeString, - Required: true, + Optional: true, + Computed: true, ValidateFunc: validation.IsURLWithHTTPorHTTPS, - }, + } } + + return s } func (k KeyvaultMHSMRoleDefinitionDataSource) Attributes() map[string]*pluginsdk.Schema { @@ -131,48 +157,90 @@ func (k KeyvaultMHSMRoleDefinitionDataSource) ResourceType() string { func (k KeyvaultMHSMRoleDefinitionDataSource) Read() sdk.ResourceFunc { return sdk.ResourceFunc{ Timeout: 5 * time.Minute, - Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { - var model KeyVaultMHSMRoleDefinitionDataSourceModel - if err := meta.Decode(&model); err != nil { - return err + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ManagedHSMs.DataPlaneRoleDefinitionsClient + domainSuffix, ok := metadata.Client.Account.Environment.ManagedHSM.DomainSuffix() + if !ok { + return fmt.Errorf("could not determine Managed HSM domain suffix for environment %q", metadata.Client.Account.Environment.Name) } - id, err := parse.NewManagedHSMRoleDefinitionID(model.VaultBaseUrl, roleDefinitionScope, model.Name) - if err != nil { + var config KeyVaultMHSMRoleDefinitionDataSourceModel + if err := metadata.Decode(&config); err != nil { return err } - client := meta.Client.ManagedHSMs.DataPlaneRoleDefinitionsClient - result, err := client.Get(ctx, id.VaultBaseUrl, roleDefinitionScope, id.Name) + var managedHsmId *managedhsms.ManagedHSMId + var endpoint *parse.ManagedHSMDataPlaneEndpoint + var err error + if config.ManagedHSMID != "" { + managedHsmId, err = managedhsms.ParseManagedHSMID(config.ManagedHSMID) + if err != nil { + return err + } + baseUri, err := metadata.Client.ManagedHSMs.BaseUriForManagedHSM(ctx, *managedHsmId) + if err != nil { + return fmt.Errorf("determining the Data Plane Endpoint for %s: %+v", *managedHsmId, err) + } + if baseUri == nil { + return fmt.Errorf("unable to determine the Data Plane Endpoint for %q", *managedHsmId) + } + endpoint, err = parse.ManagedHSMEndpoint(*baseUri, domainSuffix) + if err != nil { + return fmt.Errorf("parsing the Data Plane Endpoint %q: %+v", *endpoint, err) + } + } + + if managedHsmId == nil && !features.FourPointOhBeta() { + endpoint, err = parse.ManagedHSMEndpoint(config.VaultBaseUrl, domainSuffix) + if err != nil { + return fmt.Errorf("parsing the Data Plane Endpoint %q: %+v", *endpoint, err) + } + subscriptionId := commonids.NewSubscriptionID(metadata.Client.Account.SubscriptionId) + managedHsmId, err = metadata.Client.ManagedHSMs.ManagedHSMIDFromBaseUrl(ctx, subscriptionId, endpoint.BaseURI(), domainSuffix) + if err != nil { + return fmt.Errorf("determining the Managed HSM ID for %q: %+v", endpoint.BaseURI(), err) + } + if managedHsmId == nil { + return fmt.Errorf("unable to determine the Resource Manager ID") + } + } + + scope := keyvault.RoleScopeGlobal + id := parse.NewManagedHSMDataPlaneRoleDefinitionID(endpoint.ManagedHSMName, endpoint.DomainSuffix, string(scope), config.Name) + + result, err := client.Get(ctx, id.BaseURI(), id.Scope, id.RoleDefinitionName) if err != nil { if utils.ResponseWasNotFound(result.Response) { - return fmt.Errorf("%s does not exist", id) + return fmt.Errorf("%s was not found", id) } - return err + return fmt.Errorf("retrieving %s: %+v", id, err) } - roleID, err := roledefinitions.ParseScopedRoleDefinitionIDInsensitively(pointer.From(result.ID)) - if err != nil { - return fmt.Errorf("paring role definition id %s: %v", pointer.From(result.ID), err) + if v := pointer.From(result.ID); v != "" { + roleID, err := roledefinitions.ParseScopedRoleDefinitionIDInsensitively(v) + if err != nil { + return fmt.Errorf("paring role definition id %q: %v", v, err) + } + config.ResourceManagerId = roleID.ID() } - model.ResourceManagerId = roleID.ID() if prop := result.RoleDefinitionProperties; prop != nil { - model.Description = pointer.ToString(prop.Description) - model.RoleType = string(prop.RoleType) - model.RoleName = pointer.From(prop.RoleName) + config.Description = pointer.ToString(prop.Description) + config.RoleType = string(prop.RoleType) + config.RoleName = pointer.From(prop.RoleName) if prop.AssignableScopes != nil { + config.AssignableScopes = make([]string, 0) for _, r := range *prop.AssignableScopes { - model.AssignableScopes = append(model.AssignableScopes, string(r)) + config.AssignableScopes = append(config.AssignableScopes, string(r)) } } - model.Permission = flattenKeyVaultMHSMRolePermission(prop.Permissions) + config.Permission = flattenKeyVaultMHSMRolePermission(prop.Permissions) } - meta.SetID(id) - return meta.Encode(&model) + metadata.SetID(id) + return metadata.Encode(&config) }, } } diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_data_source_test.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_data_source_test.go new file mode 100644 index 000000000000..0422387b104a --- /dev/null +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_data_source_test.go @@ -0,0 +1,79 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package managedhsm_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" +) + +// TODO: check the UUIDs + +type KeyVaultManagedHardwareSecurityModuleRoleDefinitionDataSource struct{} + +func testAccDataSourceKeyVaultManagedHardwareSecurityModuleRoleDefinition_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_key_vault_managed_hardware_security_module_role_definition", "test") + r := KeyVaultManagedHardwareSecurityModuleRoleDefinitionDataSource{} + + data.DataSourceTestInSequence(t, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("name").Exists(), + check.That(data.ResourceName).Key("managed_hsm_id").Exists(), + check.That(data.ResourceName).Key("role_name").HasValue(fmt.Sprintf("myRole%s", data.RandomString)), + check.That(data.ResourceName).Key("description").HasValue("desc foo"), + check.That(data.ResourceName).Key("permission.%").HasValue("1"), + check.That(data.ResourceName).Key("permission.0.data_actions.%").HasValue("5"), + check.That(data.ResourceName).Key("permission.0.not_data_actions.%").HasValue("1"), + ), + }, + }) +} + +func testAccDataSourceKeyVaultManagedHardwareSecurityModuleRoleDefinition_legacy(t *testing.T) { + if !features.FourPointOhBeta() { + t.Skipf("no longer needed in v4.0") + } + + data := acceptance.BuildTestData(t, "data.azurerm_key_vault_managed_hardware_security_module_role_definition", "test") + r := KeyVaultManagedHardwareSecurityModuleRoleDefinitionDataSource{} + + data.DataSourceTestInSequence(t, []acceptance.TestStep{ + { + Config: r.legacy(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("name").Exists(), + check.That(data.ResourceName).Key("managed_hsm_id").Exists(), + check.That(data.ResourceName).Key("vault_base_url").Exists(), + ), + }, + }) +} + +func (KeyVaultManagedHardwareSecurityModuleRoleDefinitionDataSource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +data "azurerm_key_vault_managed_hardware_security_module_role_definition" "test" { + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + name = "21dbd100-6940-42c2-9190-5d6cb909625b" +} +`, KeyVaultMHSMRoleDefinitionResource{}.basic(data)) +} + +func (KeyVaultManagedHardwareSecurityModuleRoleDefinitionDataSource) legacy(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +data "azurerm_key_vault_managed_hardware_security_module_role_definition" "test" { + vault_base_url = azurerm_key_vault_managed_hardware_security_module.test.hsm_uri + name = "21dbd100-6940-42c2-9190-5d6cb909625b" +} +`, KeyVaultMHSMRoleDefinitionResource{}.basic(data)) +} diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_resource.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_resource.go index 679b543c1137..d8bec6ed6a31 100644 --- a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_resource.go +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_resource.go @@ -10,9 +10,13 @@ import ( "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" "github.com/hashicorp/go-azure-sdk/resource-manager/authorization/2022-04-01/roledefinitions" + "github.com/hashicorp/go-azure-sdk/resource-manager/keyvault/2023-07-01/managedhsms" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/locks" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" @@ -21,33 +25,35 @@ import ( "github.com/tombuildsstuff/kermit/sdk/keyvault/7.4/keyvault" ) -const roleDefinitionScope = "/" - -type Permission struct { - Actions []string `tfschema:"actions"` - NotActions []string `tfschema:"not_actions"` - DataActions []string `tfschema:"data_actions"` - NotDataActions []string `tfschema:"not_data_actions"` -} - type KeyVaultMHSMRoleDefinitionModel struct { + ManagedHSMID string `tfschema:"managed_hsm_id"` Name string `tfschema:"name"` RoleName string `tfschema:"role_name"` - VaultBaseUrl string `tfschema:"vault_base_url"` Description string `tfschema:"description"` Permission []Permission `tfschema:"permission"` RoleType string `tfschema:"role_type"` ResourceManagerId string `tfschema:"resource_manager_id"` + + // TODO: remove in 4.0 + VaultBaseUrl string `tfschema:"vault_base_url"` +} + +type Permission struct { + Actions []string `tfschema:"actions"` + NotActions []string `tfschema:"not_actions"` + DataActions []string `tfschema:"data_actions"` + NotDataActions []string `tfschema:"not_data_actions"` } type KeyVaultMHSMRoleDefinitionResource struct{} +var _ sdk.ResourceWithStateMigration = KeyVaultMHSMRoleDefinitionResource{} var _ sdk.ResourceWithUpdate = KeyVaultMHSMRoleDefinitionResource{} // Arguments ... // skip `assignable_scopes` field support as https://github.com/Azure/azure-rest-api-specs/issues/23045 -func (k KeyVaultMHSMRoleDefinitionResource) Arguments() map[string]*pluginsdk.Schema { - return map[string]*pluginsdk.Schema{ +func (r KeyVaultMHSMRoleDefinitionResource) Arguments() map[string]*pluginsdk.Schema { + s := map[string]*pluginsdk.Schema{ "name": { Type: pluginsdk.TypeString, Required: true, @@ -55,19 +61,27 @@ func (k KeyVaultMHSMRoleDefinitionResource) Arguments() map[string]*pluginsdk.Sc ValidateFunc: validation.IsUUID, }, + "managed_hsm_id": func() *pluginsdk.Schema { + s := &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ForceNew: true, + ValidateFunc: managedhsms.ValidateManagedHSMID, + } + if features.FourPointOhBeta() { + s.Required = true + } else { + s.Optional = true + s.Computed = true + } + return s + }(), + "role_name": { Type: pluginsdk.TypeString, Optional: true, ValidateFunc: validation.StringIsNotEmpty, }, - "vault_base_url": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.IsURLWithHTTPorHTTPS, - }, - "permission": { Type: pluginsdk.TypeList, Optional: true, @@ -130,9 +144,21 @@ func (k KeyVaultMHSMRoleDefinitionResource) Arguments() map[string]*pluginsdk.Sc ValidateFunc: validation.StringIsNotEmpty, }, } + + if !features.FourPointOhBeta() { + s["vault_base_url"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.IsURLWithHTTPorHTTPS, + } + } + + return s } -func (k KeyVaultMHSMRoleDefinitionResource) Attributes() map[string]*pluginsdk.Schema { +func (r KeyVaultMHSMRoleDefinitionResource) Attributes() map[string]*pluginsdk.Schema { return map[string]*pluginsdk.Schema{ "role_type": { Type: pluginsdk.TypeString, @@ -146,127 +172,237 @@ func (k KeyVaultMHSMRoleDefinitionResource) Attributes() map[string]*pluginsdk.S } } -func (k KeyVaultMHSMRoleDefinitionResource) ModelObject() interface{} { +func (r KeyVaultMHSMRoleDefinitionResource) StateUpgraders() sdk.StateUpgradeData { + return sdk.StateUpgradeData{ + SchemaVersion: 1, + Upgraders: map[int]pluginsdk.StateUpgrade{ + 0: migration.ManagedHSMRoleDefinitionV0ToV1{}, + }, + } +} + +func (r KeyVaultMHSMRoleDefinitionResource) ModelObject() interface{} { return &KeyVaultMHSMRoleDefinitionModel{} } -func (k KeyVaultMHSMRoleDefinitionResource) ResourceType() string { +func (r KeyVaultMHSMRoleDefinitionResource) ResourceType() string { return "azurerm_key_vault_managed_hardware_security_module_role_definition" } -func (k KeyVaultMHSMRoleDefinitionResource) Create() sdk.ResourceFunc { +func (r KeyVaultMHSMRoleDefinitionResource) Create() sdk.ResourceFunc { return sdk.ResourceFunc{ Timeout: 30 * time.Minute, - Func: func(ctx context.Context, meta sdk.ResourceMetaData) (err error) { - client := meta.Client.ManagedHSMs.DataPlaneRoleDefinitionsClient + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ManagedHSMs.DataPlaneRoleDefinitionsClient + domainSuffix, ok := metadata.Client.Account.Environment.ManagedHSM.DomainSuffix() + if !ok { + return fmt.Errorf("could not determine Managed HSM domain suffix for environment %q", metadata.Client.Account.Environment.Name) + } - var model KeyVaultMHSMRoleDefinitionModel - if err = meta.Decode(&model); err != nil { + var config KeyVaultMHSMRoleDefinitionModel + if err := metadata.Decode(&config); err != nil { return err } - // need a lock for hsm subresource create/update/delete, or API may respond error as below - // Status=409 Code="Conflict" Message="There was a conflict while trying to delete the role assignment. - locks.ByName(model.VaultBaseUrl, "azurerm_key_vault_managed_hardware_security_module") - defer locks.UnlockByName(model.VaultBaseUrl, "azurerm_key_vault_managed_hardware_security_module") + var managedHsmId *managedhsms.ManagedHSMId + var endpoint *parse.ManagedHSMDataPlaneEndpoint + var err error + if config.ManagedHSMID != "" { + managedHsmId, err = managedhsms.ParseManagedHSMID(config.ManagedHSMID) + if err != nil { + return err + } + baseUri, err := metadata.Client.ManagedHSMs.BaseUriForManagedHSM(ctx, *managedHsmId) + if err != nil { + return fmt.Errorf("determining the Data Plane Endpoint for %s: %+v", *managedHsmId, err) + } + if baseUri == nil { + return fmt.Errorf("unable to determine the Data Plane Endpoint for %q", *managedHsmId) + } + endpoint, err = parse.ManagedHSMEndpoint(*baseUri, domainSuffix) + if err != nil { + return fmt.Errorf("parsing the Data Plane Endpoint %q: %+v", *endpoint, err) + } + } - id, err := parse.NewManagedHSMRoleDefinitionID(model.VaultBaseUrl, roleDefinitionScope, model.Name) - if err != nil { - return err + if managedHsmId == nil && !features.FourPointOhBeta() { + endpoint, err = parse.ManagedHSMEndpoint(config.VaultBaseUrl, domainSuffix) + if err != nil { + return fmt.Errorf("parsing the Data Plane Endpoint %q: %+v", *endpoint, err) + } + subscriptionId := commonids.NewSubscriptionID(metadata.Client.Account.SubscriptionId) + managedHsmId, err = metadata.Client.ManagedHSMs.ManagedHSMIDFromBaseUrl(ctx, subscriptionId, endpoint.BaseURI(), domainSuffix) + if err != nil { + return fmt.Errorf("determining the Managed HSM ID for %q: %+v", endpoint.BaseURI(), err) + } + if managedHsmId == nil { + return fmt.Errorf("unable to determine the Resource Manager ID") + } } - existing, err := client.Get(ctx, id.VaultBaseUrl, id.Scope, id.Name) + // need a lock for hsm subresource create/update/delete, or API may respond error as below + // Status=409 Code="Conflict" Message="There was a conflict while trying to delete the role assignment. + locks.ByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") + defer locks.UnlockByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") + + scope := keyvault.RoleScopeGlobal + id := parse.NewManagedHSMDataPlaneRoleDefinitionID(endpoint.ManagedHSMName, endpoint.DomainSuffix, string(scope), config.Name) + existing, err := client.Get(ctx, id.BaseURI(), id.Scope, id.ManagedHSMName) if !utils.ResponseWasNotFound(existing.Response) { if err != nil { - return fmt.Errorf("retrieving role definition by name %s: %v", model.Name, err) + return fmt.Errorf("checking for the existence of an existing %q: %+v", id, err) } - return meta.ResourceRequiresImport(k.ResourceType(), id) + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + payload := keyvault.RoleDefinitionCreateParameters{ + Properties: &keyvault.RoleDefinitionProperties{ + RoleName: pointer.To(config.RoleName), + Description: pointer.To(config.Description), + RoleType: keyvault.RoleTypeCustomRole, + Permissions: expandKeyVaultMHSMRolePermissions(config.Permission), + AssignableScopes: pointer.To([]keyvault.RoleScope{ + scope, + }), + }, } - var param keyvault.RoleDefinitionCreateParameters - param.Properties = &keyvault.RoleDefinitionProperties{} - prop := param.Properties - prop.RoleName = pointer.To(model.RoleName) - prop.Description = pointer.To(model.Description) - prop.RoleType = keyvault.RoleTypeCustomRole - prop.Permissions = expandKeyVaultMHSMRolePermissions(model.Permission) - prop.AssignableScopes = pointer.To([]keyvault.RoleScope{"/"}) + // TODO: @manicminer: when migrating to go-azure-sdk, the SDK should auto-retry on 409 responses and should consider manually polling afterwards - if _, err = client.CreateOrUpdate(ctx, model.VaultBaseUrl, roleDefinitionScope, model.Name, param); err != nil { + if _, err = client.CreateOrUpdate(ctx, id.BaseURI(), id.Scope, id.RoleDefinitionName, payload); err != nil { return fmt.Errorf("creating %s: %v", id.ID(), err) } - meta.SetID(id) + deadline, ok := ctx.Deadline() + if !ok { + return fmt.Errorf("internal-error: context has no deadline") + } + stateConf := &pluginsdk.StateChangeConf{ + Pending: []string{"InProgress"}, + Target: []string{"Found"}, + Refresh: func() (interface{}, string, error) { + result, err := client.Get(ctx, id.BaseURI(), id.Scope, id.RoleDefinitionName) + if err != nil { + if response.WasNotFound(result.Response.Response) { + return result, "InProgress", nil + } + + return nil, "Error", err + } + + return result, "Found", nil + }, + ContinuousTargetOccurence: 5, + PollInterval: 5 * time.Second, + Timeout: time.Until(deadline), + } + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for creation of %s: %+v", id, err) + } + + metadata.SetID(id) return nil }, } } -func (k KeyVaultMHSMRoleDefinitionResource) Read() sdk.ResourceFunc { +func (r KeyVaultMHSMRoleDefinitionResource) Read() sdk.ResourceFunc { return sdk.ResourceFunc{ Timeout: 5 * time.Minute, - Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { - // import has no model data but only id - id, err := parse.ManagedHSMRoleDefinitionID(meta.ResourceData.Id()) + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ManagedHSMs.DataPlaneRoleDefinitionsClient + domainSuffix, ok := metadata.Client.Account.Environment.ManagedHSM.DomainSuffix() + if !ok { + return fmt.Errorf("could not determine Managed HSM domain suffix for environment %q", metadata.Client.Account.Environment.Name) + } + + id, err := parse.ManagedHSMDataPlaneRoleDefinitionID(metadata.ResourceData.Id(), domainSuffix) if err != nil { return err } - var model KeyVaultMHSMRoleDefinitionModel - if err = meta.Decode(&model); err != nil { - return err + subscriptionId := commonids.NewSubscriptionID(metadata.Client.Account.SubscriptionId) + managedHsmId, err := metadata.Client.ManagedHSMs.ManagedHSMIDFromBaseUrl(ctx, subscriptionId, id.BaseURI(), domainSuffix) + if err != nil { + return fmt.Errorf("determining the Managed HSM ID from the Base URI %q: %+v", id.BaseURI(), err) + } + if managedHsmId == nil { + return fmt.Errorf("unable to determine the Managed HSM ID from the Base URI %q: %+v", id.BaseURI(), err) } - client := meta.Client.ManagedHSMs.DataPlaneRoleDefinitionsClient - result, err := client.Get(ctx, id.VaultBaseUrl, id.Scope, id.Name) + locks.ByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") + defer locks.UnlockByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") + + result, err := client.Get(ctx, id.BaseURI(), id.Scope, id.RoleDefinitionName) if err != nil { if response.WasNotFound(result.Response.Response) { - return meta.MarkAsGone(id) + return metadata.MarkAsGone(id) } return err } - model.Name = pointer.From(result.Name) - model.VaultBaseUrl = id.VaultBaseUrl - roleID, err := roledefinitions.ParseScopedRoleDefinitionIDInsensitively(pointer.From(result.ID)) - if err != nil { - return fmt.Errorf("paring role definition id %s: %v", pointer.From(result.ID), err) + state := KeyVaultMHSMRoleDefinitionModel{ + Name: pointer.From(result.Name), + ManagedHSMID: managedHsmId.ID(), + + // TODO: remove in 4.0 + VaultBaseUrl: id.BaseURI(), + } + + if v := pointer.From(result.ID); v != "" { + roleID, err := roledefinitions.ParseScopedRoleDefinitionIDInsensitively(v) + if err != nil { + return fmt.Errorf("paring role definition id %q: %+v", v, err) + } + state.ResourceManagerId = roleID.ID() } - model.ResourceManagerId = roleID.ID() if prop := result.RoleDefinitionProperties; prop != nil { - model.Description = pointer.ToString(prop.Description) - model.RoleType = string(prop.RoleType) - model.RoleName = pointer.From(prop.RoleName) - model.Permission = flattenKeyVaultMHSMRolePermission(prop.Permissions) + state.Description = pointer.ToString(prop.Description) + state.RoleType = string(prop.RoleType) + state.RoleName = pointer.From(prop.RoleName) + state.Permission = flattenKeyVaultMHSMRolePermission(prop.Permissions) } - meta.SetID(id) - return meta.Encode(&model) + metadata.SetID(id) + return metadata.Encode(&state) }, } } -func (k KeyVaultMHSMRoleDefinitionResource) Update() sdk.ResourceFunc { +func (r KeyVaultMHSMRoleDefinitionResource) Update() sdk.ResourceFunc { return sdk.ResourceFunc{ Timeout: time.Minute * 10, - Func: func(ctx context.Context, meta sdk.ResourceMetaData) (err error) { - client := meta.Client.ManagedHSMs.DataPlaneRoleDefinitionsClient + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) (err error) { + client := metadata.Client.ManagedHSMs.DataPlaneRoleDefinitionsClient + domainSuffix, ok := metadata.Client.Account.Environment.ManagedHSM.DomainSuffix() + if !ok { + return fmt.Errorf("could not determine Managed HSM domain suffix for environment %q", metadata.Client.Account.Environment.Name) + } - var model KeyVaultMHSMRoleDefinitionModel - if err = meta.Decode(&model); err != nil { + id, err := parse.ManagedHSMDataPlaneRoleDefinitionID(metadata.ResourceData.Id(), domainSuffix) + if err != nil { return err } - id, err := parse.NewManagedHSMRoleDefinitionID(model.VaultBaseUrl, roleDefinitionScope, model.Name) + subscriptionId := commonids.NewSubscriptionID(metadata.Client.Account.SubscriptionId) + managedHsmId, err := metadata.Client.ManagedHSMs.ManagedHSMIDFromBaseUrl(ctx, subscriptionId, id.BaseURI(), domainSuffix) if err != nil { - return err + return fmt.Errorf("determining the Managed HSM ID from the Base URI %q: %+v", id.BaseURI(), err) + } + if managedHsmId == nil { + return fmt.Errorf("unable to determine the Managed HSM ID from the Base URI %q: %+v", id.BaseURI(), err) } - locks.ByName(model.VaultBaseUrl, "azurerm_key_vault_managed_hardware_security_module") - defer locks.UnlockByName(model.VaultBaseUrl, "azurerm_key_vault_managed_hardware_security_module") + locks.ByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") + defer locks.UnlockByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") - existing, err := client.Get(ctx, id.VaultBaseUrl, id.Scope, id.Name) + var model KeyVaultMHSMRoleDefinitionModel + if err = metadata.Decode(&model); err != nil { + return err + } + + existing, err := client.Get(ctx, id.BaseURI(), id.Scope, id.RoleDefinitionName) if err != nil { if response.WasNotFound(existing.Response.Response) { return fmt.Errorf("not found resource to update: %s", id) @@ -274,47 +410,94 @@ func (k KeyVaultMHSMRoleDefinitionResource) Update() sdk.ResourceFunc { return fmt.Errorf("retrieving role definition by name %s: %v", model.Name, err) } - var param keyvault.RoleDefinitionCreateParameters - param.Properties = &keyvault.RoleDefinitionProperties{} - prop := param.Properties - prop.RoleName = pointer.To(model.RoleName) - prop.Description = pointer.To(model.Description) - prop.RoleType = keyvault.RoleTypeCustomRole - prop.Permissions = expandKeyVaultMHSMRolePermissions(model.Permission) + payload := keyvault.RoleDefinitionCreateParameters{ + Properties: &keyvault.RoleDefinitionProperties{ + RoleName: pointer.To(model.RoleName), + Description: pointer.To(model.Description), + RoleType: keyvault.RoleTypeCustomRole, + Permissions: expandKeyVaultMHSMRolePermissions(model.Permission), + }, + } - _, err = client.CreateOrUpdate(ctx, model.VaultBaseUrl, roleDefinitionScope, model.Name, param) + _, err = client.CreateOrUpdate(ctx, id.BaseURI(), id.Scope, id.RoleDefinitionName, payload) if err != nil { - return fmt.Errorf("creating %s: %v", id.ID(), err) + return fmt.Errorf("updating %s: %v", id.ID(), err) } - meta.SetID(id) + metadata.SetID(id) return nil }, } } -func (k KeyVaultMHSMRoleDefinitionResource) Delete() sdk.ResourceFunc { +func (r KeyVaultMHSMRoleDefinitionResource) Delete() sdk.ResourceFunc { return sdk.ResourceFunc{ Timeout: 10 * time.Minute, - Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { - id, err := parse.ManagedHSMRoleDefinitionID(meta.ResourceData.Id()) + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ManagedHSMs.DataPlaneRoleDefinitionsClient + domainSuffix, ok := metadata.Client.Account.Environment.ManagedHSM.DomainSuffix() + if !ok { + return fmt.Errorf("could not determine Managed HSM domain suffix for environment %q", metadata.Client.Account.Environment.Name) + } + + id, err := parse.ManagedHSMDataPlaneRoleDefinitionID(metadata.ResourceData.Id(), domainSuffix) if err != nil { return err } - meta.Logger.Infof("deleting %s", id.ID()) - locks.ByName(id.VaultBaseUrl, "azurerm_key_vault_managed_hardware_security_module") - defer locks.UnlockByName(id.VaultBaseUrl, "azurerm_key_vault_managed_hardware_security_module") - if _, err = meta.Client.ManagedHSMs.DataPlaneRoleDefinitionsClient.Delete(ctx, id.VaultBaseUrl, id.Scope, id.Name); err != nil { + subscriptionId := commonids.NewSubscriptionID(metadata.Client.Account.SubscriptionId) + managedHsmId, err := metadata.Client.ManagedHSMs.ManagedHSMIDFromBaseUrl(ctx, subscriptionId, id.BaseURI(), domainSuffix) + if err != nil { + return fmt.Errorf("determining the Managed HSM ID from the Base URI %q: %+v", id.BaseURI(), err) + } + if managedHsmId == nil { + return fmt.Errorf("unable to determine the Managed HSM ID from the Base URI %q: %+v", id.BaseURI(), err) + } + + locks.ByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") + defer locks.UnlockByName(managedHsmId.ID(), "azurerm_key_vault_managed_hardware_security_module") + + // TODO: @manicminer: when migrating to go-azure-sdk, the SDK should auto-retry on 409 responses + // (these occur when a recently deleted assignment for the role has not yet fully replicated) + + if _, err = client.Delete(ctx, id.BaseURI(), id.Scope, id.RoleDefinitionName); err != nil { return fmt.Errorf("deleting %+v: %v", id, err) } + + deadline, ok := ctx.Deadline() + if !ok { + return fmt.Errorf("internal-error: context has no deadline") + } + stateConf := &pluginsdk.StateChangeConf{ + Pending: []string{"InProgress"}, + Target: []string{"NotFound"}, + Refresh: func() (interface{}, string, error) { + result, err := client.Get(ctx, id.BaseURI(), id.Scope, id.RoleDefinitionName) + if err != nil { + if response.WasNotFound(result.Response.Response) { + return result, "NotFound", nil + } + + return nil, "Error", err + } + + return result, "InProgress", nil + }, + ContinuousTargetOccurence: 5, + PollInterval: 5 * time.Second, + Timeout: time.Until(deadline), + } + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for deletion of %s: %+v", id, err) + } + return nil }, } } -func (k KeyVaultMHSMRoleDefinitionResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { - return validate.ManagedHSMRoleDefinitionId +func (r KeyVaultMHSMRoleDefinitionResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.ManagedHSMDataPlaneRoleDefinitionID } func expandKeyVaultMHSMRolePermissions(perms []Permission) *[]keyvault.Permission { diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_resource_test.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_resource_test.go index da5a998eef5c..579c875b4ccc 100644 --- a/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_resource_test.go +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_role_definition_resource_test.go @@ -6,8 +6,10 @@ package managedhsm_test import ( "context" "fmt" + "testing" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" @@ -16,22 +18,127 @@ import ( type KeyVaultMHSMRoleDefinitionResource struct{} +func testAccKeyVaultManagedHardwareSecurityModuleRoleDefinition_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_role_definition", "test") + r := KeyVaultMHSMRoleDefinitionResource{} + + data.ResourceSequentialTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.update(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func testAccKeyVaultManagedHardwareSecurityModuleRoleDefinition_legacyWithUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_role_definition", "test") + r := KeyVaultMHSMRoleDefinitionResource{} + + data.ResourceSequentialTest(t, r, []acceptance.TestStep{ + { + Config: r.legacy(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.legacyUpdate(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + // real test nested in TestAccKeyVaultManagedHardwareSecurityModule, only provide Exists logic here -func (k KeyVaultMHSMRoleDefinitionResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { - baseURL := state.Attributes["vault_base_url"] - id, err := parse.ManagedHSMRoleDefinitionID(state.ID) +func (r KeyVaultMHSMRoleDefinitionResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + domainSuffix, ok := client.Account.Environment.ManagedHSM.DomainSuffix() + if !ok { + return nil, fmt.Errorf("this Environment doesn't specify the Domain Suffix for Managed HSM") + } + id, err := parse.ManagedHSMDataPlaneRoleDefinitionID(state.ID, domainSuffix) if err != nil { return nil, err } - resp, err := client.ManagedHSMs.DataPlaneRoleDefinitionsClient.Get(ctx, baseURL, "/", id.Name) + + resp, err := client.ManagedHSMs.DataPlaneRoleDefinitionsClient.Get(ctx, id.BaseURI(), id.Scope, id.RoleDefinitionName) if err != nil { return nil, fmt.Errorf("retrieving Type %s: %+v", id, err) } return utils.Bool(resp.RoleDefinitionProperties != nil), nil } -func (k KeyVaultMHSMRoleDefinitionResource) withRoleDefinition(data acceptance.TestData) string { - hsm := KeyVaultManagedHardwareSecurityModuleResource{}.download(data, 3) +func (r KeyVaultMHSMRoleDefinitionResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +locals { + roleTestName = "c9562a52-2bd9-2671-3d89-cea5b4798a6b" +} + +resource "azurerm_key_vault_managed_hardware_security_module_role_definition" "test" { + name = local.roleTestName + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + role_name = "myRole%s" + description = "desc foo" + permission { + data_actions = [ + "Microsoft.KeyVault/managedHsm/keys/read/action", + "Microsoft.KeyVault/managedHsm/keys/write/action", + "Microsoft.KeyVault/managedHsm/keys/encrypt/action", + "Microsoft.KeyVault/managedHsm/keys/create", + "Microsoft.KeyVault/managedHsm/keys/delete", + ] + not_data_actions = [ + "Microsoft.KeyVault/managedHsm/roleAssignments/read/action", + ] + } +} +`, r.template(data), data.RandomString) +} + +func (r KeyVaultMHSMRoleDefinitionResource) update(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +locals { + roleTestName = "c9562a52-2bd9-2671-3d89-cea5b4798a6b" +} + +resource "azurerm_key_vault_managed_hardware_security_module_role_definition" "test" { + name = local.roleTestName + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + role_name = "myRole%s" + description = "desc foo2" + permission { + data_actions = [ + "Microsoft.KeyVault/managedHsm/keys/read/action", + "Microsoft.KeyVault/managedHsm/keys/write/action", + "Microsoft.KeyVault/managedHsm/keys/encrypt/action", + "Microsoft.KeyVault/managedHsm/keys/create", + ] + not_data_actions = [ + "Microsoft.KeyVault/managedHsm/roleAssignments/read/action", + "Microsoft.KeyVault/managedHsm/keys/delete", + ] + } +} +`, r.template(data), data.RandomString) +} + +func (r KeyVaultMHSMRoleDefinitionResource) legacy(data acceptance.TestData) string { return fmt.Sprintf(` @@ -58,11 +165,10 @@ resource "azurerm_key_vault_managed_hardware_security_module_role_definition" "t ] } } -`, hsm) +`, r.template(data)) } -func (k KeyVaultMHSMRoleDefinitionResource) withRoleDefinitionUpdate(data acceptance.TestData) string { - hsm := KeyVaultManagedHardwareSecurityModuleResource{}.download(data, 3) +func (r KeyVaultMHSMRoleDefinitionResource) legacyUpdate(data acceptance.TestData) string { return fmt.Sprintf(` @@ -89,5 +195,9 @@ resource "azurerm_key_vault_managed_hardware_security_module_role_definition" "t ] } } -`, hsm) +`, r.template(data)) +} + +func (r KeyVaultMHSMRoleDefinitionResource) template(data acceptance.TestData) string { + return KeyVaultManagedHardwareSecurityModuleResource{}.download(data, 3) } diff --git a/internal/services/managedhsm/migration/role_assignment_v0_parser.go b/internal/services/managedhsm/migration/role_assignment_v0_parser.go new file mode 100644 index 000000000000..7e4fd22624ca --- /dev/null +++ b/internal/services/managedhsm/migration/role_assignment_v0_parser.go @@ -0,0 +1,53 @@ +package migration + +import ( + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" +) + +type legacyV0RoleAssignmentId struct { + // @tombuildsstuff: For reasons I can't entirely ascertain it appears that we were making up a + // Data Plane URI for this, rather than using the existing one, hence the need for this state migration. + // + // Example Old Value: `https://tharvey-keyvault.managedhsm.azure.net///RoleAssignment/uuid-idshifds-fks` + // + // Example New Value: `https://tharvey-keyvault.managedhsm.azure.net/{scope}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName}` + + managedHSMName string + domainSuffix string + scope string + roleAssignmentName string +} + +func parseLegacyV0RoleAssignmentId(input string) (*legacyV0RoleAssignmentId, error) { + parsed, err := url.ParseRequestURI(input) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + endpoint, err := parse.ManagedHSMEndpoint(input, nil) + if err != nil { + return nil, fmt.Errorf("parsing endpoint from %q: %+v", input, err) + } + + path := strings.TrimPrefix(parsed.Path, "/") + split := strings.Split(path, "/RoleAssignment/") + if len(split) != 2 { + return nil, fmt.Errorf("expected a URI in the format `{scope}/RoleAssignment/{name}` but got %q", parsed.Path) + } + scope := split[0] + name := split[1] + if scope == "" || name == "" { + return nil, fmt.Errorf("expected a URI in the format `{scope}/RoleAssignment/{name}` but got %q", parsed.Path) + } + + return &legacyV0RoleAssignmentId{ + managedHSMName: endpoint.ManagedHSMName, + domainSuffix: endpoint.DomainSuffix, + scope: scope, + roleAssignmentName: name, + }, nil +} diff --git a/internal/services/managedhsm/migration/role_assignment_v0_parser_test.go b/internal/services/managedhsm/migration/role_assignment_v0_parser_test.go new file mode 100644 index 000000000000..a225c1a572c1 --- /dev/null +++ b/internal/services/managedhsm/migration/role_assignment_v0_parser_test.go @@ -0,0 +1,110 @@ +package migration + +import ( + "testing" +) + +func TestRoleAssignmentV0Parser(t *testing.T) { + testData := []struct { + input string + expected *legacyV0RoleAssignmentId + }{ + { + input: "", + expected: nil, + }, + { + // missing scope + input: "https://my-hsm.managedhsm.azure.net/RoleAssignment/test", + expected: nil, + }, + { + // missing role assignment name + input: "https://my-hsm.managedhsm.azure.net///RoleAssignment/", + expected: nil, + }, + { + // wrong legacy type + input: "https://my-hsm.managedhsm.azure.net///RoleDefinition/example", + expected: nil, + }, + { + // Public + input: "https://my-hsm.managedhsm.azure.net///RoleAssignment/test", + expected: &legacyV0RoleAssignmentId{ + managedHSMName: "my-hsm", + domainSuffix: "managedhsm.azure.net", + scope: "/", + roleAssignmentName: "test", + }, + }, + { + // Public - superfluous port + input: "https://my-hsm.managedhsm.azure.net:443///RoleAssignment/test", + expected: &legacyV0RoleAssignmentId{ + managedHSMName: "my-hsm", + domainSuffix: "managedhsm.azure.net", + scope: "/", + roleAssignmentName: "test", + }, + }, + { + // Public - invalid port + input: "https://my-hsm.managedhsm.azure.net:445///RoleAssignment/test", + expected: nil, + }, + { + input: "https://my-hsm.managedhsm.azure.cn///RoleAssignment/test", + expected: &legacyV0RoleAssignmentId{ + managedHSMName: "my-hsm", + domainSuffix: "managedhsm.azure.cn", + scope: "/", + roleAssignmentName: "test", + }, + }, + { + input: "https://my-hsm.managedhsm.usgovcloudapi.net///RoleAssignment/test", + expected: &legacyV0RoleAssignmentId{ + managedHSMName: "my-hsm", + domainSuffix: "managedhsm.usgovcloudapi.net", + scope: "/", + roleAssignmentName: "test", + }, + }, + } + for _, test := range testData { + t.Logf("Testing %q..", test.input) + actual, err := parseLegacyV0RoleAssignmentId(test.input) + if err != nil { + if test.expected == nil { + continue + } + + t.Fatalf("unexpected error: %+v", err) + } + + if test.expected == nil { + if actual == nil { + continue + } + + t.Fatalf("expected nothing but got %+v", actual) + } + if actual == nil { + t.Fatalf("expected %+v but got nil", test.expected) + } + + if test.expected.managedHSMName != actual.managedHSMName { + t.Fatalf("expected managedHSMName to be %q but got %q", test.expected.managedHSMName, actual.managedHSMName) + } + if test.expected.domainSuffix != actual.domainSuffix { + t.Fatalf("expected domainSuffix to be %q but got %q", test.expected.domainSuffix, actual.domainSuffix) + } + if test.expected.scope != actual.scope { + t.Fatalf("expected scope to be %q but got %q", test.expected.scope, actual.scope) + } + if test.expected.roleAssignmentName != actual.roleAssignmentName { + t.Fatalf("expected roleAssignmentName to be %q but got %q", test.expected.roleAssignmentName, actual.roleAssignmentName) + } + } +} diff --git a/internal/services/managedhsm/migration/role_assignment_v0_to_v1.go b/internal/services/managedhsm/migration/role_assignment_v0_to_v1.go new file mode 100644 index 000000000000..b58cdb9fc32e --- /dev/null +++ b/internal/services/managedhsm/migration/role_assignment_v0_to_v1.go @@ -0,0 +1,64 @@ +package migration + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +var _ pluginsdk.StateUpgrade = ManagedHSMRoleAssignmentV0ToV1{} + +type ManagedHSMRoleAssignmentV0ToV1 struct { +} + +func (m ManagedHSMRoleAssignmentV0ToV1) Schema() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "vault_base_url": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + "scope": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + "role_definition_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + "principal_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + "resource_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + } +} + +func (m ManagedHSMRoleAssignmentV0ToV1) UpgradeFunc() pluginsdk.StateUpgraderFunc { + return func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + oldIdRaw := rawState["id"].(string) + oldId, err := parseLegacyV0RoleAssignmentId(oldIdRaw) + if err != nil { + return rawState, fmt.Errorf("parsing the old Role Assignment ID %q: %+v", oldId, err) + } + + newId := parse.NewManagedHSMDataPlaneRoleAssignmentID(oldId.managedHSMName, oldId.domainSuffix, oldId.scope, oldId.roleAssignmentName).ID() + log.Printf("[DEBUG] Updating ID from %q to %q", oldIdRaw, newId) + rawState["id"] = newId + return rawState, nil + } +} diff --git a/internal/services/managedhsm/migration/role_definition_v0_parser.go b/internal/services/managedhsm/migration/role_definition_v0_parser.go new file mode 100644 index 000000000000..f453c993c277 --- /dev/null +++ b/internal/services/managedhsm/migration/role_definition_v0_parser.go @@ -0,0 +1,56 @@ +package migration + +import ( + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" +) + +type legacyV0RoleDefinitionId struct { + // @tombuildsstuff: For reasons I can't entirely ascertain it appears that we were making up a + // Data Plane URI for this, rather than using the existing one, hence the need for this state migration. + // + // Example Old Value: `https://tharvey-keyvault.managedhsm.azure.net///RoleDefinition/uuid-idshifds-fks` + // + // Example New Value: `https://tharvey-keyvault.managedhsm.azure.net/{scope}/providers/Microsoft.Authorization/roleDefinitions/{roleDefinitionName}` + // + // NOTE: that for Role Definition IDs at this time the only supported value for scope is `/` - however + // the Resource ID parser will handle any scope and the Data Source/Resource in question should limit as required. + + managedHSMName string + domainSuffix string + scope string + roleDefinitionName string +} + +func parseLegacyV0RoleDefinitionId(input string) (*legacyV0RoleDefinitionId, error) { + parsed, err := url.ParseRequestURI(input) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + endpoint, err := parse.ManagedHSMEndpoint(input, nil) + if err != nil { + return nil, fmt.Errorf("parsing endpoint from %q: %+v", input, err) + } + + path := strings.TrimPrefix(parsed.Path, "/") + split := strings.Split(path, "/RoleDefinition/") + if len(split) != 2 { + return nil, fmt.Errorf("expected a URI in the format `{scope}/RoleDefinition/{name}` but got %q", parsed.Path) + } + scope := split[0] + name := split[1] + if scope == "" || name == "" { + return nil, fmt.Errorf("expected a URI in the format `{scope}/RoleDefinition/{name}` but got %q", parsed.Path) + } + + return &legacyV0RoleDefinitionId{ + managedHSMName: endpoint.ManagedHSMName, + domainSuffix: endpoint.DomainSuffix, + scope: scope, + roleDefinitionName: name, + }, nil +} diff --git a/internal/services/managedhsm/migration/role_definition_v0_parser_test.go b/internal/services/managedhsm/migration/role_definition_v0_parser_test.go new file mode 100644 index 000000000000..935d9e931a43 --- /dev/null +++ b/internal/services/managedhsm/migration/role_definition_v0_parser_test.go @@ -0,0 +1,110 @@ +package migration + +import ( + "testing" +) + +func TestRoleDefinitionV0Parser(t *testing.T) { + testData := []struct { + input string + expected *legacyV0RoleDefinitionId + }{ + { + input: "", + expected: nil, + }, + { + // missing scope + input: "https://my-hsm.managedhsm.azure.net/RoleDefinition/test", + expected: nil, + }, + { + // missing role assignment name + input: "https://my-hsm.managedhsm.azure.net///RoleDefinition/", + expected: nil, + }, + { + // wrong legacy type + input: "https://my-hsm.managedhsm.azure.net///RoleAssignment/example", + expected: nil, + }, + { + // Public + input: "https://my-hsm.managedhsm.azure.net///RoleDefinition/test", + expected: &legacyV0RoleDefinitionId{ + managedHSMName: "my-hsm", + domainSuffix: "managedhsm.azure.net", + scope: "/", + roleDefinitionName: "test", + }, + }, + { + // Public - superfluous port + input: "https://my-hsm.managedhsm.azure.net:443///RoleDefinition/test", + expected: &legacyV0RoleDefinitionId{ + managedHSMName: "my-hsm", + domainSuffix: "managedhsm.azure.net", + scope: "/", + roleDefinitionName: "test", + }, + }, + { + // Public - invalid port + input: "https://my-hsm.managedhsm.azure.net:445///RoleDefinition/test", + expected: nil, + }, + { + input: "https://my-hsm.managedhsm.azure.cn///RoleDefinition/test", + expected: &legacyV0RoleDefinitionId{ + managedHSMName: "my-hsm", + domainSuffix: "managedhsm.azure.cn", + scope: "/", + roleDefinitionName: "test", + }, + }, + { + input: "https://my-hsm.managedhsm.usgovcloudapi.net///RoleDefinition/test", + expected: &legacyV0RoleDefinitionId{ + managedHSMName: "my-hsm", + domainSuffix: "managedhsm.usgovcloudapi.net", + scope: "/", + roleDefinitionName: "test", + }, + }, + } + for _, test := range testData { + t.Logf("Testing %q..", test.input) + actual, err := parseLegacyV0RoleDefinitionId(test.input) + if err != nil { + if test.expected == nil { + continue + } + + t.Fatalf("unexpected error: %+v", err) + } + + if test.expected == nil { + if actual == nil { + continue + } + + t.Fatalf("expected nothing but got %+v", actual) + } + if actual == nil { + t.Fatalf("expected %+v but got nil", test.expected) + } + + if test.expected.managedHSMName != actual.managedHSMName { + t.Fatalf("expected managedHSMName to be %q but got %q", test.expected.managedHSMName, actual.managedHSMName) + } + if test.expected.domainSuffix != actual.domainSuffix { + t.Fatalf("expected domainSuffix to be %q but got %q", test.expected.domainSuffix, actual.domainSuffix) + } + if test.expected.scope != actual.scope { + t.Fatalf("expected scope to be %q but got %q", test.expected.scope, actual.scope) + } + if test.expected.roleDefinitionName != actual.roleDefinitionName { + t.Fatalf("expected roleDefinitionName to be %q but got %q", test.expected.roleDefinitionName, actual.roleDefinitionName) + } + } +} diff --git a/internal/services/managedhsm/migration/role_definition_v0_to_v1.go b/internal/services/managedhsm/migration/role_definition_v0_to_v1.go new file mode 100644 index 000000000000..7e288d173e7a --- /dev/null +++ b/internal/services/managedhsm/migration/role_definition_v0_to_v1.go @@ -0,0 +1,108 @@ +package migration + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +var _ pluginsdk.StateUpgrade = ManagedHSMRoleDefinitionV0ToV1{} + +type ManagedHSMRoleDefinitionV0ToV1 struct { +} + +func (m ManagedHSMRoleDefinitionV0ToV1) Schema() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + + "role_name": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "vault_base_url": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + + "permission": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "actions": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "not_actions": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "data_actions": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + Set: pluginsdk.HashString, + }, + + "not_data_actions": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + Set: pluginsdk.HashString, + }, + }, + }, + }, + + "description": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "role_type": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "resource_manager_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + } +} + +func (m ManagedHSMRoleDefinitionV0ToV1) UpgradeFunc() pluginsdk.StateUpgraderFunc { + return func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + oldIdRaw := rawState["id"].(string) + oldId, err := parseLegacyV0RoleDefinitionId(oldIdRaw) + if err != nil { + return rawState, fmt.Errorf("parsing the old Role Definition ID %q: %+v", oldId, err) + } + + newId := parse.NewManagedHSMDataPlaneRoleDefinitionID(oldId.managedHSMName, oldId.domainSuffix, oldId.scope, oldId.roleDefinitionName).ID() + log.Printf("[DEBUG] Updating ID from %q to %q", oldIdRaw, newId) + rawState["id"] = newId + return rawState, nil + } +} diff --git a/internal/services/managedhsm/parse/data_plane_helpers.go b/internal/services/managedhsm/parse/data_plane_helpers.go new file mode 100644 index 000000000000..dc858e51184d --- /dev/null +++ b/internal/services/managedhsm/parse/data_plane_helpers.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import ( + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" +) + +type ManagedHSMDataPlaneEndpoint struct { + ManagedHSMName string + DomainSuffix string +} + +func (e ManagedHSMDataPlaneEndpoint) BaseURI() string { + return fmt.Sprintf("https://%s.%s/", e.ManagedHSMName, e.DomainSuffix) +} + +func ManagedHSMEndpoint(input string, domainSuffix *string) (*ManagedHSMDataPlaneEndpoint, error) { + // NOTE: this function can be removed in 4.0 + uri, err := url.Parse(input) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + return parseDataPlaneEndpoint(uri, domainSuffix) +} + +func parseDataPlaneEndpoint(input *url.URL, domainSuffix *string) (*ManagedHSMDataPlaneEndpoint, error) { + if input.Scheme != "https" { + return nil, fmt.Errorf("expected the scheme to be `https` but got %q", input.Scheme) + } + + hostname := strings.ToLower(input.Host) + if port := input.Port(); port != "" { + if port != "443" { + return nil, fmt.Errorf("expected port to be '443' but got %q", port) + } + hostname = strings.TrimSuffix(hostname, fmt.Sprintf(":%s", input.Port())) + } + hostnameComponents := strings.Split(hostname, ".") + if len(hostnameComponents) <= 2 { + return nil, fmt.Errorf("expected the hostname to be in the format `{name}.{managedhsm}.{domain}` but got %q - please check this is a Managed HSM ID", hostname) + } + + managedHSMName := hostnameComponents[0] + if hostnameComponents[1] != "managedhsm" { + return nil, fmt.Errorf("expected the hostname to contain `.managedhsm.` but got %q - please check this is a Managed HSM ID", hostname) + } + + parsedDomainSuffix := strings.Join(hostnameComponents[1:], ".") + if domainSuffix != nil && parsedDomainSuffix != *domainSuffix { + // if we know the domain suffix, let's check that's what we're expecting + return nil, fmt.Errorf("expected the hostname to end be in the format `{name}.%s` but got %q", *domainSuffix, hostname) + } + + return &ManagedHSMDataPlaneEndpoint{ + ManagedHSMName: managedHSMName, + DomainSuffix: parsedDomainSuffix, + }, nil +} + +type dataPlaneResource struct { + itemName string + itemVersion *string +} + +func parseDataPlaneResource(input *url.URL, expectedType string, requireVersion bool) (*dataPlaneResource, error) { + expectedSegments := 2 + expectedFormatExample := "/%s/{name}" + if requireVersion { + expectedSegments = 3 + expectedFormatExample = "/%s/{name}/{version}" + } + + // then we want to check the path, which should be in the format described above + path := strings.Split(strings.TrimPrefix(input.Path, "/"), "/") + if len(path) != expectedSegments { + return nil, fmt.Errorf("expected the path to be in the format %q but got %q", expectedFormatExample, input.Path) + } + + nestedItemType := path[0] + if nestedItemType != expectedType { + return nil, fmt.Errorf("expected the Nested Item Type to be %q but got %q", expectedType, nestedItemType) + } + output := dataPlaneResource{ + itemName: path[1], + itemVersion: nil, + } + if err := validateSegment(output.itemName); err != nil { + return nil, fmt.Errorf("expected the path to be in the format %q but %+v", expectedFormatExample, err) + } + + if requireVersion { + itemVersion := path[2] + if err := validateSegment(itemVersion); err != nil { + return nil, fmt.Errorf("expected the path to be in the format %q but %+v", expectedFormatExample, err) + } + output.itemVersion = pointer.To(itemVersion) + } + + return &output, nil +} + +func validateSegment(input string) error { + val := strings.TrimSpace(input) + if val == "" { + return fmt.Errorf("unexpected empty value") + } + if val != input { + return fmt.Errorf("contained extra whitespace") + } + + return nil +} diff --git a/internal/services/managedhsm/parse/managed_hsm_data_plane_role_assignment.go b/internal/services/managedhsm/parse/managed_hsm_data_plane_role_assignment.go new file mode 100644 index 000000000000..883d8f21388b --- /dev/null +++ b/internal/services/managedhsm/parse/managed_hsm_data_plane_role_assignment.go @@ -0,0 +1,150 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import ( + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +// @tombuildsstuff: note this intentionally implements `resourceids.Id` and not `resourceids.ResourceId` since we +// can't currently represent the full URI in the Segment types until https://github.com/hashicorp/go-azure-helpers/issues/187 +// is implemented (including having Pandora be aware of the new segment types) +var _ resourceids.Id = ManagedHSMDataPlaneRoleAssignmentId{} + +type ManagedHSMDataPlaneRoleAssignmentId struct { + ManagedHSMName string + DomainSuffix string + Scope string + RoleAssignmentName string +} + +func NewManagedHSMDataPlaneRoleAssignmentID(managedHSMName, domainSuffix, scope, roleAssignmentName string) ManagedHSMDataPlaneRoleAssignmentId { + return ManagedHSMDataPlaneRoleAssignmentId{ + ManagedHSMName: managedHSMName, + DomainSuffix: domainSuffix, + Scope: scope, + RoleAssignmentName: roleAssignmentName, + } +} + +func ManagedHSMDataPlaneRoleAssignmentID(input string, domainSuffix *string) (*ManagedHSMDataPlaneRoleAssignmentId, error) { + if input == "" { + return nil, fmt.Errorf("`input` was empty") + } + if domainSuffix != nil && !strings.HasPrefix(strings.ToLower(*domainSuffix), "managedhsm.") { + return nil, fmt.Errorf("internal-error: the domainSuffix for Managed HSM %q didn't contain `managedhsm.`", *domainSuffix) + } + + uri, err := url.Parse(input) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + // we need the ManagedHSMName and DomainSuffix from the Host + endpoint, err := parseDataPlaneEndpoint(uri, domainSuffix) + if err != nil { + // intentionally not wrapping this + return nil, err + } + + // and then the Scope and RoleAssignmentName from the URI + if !strings.HasPrefix(uri.Path, "/") { + // sanity-checking, but we're expecting at least a `//` on the front + return nil, fmt.Errorf("expected the path to start with `//` but got %q", uri.Path) + } + pathRaw := strings.TrimPrefix(uri.Path, "/") + path, err := parseManagedHSMRoleAssignmentFromPath(pathRaw) + if err != nil { + return nil, err + } + + return &ManagedHSMDataPlaneRoleAssignmentId{ + ManagedHSMName: endpoint.ManagedHSMName, + DomainSuffix: endpoint.DomainSuffix, + Scope: path.scope, + RoleAssignmentName: path.roleAssignmentName, + }, nil +} + +func (id ManagedHSMDataPlaneRoleAssignmentId) BaseURI() string { + return ManagedHSMDataPlaneEndpoint{ + ManagedHSMName: id.ManagedHSMName, + DomainSuffix: id.DomainSuffix, + }.BaseURI() +} + +func (id ManagedHSMDataPlaneRoleAssignmentId) ID() string { + path := managedHSMRoleAssignmentPathId{ + scope: id.Scope, + roleAssignmentName: id.RoleAssignmentName, + } + return fmt.Sprintf("https://%s.%s%s", id.ManagedHSMName, id.DomainSuffix, path.ID()) +} + +func (id ManagedHSMDataPlaneRoleAssignmentId) String() string { + components := []string{ + fmt.Sprintf("Managed HSM Name %q", id.ManagedHSMName), + fmt.Sprintf("Domain Suffix Name %q", id.DomainSuffix), + fmt.Sprintf("Scope %q", id.Scope), + fmt.Sprintf("Role Assignment Name %q", id.RoleAssignmentName), + } + return fmt.Sprintf("Managed HSM Data Plane Role Assignment ID (%s)", strings.Join(components, " | ")) +} + +var _ resourceids.ResourceId = &managedHSMRoleAssignmentPathId{} + +// managedHSMRoleAssignmentPathId parses the Path component +type managedHSMRoleAssignmentPathId struct { + scope string + roleAssignmentName string +} + +func parseManagedHSMRoleAssignmentFromPath(input string) (*managedHSMRoleAssignmentPathId, error) { + id := managedHSMRoleAssignmentPathId{} + parsed, err := resourceids.NewParserFromResourceIdType(&id).Parse(input, false) + if err != nil { + return nil, err + } + + if err := id.FromParseResult(*parsed); err != nil { + return nil, err + } + + return &id, nil +} + +func (id *managedHSMRoleAssignmentPathId) FromParseResult(parsed resourceids.ParseResult) error { + var ok bool + if id.scope, ok = parsed.Parsed["scope"]; !ok { + return resourceids.NewSegmentNotSpecifiedError(id, "scope", parsed) + } + if id.roleAssignmentName, ok = parsed.Parsed["roleAssignmentName"]; !ok { + return resourceids.NewSegmentNotSpecifiedError(id, "roleAssignmentName", parsed) + } + + return nil +} + +func (id *managedHSMRoleAssignmentPathId) ID() string { + return fmt.Sprintf("/%s/providers/Microsoft.Authorization/roleAssignments/%s", id.scope, id.roleAssignmentName) +} + +func (id *managedHSMRoleAssignmentPathId) String() string { + return fmt.Sprintf("Role Assignment %q (Scope %q)", id.roleAssignmentName, id.scope) +} + +func (id *managedHSMRoleAssignmentPathId) Segments() []resourceids.Segment { + return []resourceids.Segment{ + // /{scope}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName} + resourceids.ScopeSegment("scope", "/"), + resourceids.StaticSegment("providers", "providers", "providers"), + resourceids.ResourceProviderSegment("resourceProvider", "Microsoft.Authorization", "Microsoft.Authorization"), + resourceids.StaticSegment("roleAssignments", "roleAssignments", "roleAssignments"), + resourceids.UserSpecifiedSegment("roleAssignmentName", "roleAssignmentValue"), + } +} diff --git a/internal/services/managedhsm/parse/managed_hsm_data_plane_role_assignment_test.go b/internal/services/managedhsm/parse/managed_hsm_data_plane_role_assignment_test.go new file mode 100644 index 000000000000..fb5794ac9ec4 --- /dev/null +++ b/internal/services/managedhsm/parse/managed_hsm_data_plane_role_assignment_test.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import "testing" + +func TestManagedHSMDataPlaneRoleAssignmentID(t *testing.T) { + cases := []struct { + input string + expected *ManagedHSMDataPlaneRoleAssignmentId + }{ + { + input: "", + expected: nil, + }, + { + // missing the path + input: "https://my-hsm.managedhsm.azure.net/", + expected: nil, + }, + { + // scope but no middle component or role assignment name + input: "https://my-hsm.managedhsm.azure.net///test", + expected: nil, + }, + { + // missing role assignment name + input: "https://my-hsm.managedhsm.azure.net////providers/Microsoft.Authorization/roleAssignments/", + expected: nil, + }, + { + // Key Vault URIs are not valid + input: "https://my-hsm.vault.azure.net///providers/Microsoft.Authorization/roleAssignments/test", + expected: nil, + }, + { + input: "https://my-hsm.managedhsm.azure.net///providers/Microsoft.Authorization/roleAssignments/test", + expected: &ManagedHSMDataPlaneRoleAssignmentId{ + ManagedHSMName: "my-hsm", + DomainSuffix: "managedhsm.azure.net", + Scope: "/", + RoleAssignmentName: "test", + }, + }, + { + input: "https://my-hsm.managedhsm.azure.net//keys/providers/Microsoft.Authorization/roleAssignments/1492", + expected: &ManagedHSMDataPlaneRoleAssignmentId{ + ManagedHSMName: "my-hsm", + DomainSuffix: "managedhsm.azure.net", + Scope: "/keys", + RoleAssignmentName: "1492", + }, + }, + { + input: "https://my-hsm.managedhsm.azure.net//keys/abc123/providers/Microsoft.Authorization/roleAssignments/1492", + expected: &ManagedHSMDataPlaneRoleAssignmentId{ + ManagedHSMName: "my-hsm", + DomainSuffix: "managedhsm.azure.net", + Scope: "/keys/abc123", + RoleAssignmentName: "1492", + }, + }, + { + input: "https://my-hsm.managedhsm.azure.cn///providers/Microsoft.Authorization/roleAssignments/test", + expected: &ManagedHSMDataPlaneRoleAssignmentId{ + ManagedHSMName: "my-hsm", + DomainSuffix: "managedhsm.azure.cn", + Scope: "/", + RoleAssignmentName: "test", + }, + }, + { + input: "https://my-hsm.managedhsm.usgovcloudapi.net//keys/providers/Microsoft.Authorization/roleAssignments/1492", + expected: &ManagedHSMDataPlaneRoleAssignmentId{ + ManagedHSMName: "my-hsm", + DomainSuffix: "managedhsm.usgovcloudapi.net", + Scope: "/keys", + RoleAssignmentName: "1492", + }, + }, + { + // extra suffix at the end + input: "https://my-hsm.managedhsm.azure.net//keys//providers/Microsoft.Authorization/roleAssignments/1492/suffix", + expected: nil, + }, + { + // valid format but missing scope + input: "https://my-hsm.managedhsm.azure.net/providers/Microsoft.Authorization/roleDefinitions/000-000", + expected: nil, + }, + } + + for _, test := range cases { + t.Logf("Testing %q..", test.input) + actual, err := ManagedHSMDataPlaneRoleAssignmentID(test.input, nil) + if err != nil { + if test.expected == nil { + continue + } + + t.Fatalf("unexpected error: %+v", err) + } + if test.expected == nil { + if actual == nil { + continue + } + + t.Fatalf("expected nothing but got %+v", actual) + } + if actual == nil { + t.Fatalf("expected %+v but got nil", test.expected) + } + if test.expected.ManagedHSMName != actual.ManagedHSMName { + t.Fatalf("expected ManagedHSMName to be %q but got %q", test.expected.ManagedHSMName, actual.ManagedHSMName) + } + if test.expected.DomainSuffix != actual.DomainSuffix { + t.Fatalf("expected DomainSuffix to be %q but got %q", test.expected.DomainSuffix, actual.DomainSuffix) + } + if test.expected.Scope != actual.Scope { + t.Fatalf("expected Scope to be %q but got %q", test.expected.Scope, actual.Scope) + } + if test.expected.RoleAssignmentName != actual.RoleAssignmentName { + t.Fatalf("expected RoleAssignmentName to be %q but got %q", test.expected.RoleAssignmentName, actual.RoleAssignmentName) + } + } +} diff --git a/internal/services/managedhsm/parse/managed_hsm_data_plane_role_definition.go b/internal/services/managedhsm/parse/managed_hsm_data_plane_role_definition.go new file mode 100644 index 000000000000..d6ab3b08a96a --- /dev/null +++ b/internal/services/managedhsm/parse/managed_hsm_data_plane_role_definition.go @@ -0,0 +1,150 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import ( + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +// @tombuildsstuff: note this intentionally implements `resourceids.Id` and not `resourceids.ResourceId` since we +// can't currently represent the full URI in the Segment types until https://github.com/hashicorp/go-azure-helpers/issues/187 +// is implemented (including having Pandora be aware of the new segment types) +var _ resourceids.Id = ManagedHSMDataPlaneRoleDefinitionId{} + +type ManagedHSMDataPlaneRoleDefinitionId struct { + ManagedHSMName string + DomainSuffix string + Scope string + RoleDefinitionName string +} + +func NewManagedHSMDataPlaneRoleDefinitionID(managedHSMName, domainSuffix, scope, roleDefinitionName string) ManagedHSMDataPlaneRoleDefinitionId { + return ManagedHSMDataPlaneRoleDefinitionId{ + ManagedHSMName: managedHSMName, + DomainSuffix: domainSuffix, + Scope: scope, + RoleDefinitionName: roleDefinitionName, + } +} + +func ManagedHSMDataPlaneRoleDefinitionID(input string, domainSuffix *string) (*ManagedHSMDataPlaneRoleDefinitionId, error) { + if input == "" { + return nil, fmt.Errorf("`input` was empty") + } + if domainSuffix != nil && !strings.HasPrefix(strings.ToLower(*domainSuffix), "managedhsm.") { + return nil, fmt.Errorf("internal-error: the domainSuffix for Managed HSM %q didn't contain `managedhsm.`", *domainSuffix) + } + + uri, err := url.Parse(input) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + // we need the ManagedHSMName and DomainSuffix from the Host + endpoint, err := parseDataPlaneEndpoint(uri, domainSuffix) + if err != nil { + // intentionally not wrapping this + return nil, err + } + + // and then the Scope and RoleDefinitionName from the URI + if !strings.HasPrefix(uri.Path, "/") { + // sanity-checking, but we're expecting at least a `//` on the front + return nil, fmt.Errorf("expected the path to start with `//` but got %q", uri.Path) + } + pathRaw := strings.TrimPrefix(uri.Path, "/") + path, err := parseManagedHSMRoleDefinitionFromPath(pathRaw) + if err != nil { + return nil, err + } + + return &ManagedHSMDataPlaneRoleDefinitionId{ + ManagedHSMName: endpoint.ManagedHSMName, + DomainSuffix: endpoint.DomainSuffix, + Scope: path.scope, + RoleDefinitionName: path.roleDefinitionName, + }, nil +} + +func (id ManagedHSMDataPlaneRoleDefinitionId) BaseURI() string { + return ManagedHSMDataPlaneEndpoint{ + ManagedHSMName: id.ManagedHSMName, + DomainSuffix: id.DomainSuffix, + }.BaseURI() +} + +func (id ManagedHSMDataPlaneRoleDefinitionId) ID() string { + path := managedHSMRoleDefinitionPathId{ + scope: id.Scope, + roleDefinitionName: id.RoleDefinitionName, + } + return fmt.Sprintf("https://%s.%s%s", id.ManagedHSMName, id.DomainSuffix, path.ID()) +} + +func (id ManagedHSMDataPlaneRoleDefinitionId) String() string { + components := []string{ + fmt.Sprintf("Managed HSM Name %q", id.ManagedHSMName), + fmt.Sprintf("Domain Suffix Name %q", id.DomainSuffix), + fmt.Sprintf("Scope %q", id.Scope), + fmt.Sprintf("Role Definition Name %q", id.RoleDefinitionName), + } + return fmt.Sprintf("Managed HSM Data Plane Role Definition ID (%s)", strings.Join(components, " | ")) +} + +var _ resourceids.ResourceId = &managedHSMRoleDefinitionPathId{} + +// managedHSMRoleDefinitionPathId parses the Path component +type managedHSMRoleDefinitionPathId struct { + scope string + roleDefinitionName string +} + +func parseManagedHSMRoleDefinitionFromPath(input string) (*managedHSMRoleDefinitionPathId, error) { + id := managedHSMRoleDefinitionPathId{} + parsed, err := resourceids.NewParserFromResourceIdType(&id).Parse(input, false) + if err != nil { + return nil, err + } + + if err := id.FromParseResult(*parsed); err != nil { + return nil, err + } + + return &id, nil +} + +func (id *managedHSMRoleDefinitionPathId) FromParseResult(parsed resourceids.ParseResult) error { + var ok bool + if id.scope, ok = parsed.Parsed["scope"]; !ok { + return resourceids.NewSegmentNotSpecifiedError(id, "scope", parsed) + } + if id.roleDefinitionName, ok = parsed.Parsed["roleDefinitionName"]; !ok { + return resourceids.NewSegmentNotSpecifiedError(id, "roleDefinitionName", parsed) + } + + return nil +} + +func (id *managedHSMRoleDefinitionPathId) ID() string { + return fmt.Sprintf("/%s/providers/Microsoft.Authorization/roleDefinitions/%s", id.scope, id.roleDefinitionName) +} + +func (id *managedHSMRoleDefinitionPathId) String() string { + return fmt.Sprintf("Role Definition %q (Scope %q)", id.roleDefinitionName, id.scope) +} + +func (id *managedHSMRoleDefinitionPathId) Segments() []resourceids.Segment { + return []resourceids.Segment{ + // /{scope}/providers/Microsoft.Authorization/roleDefinitions/{roleDefinitionName} + resourceids.ScopeSegment("scope", "/"), + resourceids.StaticSegment("providers", "providers", "providers"), + resourceids.ResourceProviderSegment("resourceProvider", "Microsoft.Authorization", "Microsoft.Authorization"), + resourceids.StaticSegment("roleDefinitions", "roleDefinitions", "roleDefinitions"), + resourceids.UserSpecifiedSegment("roleDefinitionName", "roleDefinitionValue"), + } +} diff --git a/internal/services/managedhsm/parse/managed_hsm_data_plane_role_definition_test.go b/internal/services/managedhsm/parse/managed_hsm_data_plane_role_definition_test.go new file mode 100644 index 000000000000..63528c6f6936 --- /dev/null +++ b/internal/services/managedhsm/parse/managed_hsm_data_plane_role_definition_test.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import "testing" + +func TestManagedHSMDataPlaneRoleDefinitionID(t *testing.T) { + cases := []struct { + input string + expected *ManagedHSMDataPlaneRoleDefinitionId + }{ + { + input: "", + expected: nil, + }, + { + // missing the path + input: "https://my-hsm.managedhsm.azure.net/", + expected: nil, + }, + { + // scope but no middle component or role definition name + input: "https://my-hsm.managedhsm.azure.net///test", + expected: nil, + }, + { + // missing role definition name + input: "https://my-hsm.managedhsm.azure.net////providers/Microsoft.Authorization/roleDefinitions/", + expected: nil, + }, + { + // Key Vault URIs are not valid + input: "https://my-hsm.vault.azure.net///providers/Microsoft.Authorization/roleDefinitions/test", + expected: nil, + }, + { + // scope is missing + input: "https://my-hsm.managedhsm.azure.net//providers/Microsoft.Authorization/roleDefinitions/test", + expected: nil, + }, + { + input: "https://my-hsm.managedhsm.azure.net///providers/Microsoft.Authorization/roleDefinitions/test", + expected: &ManagedHSMDataPlaneRoleDefinitionId{ + ManagedHSMName: "my-hsm", + DomainSuffix: "managedhsm.azure.net", + Scope: "/", + RoleDefinitionName: "test", + }, + }, + { + input: "https://my-hsm.managedhsm.azure.net//keys/providers/Microsoft.Authorization/roleDefinitions/1492", + expected: &ManagedHSMDataPlaneRoleDefinitionId{ + ManagedHSMName: "my-hsm", + DomainSuffix: "managedhsm.azure.net", + Scope: "/keys", + RoleDefinitionName: "1492", + }, + }, + { + input: "https://my-hsm.managedhsm.azure.net//keys/abc123/providers/Microsoft.Authorization/roleDefinitions/1492", + expected: &ManagedHSMDataPlaneRoleDefinitionId{ + ManagedHSMName: "my-hsm", + DomainSuffix: "managedhsm.azure.net", + Scope: "/keys/abc123", + RoleDefinitionName: "1492", + }, + }, + { + input: "https://my-hsm.managedhsm.azure.cn///providers/Microsoft.Authorization/roleDefinitions/test", + expected: &ManagedHSMDataPlaneRoleDefinitionId{ + ManagedHSMName: "my-hsm", + DomainSuffix: "managedhsm.azure.cn", + Scope: "/", + RoleDefinitionName: "test", + }, + }, + { + input: "https://my-hsm.managedhsm.usgovcloudapi.net//keys/providers/Microsoft.Authorization/roleDefinitions/1492", + expected: &ManagedHSMDataPlaneRoleDefinitionId{ + ManagedHSMName: "my-hsm", + DomainSuffix: "managedhsm.usgovcloudapi.net", + Scope: "/keys", + RoleDefinitionName: "1492", + }, + }, + { + // extra suffix at the end + input: "https://my-hsm.managedhsm.azure.net//keys//providers/Microsoft.Authorization/roleDefinitions/1492/suffix", + expected: nil, + }, + { + // valid format but missing scope + input: "https://my-hsm.managedhsm.azure.net/providers/Microsoft.Authorization/roleDefinitions/000-000", + expected: nil, + }, + } + + for _, test := range cases { + t.Logf("Testing %q..", test.input) + actual, err := ManagedHSMDataPlaneRoleDefinitionID(test.input, nil) + if err != nil { + if test.expected == nil { + continue + } + t.Fatalf("unexpected error: %+v", err) + } + if test.expected == nil { + if actual == nil { + continue + } + t.Fatalf("expected nothing but got %+v", actual) + } + if actual == nil { + t.Fatalf("expected %+v but got nil", test.expected) + } + if test.expected.ManagedHSMName != actual.ManagedHSMName { + t.Fatalf("expected ManagedHSMName to be %q but got %q", test.expected.ManagedHSMName, actual.ManagedHSMName) + } + if test.expected.DomainSuffix != actual.DomainSuffix { + t.Fatalf("expected DomainSuffix to be %q but got %q", test.expected.DomainSuffix, actual.DomainSuffix) + } + if test.expected.Scope != actual.Scope { + t.Fatalf("expected Scope to be %q but got %q", test.expected.Scope, actual.Scope) + } + if test.expected.RoleDefinitionName != actual.RoleDefinitionName { + t.Fatalf("expected RoleDefinitionName to be %q but got %q", test.expected.RoleDefinitionName, actual.RoleDefinitionName) + } + } +} diff --git a/internal/services/managedhsm/parse/managed_hsm_data_plane_versioned_key.go b/internal/services/managedhsm/parse/managed_hsm_data_plane_versioned_key.go new file mode 100644 index 000000000000..09c9168f48de --- /dev/null +++ b/internal/services/managedhsm/parse/managed_hsm_data_plane_versioned_key.go @@ -0,0 +1,87 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import ( + "fmt" + "net/url" + "strings" +) + +// NOTE: whilst we wouldn't normally prefix a struct and parser name with the package name, given the +// similarities between Key Vault and Managed HSM resources, we're intentionally doing that to make +// this clear, regardless of the import alias used for this package. + +// ManagedHSMDataPlaneVersionedKeyId defines the Data Plane ID for a Managed HSM Key with a Version. +// Example format: `https://{name}.{domainSuffix}/keys/{keyName}/{keyVersion}` +// Example value: `https://example.managedhsm.azure.net/keys/bird/fdf067c93bbb4b22bff4d8b7a9a56217` +type ManagedHSMDataPlaneVersionedKeyId struct { + // ManagedHSMName specifies the Name of this Managed HSM. + ManagedHSMName string + + // DomainSuffix specifies the Domain Suffix used for Managed HSMs in the Azure Environment + // where the Managed HSM exists - in the format `managedhsm.azure.net`. + DomainSuffix string + + // KeyName specifies the name of this Managed HSM Key. + KeyName string + + // KeyVersion specifies the version of this Managed HSM Key. + KeyVersion string +} + +// NewManagedHSMDataPlaneVersionedKeyID returns a new instance of ManagedHSMDataPlaneVersionedKeyId with the specified values. +func NewManagedHSMDataPlaneVersionedKeyID(managedHsmName, domainSuffix, keyName, keyVersion string) ManagedHSMDataPlaneVersionedKeyId { + return ManagedHSMDataPlaneVersionedKeyId{ + ManagedHSMName: managedHsmName, + DomainSuffix: domainSuffix, + KeyName: keyName, + KeyVersion: keyVersion, + } +} + +// ManagedHSMDataPlaneVersionedKeyID parses the Data Plane Managed HSM Key ID which requires a Version. +func ManagedHSMDataPlaneVersionedKeyID(input string, domainSuffix *string) (*ManagedHSMDataPlaneVersionedKeyId, error) { + if input == "" { + return nil, fmt.Errorf("`input` was empty") + } + if domainSuffix != nil && !strings.HasPrefix(strings.ToLower(*domainSuffix), "managedhsm.") { + return nil, fmt.Errorf("internal-error: the domainSuffix for Managed HSM %q didn't contain `managedhsm.`", *domainSuffix) + } + + uri, err := url.Parse(input) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + endpoint, err := parseDataPlaneEndpoint(uri, domainSuffix) + if err != nil { + // intentionally not wrapping this + return nil, err + } + + const requireVersion = true + resource, err := parseDataPlaneResource(uri, "keys", requireVersion) + if err != nil { + // intentionally not wrapping this + return nil, err + } + + return &ManagedHSMDataPlaneVersionedKeyId{ + ManagedHSMName: endpoint.ManagedHSMName, + DomainSuffix: endpoint.DomainSuffix, + KeyName: resource.itemName, + KeyVersion: *resource.itemVersion, + }, nil +} + +// BaseUri returns the Base URI for this Managed HSM Data Plane Versioned Key +func (id ManagedHSMDataPlaneVersionedKeyId) BaseUri() string { + return fmt.Sprintf("https://%s.%s/", id.ManagedHSMName, id.DomainSuffix) +} + +// ID returns the full Resource ID for this Managed HSM Data Plane Versioned Key +func (id ManagedHSMDataPlaneVersionedKeyId) ID() string { + return fmt.Sprintf("https://%s.%s/keys/%s/%s", id.ManagedHSMName, id.DomainSuffix, id.KeyName, id.KeyVersion) +} diff --git a/internal/services/managedhsm/parse/managed_hsm_data_plane_versioned_key_test.go b/internal/services/managedhsm/parse/managed_hsm_data_plane_versioned_key_test.go new file mode 100644 index 000000000000..e2d71f617fa1 --- /dev/null +++ b/internal/services/managedhsm/parse/managed_hsm_data_plane_versioned_key_test.go @@ -0,0 +1,201 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" +) + +func TestParseManagedHSMDataPlaneVersionedKeyID_NoDomainSuffix_InvalidValuesFail(t *testing.T) { + values := []string{ + "", // empty = invalid + "https://example.com/keys/abc123/foobar", // Hostname is incomplete + "https://example.keyvault.azure.net/keys/abc123/foobar", // Key Vault Key + "https://example.managedhsm.azure.net/", // no path + "https://example.managedhsm.azure.net/keys/", // trailing slash - no key/no version + "https://example.managedhsm.azure.net/keys/abc123", // no version + "https://example.managedhsm.azure.net/keys/abc123/", // trailing slash but no version + "https://example.managedhsm.azure.net/keys/abc123/bcd234/foobar", // too many segments + "https://example.managedhsm.azure.net/numbers/abc123/bcd234", // wrong type + "http://example.managedhsm.azure.net:80/keys/abc123/123456789oijhgfdertyhj", // HTTP rather than HTTPS + } + for _, input := range values { + t.Logf("Validating %q", input) + actual, err := ManagedHSMDataPlaneVersionedKeyID(input, nil) + if err != nil { + continue + } + t.Fatalf("unexpected value for %q: %q", input, actual.ID()) + } +} + +func TestParseManagedHSMDataPlaneVersionedKeyID_NoDomainSuffix_ValidValues(t *testing.T) { + // NOTE: this scenario tests the Validation use-case - where a Domain Suffix isn't known + // since the Environment/Credentials aren't available until Provider initialization. + values := map[string]ManagedHSMDataPlaneVersionedKeyId{ + "https://example.managedhsm.azure.net/keys/abc123/123456789oijhgfdertyhj": { + // Public + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + KeyVersion: "123456789oijhgfdertyhj", + }, + "https://EXAMPLE.managedhsm.azure.net/keys/abc123/123456789oijhgfdertyhj": { + // Public but the uppercase name should be normalised + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + KeyVersion: "123456789oijhgfdertyhj", + }, + "https://example.managedhsm.azure.net:443/keys/abc123/123456789oijhgfdertyhj": { + // in this instance the domain suffix includes a port which should be removed + // this is a bug in the Azure API response data, so we should filter it out + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + KeyVersion: "123456789oijhgfdertyhj", + }, + "https://example.managedhsm.azure.cn/keys/abc123/123456789oijhgfdertyhj": { + // China + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.cn", + KeyName: "abc123", + KeyVersion: "123456789oijhgfdertyhj", + }, + "https://example.managedhsm.usgovcloudapi.net/keys/abc123/123456789oijhgfdertyhj": { + // US Gov + ManagedHSMName: "example", + DomainSuffix: "managedhsm.usgovcloudapi.net", + KeyName: "abc123", + KeyVersion: "123456789oijhgfdertyhj", + }, + } + for input, expected := range values { + t.Logf("Validating %q", input) + actual, err := ManagedHSMDataPlaneVersionedKeyID(input, nil) + if err != nil { + t.Fatalf("unexpected error: %+v", err.Error()) + } + + if actual.ManagedHSMName != expected.ManagedHSMName { + t.Fatalf("expected `ManagedHSMName` to be %q but got %q", expected.ManagedHSMName, actual.ManagedHSMName) + } + if actual.DomainSuffix != expected.DomainSuffix { + t.Fatalf("expected `DomainSuffix` to be %q but got %q", expected.DomainSuffix, actual.DomainSuffix) + } + if actual.KeyName != expected.KeyName { + t.Fatalf("expected `KeyName` to be %q but got %q", expected.KeyName, actual.KeyName) + } + if actual.KeyVersion != expected.KeyVersion { + t.Fatalf("expected `KeyVersion` to be %q but got %q", expected.KeyVersion, actual.KeyVersion) + } + } +} + +func TestParseManagedHSMDataPlaneVersionedKeyID_WithDomainSuffix_InvalidValuesFail(t *testing.T) { + values := []string{ + "", // empty = invalid + "https://example.com/keys/abc123/foobar", // Hostname is incomplete + "https://example.keyvault.azure.net/keys/abc123/foobar", // Key Vault Key + "https://example.managedhsm.azure.net/", // no path + "https://example.managedhsm.azure.net/keys/", // trailing slash - no key/no version + "https://example.managedhsm.azure.net/keys/abc123", // no version + "https://example.managedhsm.azure.net/keys/abc123/", // trailing slash but no version + "https://example.managedhsm.azure.net/keys/abc123/bcd234/foobar", // too many segments + "https://example.managedhsm.azure.net/numbers/abc123/bcd234", // wrong type + "http://example.managedhsm.azure.net:80/keys/abc123/123456789oijhgfdertyhj", // HTTP rather than HTTPS + "https://managedhsm.azure.net/keys/foo/bar", // hostname is only the domainSuffix + "https://example.managedhsm.some.domain/keys/foo/bar", // hostname doesn't contain the domainSuffix + } + for _, input := range values { + t.Logf("Validating %q", input) + actual, err := ManagedHSMDataPlaneVersionedKeyID(input, pointer.To("managedhsm.azure.net")) + if err == nil { + t.Fatalf("unexpected value for %q: %q", input, actual.ID()) + } + } +} + +func TestParseManagedHSMDataPlaneVersionedKeyID_WithDomainSuffix_ValidValues(t *testing.T) { + // NOTE: this scenario tests the Validation use-case - where a Domain Suffix isn't known + // since the Environment/Credentials aren't available until Provider initialization. + values := map[string]struct { + expected ManagedHSMDataPlaneVersionedKeyId + domainSuffix string + }{ + "https://example.managedhsm.azure.net/keys/abc123/123456789oijhgfdertyhj": { + // Public + domainSuffix: "managedhsm.azure.net", + expected: ManagedHSMDataPlaneVersionedKeyId{ + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + KeyVersion: "123456789oijhgfdertyhj", + }, + }, + "https://EXAMPLE.managedhsm.azure.net/keys/abc123/123456789oijhgfdertyhj": { + // Public but the uppercase name should be normalised + domainSuffix: "managedhsm.azure.net", + expected: ManagedHSMDataPlaneVersionedKeyId{ + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + KeyVersion: "123456789oijhgfdertyhj", + }, + }, + "https://example.managedhsm.azure.net:443/keys/abc123/123456789oijhgfdertyhj": { + // in this instance the domain suffix includes a port which should be removed + // this is a bug in the Azure API response data, so we should filter it out + domainSuffix: "managedhsm.azure.net", + expected: ManagedHSMDataPlaneVersionedKeyId{ + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + KeyVersion: "123456789oijhgfdertyhj", + }, + }, + "https://example.managedhsm.azure.cn/keys/abc123/123456789oijhgfdertyhj": { + // China + domainSuffix: "managedhsm.azure.cn", + expected: ManagedHSMDataPlaneVersionedKeyId{ + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.cn", + KeyName: "abc123", + KeyVersion: "123456789oijhgfdertyhj", + }, + }, + "https://example.managedhsm.usgovcloudapi.net/keys/abc123/123456789oijhgfdertyhj": { + // US Gov + domainSuffix: "managedhsm.usgovcloudapi.net", + expected: ManagedHSMDataPlaneVersionedKeyId{ + ManagedHSMName: "example", + DomainSuffix: "managedhsm.usgovcloudapi.net", + KeyName: "abc123", + KeyVersion: "123456789oijhgfdertyhj", + }, + }, + } + for input, item := range values { + t.Logf("Validating %q", input) + actual, err := ManagedHSMDataPlaneVersionedKeyID(input, pointer.To(item.domainSuffix)) + if err != nil { + t.Fatalf("unexpected error: %+v", err.Error()) + } + + if actual.ManagedHSMName != item.expected.ManagedHSMName { + t.Fatalf("expected `ManagedHSMName` to be %q but got %q", item.expected.ManagedHSMName, actual.ManagedHSMName) + } + if actual.DomainSuffix != item.expected.DomainSuffix { + t.Fatalf("expected `DomainSuffix` to be %q but got %q", item.expected.DomainSuffix, actual.DomainSuffix) + } + if actual.KeyName != item.expected.KeyName { + t.Fatalf("expected `KeyName` to be %q but got %q", item.expected.KeyName, actual.KeyName) + } + if actual.KeyVersion != item.expected.KeyVersion { + t.Fatalf("expected `KeyVersion` to be %q but got %q", item.expected.KeyVersion, actual.KeyVersion) + } + } +} diff --git a/internal/services/managedhsm/parse/managed_hsm_data_plane_versionless_key.go b/internal/services/managedhsm/parse/managed_hsm_data_plane_versionless_key.go new file mode 100644 index 000000000000..63ad4789c7cc --- /dev/null +++ b/internal/services/managedhsm/parse/managed_hsm_data_plane_versionless_key.go @@ -0,0 +1,82 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import ( + "fmt" + "net/url" + "strings" +) + +// NOTE: whilst we wouldn't normally prefix a struct and parser name with the package name, given the +// similarities between Key Vault and Managed HSM resources, we're intentionally doing that to make +// this clear, regardless of the import alias used for this package. + +// ManagedHSMDataPlaneVersionlessKeyId defines the Data Plane ID for a Managed HSM Key without a Version. +// Example format: `https://{name}.{domainSuffix}/keys/{keyName}` +// Example value: `https://example.managedhsm.azure.net/keys/bird` +type ManagedHSMDataPlaneVersionlessKeyId struct { + // ManagedHSMName specifies the Name of this Managed HSM. + ManagedHSMName string + + // DomainSuffix specifies the Domain Suffix used for Managed HSMs in the Azure Environment + // where the Managed HSM exists - in the format `managedhsm.azure.net`. + DomainSuffix string + + // KeyName specifies the name of this Managed HSM Key. + KeyName string +} + +// NewManagedHSMDataPlaneVersionlessKeyID returns a new instance of ManagedHSMDataPlaneVersionlessKeyId with the specified values. +func NewManagedHSMDataPlaneVersionlessKeyID(managedHsmName, domainSuffix, keyName string) ManagedHSMDataPlaneVersionlessKeyId { + return ManagedHSMDataPlaneVersionlessKeyId{ + ManagedHSMName: managedHsmName, + DomainSuffix: domainSuffix, + KeyName: keyName, + } +} + +// ManagedHSMDataPlaneVersionlessKeyID parses the Data Plane Managed HSM Key ID without a Version. +func ManagedHSMDataPlaneVersionlessKeyID(input string, domainSuffix *string) (*ManagedHSMDataPlaneVersionlessKeyId, error) { + if input == "" { + return nil, fmt.Errorf("`input` was empty") + } + if domainSuffix != nil && !strings.HasPrefix(strings.ToLower(*domainSuffix), "managedhsm.") { + return nil, fmt.Errorf("internal-error: the domainSuffix for Managed HSM %q didn't contain `managedhsm.`", *domainSuffix) + } + + uri, err := url.Parse(input) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + endpoint, err := parseDataPlaneEndpoint(uri, domainSuffix) + if err != nil { + // intentionally not wrapping this + return nil, err + } + + const requireVersion = false + resource, err := parseDataPlaneResource(uri, "keys", requireVersion) + if err != nil { + // intentionally not wrapping this + return nil, err + } + + return &ManagedHSMDataPlaneVersionlessKeyId{ + ManagedHSMName: endpoint.ManagedHSMName, + DomainSuffix: endpoint.DomainSuffix, + KeyName: resource.itemName, + }, nil +} + +// BaseUri returns the Base URI for this Managed HSM Data Plane Versionless Key +func (id ManagedHSMDataPlaneVersionlessKeyId) BaseUri() string { + return fmt.Sprintf("https://%s.%s/", id.ManagedHSMName, id.DomainSuffix) +} + +// ID returns the full Resource ID for this Managed HSM Data Plane Versionless Key +func (id ManagedHSMDataPlaneVersionlessKeyId) ID() string { + return fmt.Sprintf("https://%s.%s/keys/%s", id.ManagedHSMName, id.DomainSuffix, id.KeyName) +} diff --git a/internal/services/managedhsm/parse/managed_hsm_data_plane_versionless_key_test.go b/internal/services/managedhsm/parse/managed_hsm_data_plane_versionless_key_test.go new file mode 100644 index 000000000000..57db5bd61c24 --- /dev/null +++ b/internal/services/managedhsm/parse/managed_hsm_data_plane_versionless_key_test.go @@ -0,0 +1,181 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" +) + +func TestParseManagedHSMDataPlaneVersionlessKeyID_NoDomainSuffix_InvalidValuesFail(t *testing.T) { + values := []string{ + "", // empty = invalid + "https://example.com/keys/abc123/foobar", // Hostname is incomplete + "https://example.keyvault.azure.net/keys/abc123", // Key Vault Key + "https://example.managedhsm.azure.net/", // no path + "https://example.managedhsm.azure.net/keys/", // trailing slash - no key + "https://example.managedhsm.azure.net/keys/abc123/bcd234", // with version + "https://example.managedhsm.azure.net/numbers/abc123/bcd234", // wrong type + "http://example.managedhsm.azure.net:80/keys/abc123", // HTTP rather than HTTPS + } + for _, input := range values { + t.Logf("Validating %q", input) + actual, err := ManagedHSMDataPlaneVersionlessKeyID(input, nil) + if err != nil { + continue + } + t.Fatalf("unexpected value for %q: %q", input, actual.ID()) + } +} + +func TestParseManagedHSMDataPlaneVersionlessKeyID_NoDomainSuffix_ValidValues(t *testing.T) { + // NOTE: this scenario tests the Validation use-case - where a Domain Suffix isn't known + // since the Environment/Credentials aren't available until Provider initialization. + values := map[string]ManagedHSMDataPlaneVersionlessKeyId{ + "https://example.managedhsm.azure.net/keys/abc123": { + // Public + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + }, + "https://EXAMPLE.managedhsm.azure.net/keys/abc123": { + // Public but the uppercase name should be normalised + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + }, + "https://example.managedhsm.azure.net:443/keys/abc123": { + // in this instance the domain suffix includes a port which should be removed + // this is a bug in the Azure API response data, so we should filter it out + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + }, + "https://example.managedhsm.azure.cn/keys/abc123": { + // China + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.cn", + KeyName: "abc123", + }, + "https://example.managedhsm.usgovcloudapi.net/keys/abc123": { + // US Gov + ManagedHSMName: "example", + DomainSuffix: "managedhsm.usgovcloudapi.net", + KeyName: "abc123", + }, + } + for input, expected := range values { + t.Logf("Validating %q", input) + actual, err := ManagedHSMDataPlaneVersionlessKeyID(input, nil) + if err != nil { + t.Fatalf("unexpected error: %+v", err.Error()) + } + + if actual.ManagedHSMName != expected.ManagedHSMName { + t.Fatalf("expected `ManagedHSMName` to be %q but got %q", expected.ManagedHSMName, actual.ManagedHSMName) + } + if actual.DomainSuffix != expected.DomainSuffix { + t.Fatalf("expected `DomainSuffix` to be %q but got %q", expected.DomainSuffix, actual.DomainSuffix) + } + if actual.KeyName != expected.KeyName { + t.Fatalf("expected `KeyName` to be %q but got %q", expected.KeyName, actual.KeyName) + } + } +} + +func TestParseManagedHSMDataPlaneVersionlessKeyID_WithDomainSuffix_InvalidValuesFail(t *testing.T) { + values := []string{ + "", // empty = invalid + "https://example.com/keys/abc123/foobar", // Hostname is incomplete + "https://example.keyvault.azure.net/keys/abc123", // Key Vault Key + "https://example.managedhsm.azure.net/", // no path + "https://example.managedhsm.azure.net/keys/", // trailing slash - no key + "https://example.managedhsm.azure.net/keys/abc123/bcd234", // with version + "https://example.managedhsm.azure.net/numbers/abc123/bcd234", // wrong type + "http://example.managedhsm.azure.net:80/keys/abc123", // HTTP rather than HTTPS + "https://managedhsm.azure.net/keys/foo", // hostname is only the domainSuffix + "https://example.managedhsm.some.domain/keys/foo", // hostname doesn't contain the domainSuffix + } + for _, input := range values { + t.Logf("Validating %q", input) + actual, err := ManagedHSMDataPlaneVersionlessKeyID(input, pointer.To("managedhsm.azure.net")) + if err == nil { + t.Fatalf("unexpected value for %q: %q", input, actual.ID()) + } + } +} + +func TestParseManagedHSMDataPlaneVersionlessKeyID_WithDomainSuffix_ValidValues(t *testing.T) { + // NOTE: this scenario tests the Validation use-case - where a Domain Suffix isn't known + // since the Environment/Credentials aren't available until Provider initialization. + values := map[string]struct { + expected ManagedHSMDataPlaneVersionlessKeyId + domainSuffix string + }{ + "https://example.managedhsm.azure.net/keys/abc123": { + // Public + domainSuffix: "managedhsm.azure.net", + expected: ManagedHSMDataPlaneVersionlessKeyId{ + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + }, + }, + "https://EXAMPLE.managedhsm.azure.net/keys/abc123": { + // Public but the uppercase name should be normalised + domainSuffix: "managedhsm.azure.net", + expected: ManagedHSMDataPlaneVersionlessKeyId{ + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + }, + }, + "https://example.managedhsm.azure.net:443/keys/abc123": { + // in this instance the domain suffix includes a port which should be removed + // this is a bug in the Azure API response data, so we should filter it out + domainSuffix: "managedhsm.azure.net", + expected: ManagedHSMDataPlaneVersionlessKeyId{ + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.net", + KeyName: "abc123", + }, + }, + "https://example.managedhsm.azure.cn/keys/abc123": { + // China + domainSuffix: "managedhsm.azure.cn", + expected: ManagedHSMDataPlaneVersionlessKeyId{ + ManagedHSMName: "example", + DomainSuffix: "managedhsm.azure.cn", + KeyName: "abc123", + }, + }, + "https://example.managedhsm.usgovcloudapi.net/keys/abc123": { + // US Gov + domainSuffix: "managedhsm.usgovcloudapi.net", + expected: ManagedHSMDataPlaneVersionlessKeyId{ + ManagedHSMName: "example", + DomainSuffix: "managedhsm.usgovcloudapi.net", + KeyName: "abc123", + }, + }, + } + for input, item := range values { + t.Logf("Validating %q", input) + actual, err := ManagedHSMDataPlaneVersionlessKeyID(input, pointer.To(item.domainSuffix)) + if err != nil { + t.Fatalf("unexpected error: %+v", err.Error()) + } + + if actual.ManagedHSMName != item.expected.ManagedHSMName { + t.Fatalf("expected `ManagedHSMName` to be %q but got %q", item.expected.ManagedHSMName, actual.ManagedHSMName) + } + if actual.DomainSuffix != item.expected.DomainSuffix { + t.Fatalf("expected `DomainSuffix` to be %q but got %q", item.expected.DomainSuffix, actual.DomainSuffix) + } + if actual.KeyName != item.expected.KeyName { + t.Fatalf("expected `KeyName` to be %q but got %q", item.expected.KeyName, actual.KeyName) + } + } +} diff --git a/internal/services/managedhsm/parse/managed_hsm_role_assignment_id.go b/internal/services/managedhsm/parse/managed_hsm_role_assignment_id.go deleted file mode 100644 index a27efc491b9e..000000000000 --- a/internal/services/managedhsm/parse/managed_hsm_role_assignment_id.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package parse - -import ( - "fmt" - "net/url" - "strings" - - "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" -) - -var _ resourceids.Id = ManagedHSMRoleAssignmentId{} - -type ManagedHSMRoleAssignmentId struct { - VaultBaseUrl string - Scope string - Name string -} - -func NewManagedHSMRoleAssignmentID(hsmBaseUrl, scope string, name string) (*ManagedHSMRoleAssignmentId, error) { - keyVaultUrl, err := url.Parse(hsmBaseUrl) - if err != nil || hsmBaseUrl == "" { - return nil, fmt.Errorf("parsing managedHSM nested itemID %q: %+v", hsmBaseUrl, err) - } - - return &ManagedHSMRoleAssignmentId{ - VaultBaseUrl: keyVaultUrl.String(), - Scope: scope, - Name: name, - }, nil -} - -func (n ManagedHSMRoleAssignmentId) ID() string { - // example: https://tharvey-keyvault.managedhsm.azure.net///RoleAssignment/uuid-idshifds-fks - segments := []string{ - strings.TrimSuffix(n.VaultBaseUrl, "/"), - n.Scope, - "RoleAssignment", - n.Name, - } - return strings.TrimSuffix(strings.Join(segments, "/"), "/") -} - -func (n ManagedHSMRoleAssignmentId) String() string { - return n.ID() -} - -func ManagedHSMRoleAssignmentID(input string) (*ManagedHSMRoleAssignmentId, error) { - idURL, err := url.ParseRequestURI(input) - 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, "/") - - nameSep := strings.LastIndex(path, "/") - if nameSep <= 0 { - return nil, fmt.Errorf("no name speparate exist in %s", input) - } - scope, name := path[:nameSep], path[nameSep+1:] - - typeSep := strings.LastIndex(scope, "/") - if typeSep <= 0 { - return nil, fmt.Errorf("no type speparate exist in %s", input) - } - scope, typ := scope[:typeSep], scope[typeSep+1:] - if typ != "RoleAssignment" { - return nil, fmt.Errorf("invalid type %s, must be 'RoleAssignment'", typ) - } - - childId := ManagedHSMRoleAssignmentId{ - VaultBaseUrl: fmt.Sprintf("%s://%s/", idURL.Scheme, idURL.Host), - Scope: scope, - Name: name, - } - - return &childId, nil -} diff --git a/internal/services/managedhsm/parse/managed_hsm_role_assignment_id_test.go b/internal/services/managedhsm/parse/managed_hsm_role_assignment_id_test.go deleted file mode 100644 index dde15a3472fb..000000000000 --- a/internal/services/managedhsm/parse/managed_hsm_role_assignment_id_test.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package parse - -import ( - "fmt" - "testing" -) - -func TestNewManagedHSMRoleAssignmentID(t *testing.T) { - mhsmType := "RoleDefinition" - cases := []struct { - Scenario string - keyVaultBaseUrl string - Expected string - Scope string - Name string - ExpectError bool - }{ - { - Scenario: "empty values", - keyVaultBaseUrl: "", - Expected: "", - ExpectError: true, - }, - { - Scenario: "valid, no port", - keyVaultBaseUrl: "https://test.managedhsm.azure.net", - Scope: "/", - Name: "test", - Expected: fmt.Sprintf("https://test.managedhsm.azure.net///%s/test", mhsmType), - ExpectError: false, - }, - { - Scenario: "valid, with port", - keyVaultBaseUrl: "https://test.managedhsm.azure.net:443", - Scope: "/keys", - Name: "test", - Expected: fmt.Sprintf("https://test.managedhsm.azure.net:443//keys/%s/test", mhsmType), - ExpectError: false, - }, - } - for idx, tc := range cases { - id, err := NewManagedHSMRoleDefinitionID(tc.keyVaultBaseUrl, tc.Scope, tc.Name) - if err != nil { - if !tc.ExpectError { - t.Fatalf("Got error for New Resource ID '%s': %+v", tc.keyVaultBaseUrl, err) - return - } - continue - } - if id.ID() != tc.Expected { - t.Fatalf("Expected %d id for %q to be %q, got %q", idx, tc.keyVaultBaseUrl, tc.Expected, id) - } - } -} - -func TestParseManagedHSMRoleAssignmentID(t *testing.T) { - typ := "RoleDefinition" - cases := []struct { - Input string - Expected ManagedHSMRoleDefinitionId - ExpectError bool - }{ - { - Input: "", - ExpectError: true, - }, - { - Input: fmt.Sprintf("https://my-keyvault.managedhsm.azure.net///%s/test", typ), - ExpectError: true, - Expected: ManagedHSMRoleDefinitionId{ - Name: "test", - VaultBaseUrl: "https://my-keyvault.managedhsm.azure.net/", - Scope: "/", - }, - }, - { - Input: fmt.Sprintf("https://my-keyvault.managedhsm.azure.net///%s/bird", typ), - ExpectError: true, - Expected: ManagedHSMRoleDefinitionId{ - Name: "bird", - VaultBaseUrl: "https://my-keyvault.managedhsm.azure.net/", - Scope: "/", - }, - }, - { - Input: fmt.Sprintf("https://my-keyvault.managedhsm.azure.net///%s/bird", typ), - ExpectError: false, - Expected: ManagedHSMRoleDefinitionId{ - Name: "bird", - VaultBaseUrl: "https://my-keyvault.managedhsm.azure.net/", - Scope: "/", - }, - }, - { - Input: fmt.Sprintf("https://my-keyvault.managedhsm.azure.net//keys/%s/world", typ), - ExpectError: false, - Expected: ManagedHSMRoleDefinitionId{ - Name: "world", - VaultBaseUrl: "https://my-keyvault.managedhsm.azure.net/", - Scope: "/keys", - }, - }, - { - Input: fmt.Sprintf("https://my-keyvault.managedhsm.azure.net//keys/%s/fdf067c93bbb4b22bff4d8b7a9a56217", typ), - ExpectError: true, - Expected: ManagedHSMRoleDefinitionId{ - Name: "fdf067c93bbb4b22bff4d8b7a9a56217", - VaultBaseUrl: "https://my-keyvault.managedhsm.azure.net/", - Scope: "/keys", - }, - }, - { - Input: "https://kvhsm23030816100222.managedhsm.azure.net///RoleDefinition/862d4d5e-bf01-11ed-a49d-00155d61ee9e", - ExpectError: true, - Expected: ManagedHSMRoleDefinitionId{ - Name: "862d4d5e-bf01-11ed-a49d-00155d61ee9e", - VaultBaseUrl: "https://kvhsm23030816100222.managedhsm.azure.net/", - Scope: "/", - }, - }, - } - - for idx, tc := range cases { - roleId, err := ManagedHSMRoleDefinitionID(tc.Input) - if err != nil { - if tc.ExpectError { - continue - } - - t.Fatalf("Got error for ID '%s': %+v", tc.Input, err) - } - - if roleId == nil { - t.Fatalf("Expected a SecretID to be parsed for ID '%s', got nil.", tc.Input) - } - - if tc.Expected.VaultBaseUrl != roleId.VaultBaseUrl { - t.Fatalf("Expected %d 'KeyVaultBaseUrl' to be '%s', got '%s' for ID '%s'", idx, tc.Expected.VaultBaseUrl, roleId.VaultBaseUrl, tc.Input) - } - - if tc.Expected.Name != roleId.Name { - t.Fatalf("Expected 'Name' to be '%s', got '%s' for ID '%s'", tc.Expected.Name, roleId.Name, tc.Input) - } - - if tc.Expected.Scope != roleId.Scope { - t.Fatalf("Expected 'Scope' to be '%s', got '%s' for ID '%s'", tc.Expected.Scope, roleId.Scope, tc.Input) - } - - if tc.Input != roleId.ID() { - t.Fatalf("Expected 'ID()' to be '%s', got '%s'", tc.Input, roleId.ID()) - } - } -} diff --git a/internal/services/managedhsm/parse/managed_hsm_role_definition_id.go b/internal/services/managedhsm/parse/managed_hsm_role_definition_id.go deleted file mode 100644 index 2862989e5461..000000000000 --- a/internal/services/managedhsm/parse/managed_hsm_role_definition_id.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package parse - -import ( - "fmt" - "net/url" - "strings" - - "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" -) - -var _ resourceids.Id = ManagedHSMRoleDefinitionId{} - -const ( -// TODO: this should be extended to support the other types of Nested Items available for a Managed HSM - -// RoleDefinitionType MHSMResourceType = "RoleDefinition" -// RoleAssignmentType MHSMResourceType = "RoleAssignment" -) - -type ManagedHSMRoleDefinitionId struct { - VaultBaseUrl string - Scope string - Name string -} - -func NewManagedHSMRoleDefinitionID(hsmBaseUrl, scope string, name string) (*ManagedHSMRoleDefinitionId, error) { - keyVaultUrl, err := url.Parse(hsmBaseUrl) - if err != nil || hsmBaseUrl == "" { - return nil, fmt.Errorf("parsing managedHSM nested itemID %q: %+v", hsmBaseUrl, err) - } - - return &ManagedHSMRoleDefinitionId{ - VaultBaseUrl: keyVaultUrl.String(), - Scope: scope, - Name: name, - }, nil -} - -func (n ManagedHSMRoleDefinitionId) ID() string { - // example: https://tharvey-keyvault.managedhsm.azure.net///uuid-idshifds-fks - segments := []string{ - strings.TrimSuffix(n.VaultBaseUrl, "/"), - n.Scope, - "RoleDefinition", - n.Name, - } - return strings.TrimSuffix(strings.Join(segments, "/"), "/") -} - -func (n ManagedHSMRoleDefinitionId) String() string { - return n.ID() -} - -func ManagedHSMRoleDefinitionID(input string) (*ManagedHSMRoleDefinitionId, error) { - idURL, err := url.ParseRequestURI(input) - 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, "/") - - nameSep := strings.LastIndex(path, "/") - if nameSep <= 0 { - return nil, fmt.Errorf("no name speparate exist in %s", input) - } - scope, name := path[:nameSep], path[nameSep+1:] - - typeSep := strings.LastIndex(scope, "/") - if typeSep <= 0 { - return nil, fmt.Errorf("no type speparate exist in %s", input) - } - scope, typ := scope[:typeSep], scope[typeSep+1:] - if typ != "RoleDefinition" { - return nil, fmt.Errorf("invalid type %s, must be 'RoleDefinition'", typ) - } - - childId := ManagedHSMRoleDefinitionId{ - VaultBaseUrl: fmt.Sprintf("%s://%s/", idURL.Scheme, idURL.Host), - Scope: scope, - Name: name, - } - - return &childId, nil -} diff --git a/internal/services/managedhsm/parse/managed_hsm_role_definition_id_test.go b/internal/services/managedhsm/parse/managed_hsm_role_definition_id_test.go deleted file mode 100644 index 342beef3ac37..000000000000 --- a/internal/services/managedhsm/parse/managed_hsm_role_definition_id_test.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package parse - -import ( - "fmt" - "testing" -) - -func TestNewManagedHSMRoleDefinitionID(t *testing.T) { - mhsmType := "RoleAssignment" - cases := []struct { - Scenario string - keyVaultBaseUrl string - Expected string - Scope string - Name string - ExpectError bool - }{ - { - Scenario: "empty values", - keyVaultBaseUrl: "", - Expected: "", - ExpectError: true, - }, - { - Scenario: "valid, no port", - keyVaultBaseUrl: "https://test.managedhsm.azure.net", - Scope: "/", - Name: "test", - Expected: fmt.Sprintf("https://test.managedhsm.azure.net///%s/test", mhsmType), - ExpectError: false, - }, - { - Scenario: "valid, with port", - keyVaultBaseUrl: "https://test.managedhsm.azure.net:443", - Scope: "/keys", - Name: "test", - Expected: fmt.Sprintf("https://test.managedhsm.azure.net:443//keys/%s/test", mhsmType), - ExpectError: false, - }, - } - for idx, tc := range cases { - id, err := NewManagedHSMRoleAssignmentID(tc.keyVaultBaseUrl, tc.Scope, tc.Name) - if err != nil { - if !tc.ExpectError { - t.Fatalf("Got error for New Resource ID '%s': %+v", tc.keyVaultBaseUrl, err) - return - } - continue - } - if id.ID() != tc.Expected { - t.Fatalf("Expected %d id for %q to be %q, got %q", idx, tc.keyVaultBaseUrl, tc.Expected, id) - } - } -} - -func TestParseManagedHSMRoleDefinitionID(t *testing.T) { - typ := "RoleAssignment" - cases := []struct { - Input string - Expected ManagedHSMRoleDefinitionId - ExpectError bool - }{ - { - Input: "", - ExpectError: true, - }, - { - Input: fmt.Sprintf("https://my-keyvault.managedhsm.azure.net///%s/test", typ), - ExpectError: true, - Expected: ManagedHSMRoleDefinitionId{ - Name: "test", - VaultBaseUrl: "https://my-keyvault.managedhsm.azure.net/", - Scope: "/", - }, - }, - { - Input: fmt.Sprintf("https://my-keyvault.managedhsm.azure.net///%s/bird", typ), - ExpectError: true, - Expected: ManagedHSMRoleDefinitionId{ - Name: "bird", - VaultBaseUrl: "https://my-keyvault.managedhsm.azure.net/", - Scope: "/", - }, - }, - { - Input: fmt.Sprintf("https://my-keyvault.managedhsm.azure.net///%s/bird", typ), - ExpectError: false, - Expected: ManagedHSMRoleDefinitionId{ - Name: "bird", - VaultBaseUrl: "https://my-keyvault.managedhsm.azure.net/", - Scope: "/", - }, - }, - { - Input: fmt.Sprintf("https://my-keyvault.managedhsm.azure.net//keys/%s/world", typ), - ExpectError: false, - Expected: ManagedHSMRoleDefinitionId{ - Name: "world", - VaultBaseUrl: "https://my-keyvault.managedhsm.azure.net/", - Scope: "/keys", - }, - }, - { - Input: fmt.Sprintf("https://my-keyvault.managedhsm.azure.net//keys/%s/fdf067c93bbb4b22bff4d8b7a9a56217", typ), - ExpectError: true, - Expected: ManagedHSMRoleDefinitionId{ - Name: "fdf067c93bbb4b22bff4d8b7a9a56217", - VaultBaseUrl: "https://my-keyvault.managedhsm.azure.net/", - Scope: "/keys", - }, - }, - { - Input: "https://kvhsm23030816100222.managedhsm.azure.net///RoleAssignment/862d4d5e-bf01-11ed-a49d-00155d61ee9e", - ExpectError: true, - Expected: ManagedHSMRoleDefinitionId{ - Name: "862d4d5e-bf01-11ed-a49d-00155d61ee9e", - VaultBaseUrl: "https://kvhsm23030816100222.managedhsm.azure.net/", - Scope: "/", - }, - }, - } - - for idx, tc := range cases { - roleId, err := ManagedHSMRoleAssignmentID(tc.Input) - if err != nil { - if tc.ExpectError { - continue - } - - t.Fatalf("Got error for ID '%s': %+v", tc.Input, err) - } - - if roleId == nil { - t.Fatalf("Expected a SecretID to be parsed for ID '%s', got nil.", tc.Input) - } - - if tc.Expected.VaultBaseUrl != roleId.VaultBaseUrl { - t.Fatalf("Expected %d 'KeyVaultBaseUrl' to be '%s', got '%s' for ID '%s'", idx, tc.Expected.VaultBaseUrl, roleId.VaultBaseUrl, tc.Input) - } - - if tc.Expected.Name != roleId.Name { - t.Fatalf("Expected 'Name' to be '%s', got '%s' for ID '%s'", tc.Expected.Name, roleId.Name, tc.Input) - } - - if tc.Expected.Scope != roleId.Scope { - t.Fatalf("Expected 'Scope' to be '%s', got '%s' for ID '%s'", tc.Expected.Scope, roleId.Scope, tc.Input) - } - - if tc.Input != roleId.ID() { - t.Fatalf("Expected 'ID()' to be '%s', got '%s'", tc.Input, roleId.ID()) - } - } -} diff --git a/internal/services/managedhsm/validate/mhsm_role_assignment_id.go b/internal/services/managedhsm/validate/managed_hsm_data_plane_role_assignment.go similarity index 69% rename from internal/services/managedhsm/validate/mhsm_role_assignment_id.go rename to internal/services/managedhsm/validate/managed_hsm_data_plane_role_assignment.go index 2d53bbe5db51..80a719026611 100644 --- a/internal/services/managedhsm/validate/mhsm_role_assignment_id.go +++ b/internal/services/managedhsm/validate/managed_hsm_data_plane_role_assignment.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" ) -func ManagedHSMRoleAssignmentId(i interface{}, k string) (warnings []string, errors []error) { +func ManagedHSMDataPlaneRoleAssignmentID(i interface{}, k string) (warnings []string, errors []error) { if warnings, errors = validation.StringIsNotEmpty(i, k); len(errors) > 0 { return warnings, errors } @@ -21,8 +21,8 @@ func ManagedHSMRoleAssignmentId(i interface{}, k string) (warnings []string, err return warnings, errors } - if _, err := parse.ManagedHSMRoleAssignmentID(v); err != nil { - errors = append(errors, fmt.Errorf("parsing %q: %s", v, err)) + if _, err := parse.ManagedHSMDataPlaneRoleAssignmentID(v, nil); err != nil { + errors = append(errors, fmt.Errorf("parsing %q: %+v", v, err)) return warnings, errors } diff --git a/internal/services/managedhsm/validate/managed_hsm_data_plane_role_assignment_test.go b/internal/services/managedhsm/validate/managed_hsm_data_plane_role_assignment_test.go new file mode 100644 index 000000000000..326ad19ceeaf --- /dev/null +++ b/internal/services/managedhsm/validate/managed_hsm_data_plane_role_assignment_test.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import ( + "testing" +) + +func TestManagedHSMDataPlaneRoleAssignmentID(t *testing.T) { + cases := []struct { + Input string + ExpectError bool + }{ + { + Input: "", + ExpectError: true, + }, + { + Input: "https://my-hsm.managedhsm.azure.net///test", + ExpectError: true, + }, + { + Input: "https://my-hsm.managedhsm.azure.net////providers/Microsoft.Authorization/roleAssignments/test", + ExpectError: false, + }, + { + Input: "https://my-hsm.managedhsm.azure.net//keys//providers/Microsoft.Authorization/roleAssignments/1492", + ExpectError: false, + }, + { + Input: "https://my-hsm.managedhsm.azure.cn////providers/Microsoft.Authorization/roleAssignments/test", + ExpectError: false, + }, + { + Input: "https://my-hsm.managedhsm.usgovcloudapi.net//keys//providers/Microsoft.Authorization/roleAssignments/1492", + ExpectError: false, + }, + { + Input: "https://my-hsm.managedhsm.azure.net//keys//providers/Microsoft.Authorization/roleAssignments/1492/suffix", + ExpectError: true, + }, + { + Input: "https://my-hsm.managedhsm.azure.net////providers/Microsoft.Authorization/roleDefinitions/000-000", + ExpectError: true, + }, + } + + for _, tc := range cases { + t.Logf("Testing %q..", tc.Input) + warnings, errors := ManagedHSMDataPlaneRoleAssignmentID(tc.Input, "example") + + if tc.ExpectError && len(errors) == 0 { + t.Fatalf("Got no errors for input %q but expected some", tc.Input) + } else if !tc.ExpectError && len(errors) > 0 { + t.Fatalf("Got %d errors for input %q when didn't expect any", len(errors), tc.Input) + } + if len(warnings) > 0 { + t.Fatalf("Got %d warnings for input %q when didn't expect any", len(warnings), tc.Input) + } + } +} diff --git a/internal/services/managedhsm/validate/mhsm_role_definition_id.go b/internal/services/managedhsm/validate/managed_hsm_data_plane_role_definition.go similarity index 69% rename from internal/services/managedhsm/validate/mhsm_role_definition_id.go rename to internal/services/managedhsm/validate/managed_hsm_data_plane_role_definition.go index 16385b4bf34c..2f93260a875e 100644 --- a/internal/services/managedhsm/validate/mhsm_role_definition_id.go +++ b/internal/services/managedhsm/validate/managed_hsm_data_plane_role_definition.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" ) -func ManagedHSMRoleDefinitionId(i interface{}, k string) (warnings []string, errors []error) { +func ManagedHSMDataPlaneRoleDefinitionID(i interface{}, k string) (warnings []string, errors []error) { if warnings, errors = validation.StringIsNotEmpty(i, k); len(errors) > 0 { return warnings, errors } @@ -21,8 +21,8 @@ func ManagedHSMRoleDefinitionId(i interface{}, k string) (warnings []string, err return warnings, errors } - if _, err := parse.ManagedHSMRoleDefinitionID(v); err != nil { - errors = append(errors, fmt.Errorf("parsing %q: %s", v, err)) + if _, err := parse.ManagedHSMDataPlaneRoleDefinitionID(v, nil); err != nil { + errors = append(errors, fmt.Errorf("parsing %q: %+v", v, err)) return warnings, errors } diff --git a/internal/services/managedhsm/validate/managed_hsm_data_plane_role_definition_test.go b/internal/services/managedhsm/validate/managed_hsm_data_plane_role_definition_test.go new file mode 100644 index 000000000000..75a485b076e5 --- /dev/null +++ b/internal/services/managedhsm/validate/managed_hsm_data_plane_role_definition_test.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import ( + "testing" +) + +func TestManagedHSMDataPlaneRoleDefinitionID(t *testing.T) { + cases := []struct { + Input string + ExpectError bool + }{ + { + Input: "", + ExpectError: true, + }, + { + Input: "https://my-hsm.managedhsm.azure.net///test", + ExpectError: true, + }, + { + Input: "https://my-hsm.managedhsm.azure.net////providers/Microsoft.Authorization/roleDefinitions/test", + ExpectError: false, + }, + { + Input: "https://my-hsm.managedhsm.azure.net//keys//providers/Microsoft.Authorization/roleDefinitions/1492", + ExpectError: false, + }, + { + Input: "https://my-hsm.managedhsm.azure.cn////providers/Microsoft.Authorization/roleDefinitions/test", + ExpectError: false, + }, + { + Input: "https://my-hsm.managedhsm.usgovcloudapi.net//keys//providers/Microsoft.Authorization/roleDefinitions/1492", + ExpectError: false, + }, + { + Input: "https://my-hsm.managedhsm.azure.net//keys//providers/Microsoft.Authorization/roleDefinitions/1492/suffix", + ExpectError: true, + }, + { + Input: "https://my-hsm.managedhsm.azure.net////providers/Microsoft.Authorization/roleAssignments/000-000", + ExpectError: true, + }, + } + + for _, tc := range cases { + t.Logf("Testing %q..", tc.Input) + warnings, errors := ManagedHSMDataPlaneRoleDefinitionID(tc.Input, "example") + + if tc.ExpectError && len(errors) == 0 { + t.Fatalf("Got no errors for input %q but expected some", tc.Input) + } else if !tc.ExpectError && len(errors) > 0 { + t.Fatalf("Got %d errors for input %q when didn't expect any", len(errors), tc.Input) + } + if len(warnings) > 0 { + t.Fatalf("Got %d warnings for input %q when didn't expect any", len(warnings), tc.Input) + } + } +} diff --git a/internal/services/managedhsm/validate/managed_hsm_data_plane_versioned_key.go b/internal/services/managedhsm/validate/managed_hsm_data_plane_versioned_key.go new file mode 100644 index 000000000000..dc233ead44e4 --- /dev/null +++ b/internal/services/managedhsm/validate/managed_hsm_data_plane_versioned_key.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" +) + +func ManagedHSMDataPlaneVersionedKeyID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + return warnings, append(errors, fmt.Errorf("expected type of %s to be string", k)) + } + + if _, err := parse.ManagedHSMDataPlaneVersionedKeyID(v, nil); err != nil { + errors = append(errors, fmt.Errorf("parsing %q as a Managed HSM Data Plane Versioned Key ID: %+v", v, err)) + } + + return warnings, errors +} diff --git a/internal/services/managedhsm/validate/managed_hsm_data_plane_versioned_key_test.go b/internal/services/managedhsm/validate/managed_hsm_data_plane_versioned_key_test.go new file mode 100644 index 000000000000..73aba78570a5 --- /dev/null +++ b/internal/services/managedhsm/validate/managed_hsm_data_plane_versioned_key_test.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import "testing" + +func TestManagedHSMDataPlaneVersionedKeyID(t *testing.T) { + testData := []struct { + input string + valid bool + }{ + { + // empty = invalid + input: "", + valid: false, + }, + { + // key vault versioned key id + input: "https://example.keyvault.azure.net/keys/abc123/bcd234", + valid: false, + }, + { + // domain but no uri + input: "https://example.keyvault.azure.net/", + valid: false, + }, + { + // managed hsm domain but wrong type + input: "https://example.managedhsm.azure.net/numbers/abc123/bcd234", + valid: false, + }, + { + // managed hsm key id (no version) + input: "https://example.managedhsm.azure.net/keys/abc123", + valid: false, + }, + { + // managed hsm key id (with version) + input: "https://example.managedhsm.azure.net/keys/abc123/bcd234", + valid: true, + }, + { + // managed hsm key id (with version but extra) + input: "https://example.managedhsm.azure.net/keys/abc123/bcd234/cde345", + valid: false, + }, + } + for _, item := range testData { + t.Logf("Testing %q", item.input) + warnings, errs := ManagedHSMDataPlaneVersionedKeyID(item.input, "some_field") + actual := len(warnings) == 0 && len(errs) == 0 + if item.valid != actual { + t.Fatalf("expected %t but got %t", item.valid, actual) + } + } +} diff --git a/internal/services/managedhsm/validate/managed_hsm_data_plane_versionless_key.go b/internal/services/managedhsm/validate/managed_hsm_data_plane_versionless_key.go new file mode 100644 index 000000000000..64733b93151d --- /dev/null +++ b/internal/services/managedhsm/validate/managed_hsm_data_plane_versionless_key.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" +) + +func ManagedHSMDataPlaneVersionlessKeyID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + return warnings, append(errors, fmt.Errorf("expected type of %s to be string", k)) + } + + if _, err := parse.ManagedHSMDataPlaneVersionlessKeyID(v, nil); err != nil { + errors = append(errors, fmt.Errorf("parsing %q as a Managed HSM Data Plane Versioned Key ID: %+v", v, err)) + } + + return warnings, errors +} diff --git a/internal/services/managedhsm/validate/managed_hsm_data_plane_versionless_key_test.go b/internal/services/managedhsm/validate/managed_hsm_data_plane_versionless_key_test.go new file mode 100644 index 000000000000..df4c9a72e96f --- /dev/null +++ b/internal/services/managedhsm/validate/managed_hsm_data_plane_versionless_key_test.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import "testing" + +func TestManagedHSMDataPlaneVersionlessKeyID(t *testing.T) { + testData := []struct { + input string + valid bool + }{ + { + // empty = invalid + input: "", + valid: false, + }, + { + // key vault key id + input: "https://example.keyvault.azure.net/keys/abc123", + valid: false, + }, + { + // domain but no uri + input: "https://example.keyvault.azure.net/", + valid: false, + }, + { + // managed hsm domain but wrong type + input: "https://example.managedhsm.azure.net/numbers/abc123", + valid: false, + }, + { + // managed hsm key id (no version) + input: "https://example.managedhsm.azure.net/keys/abc123", + valid: true, + }, + { + // managed hsm key id (with version) + input: "https://example.managedhsm.azure.net/keys/abc123/bcd234", + valid: false, + }, + } + for _, item := range testData { + t.Logf("Testing %q", item.input) + warnings, errs := ManagedHSMDataPlaneVersionlessKeyID(item.input, "some_field") + actual := len(warnings) == 0 && len(errs) == 0 + if item.valid != actual { + t.Fatalf("expected %t but got %t", item.valid, actual) + } + } +} diff --git a/internal/services/managedhsm/validate/mhsm_role_assignment_id_test.go b/internal/services/managedhsm/validate/mhsm_role_assignment_id_test.go deleted file mode 100644 index 48a0cbc9a0f8..000000000000 --- a/internal/services/managedhsm/validate/mhsm_role_assignment_id_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package validate - -import ( - "testing" -) - -func TestManagedHSMRoleAssignmentId(t *testing.T) { - cases := []struct { - Input string - ExpectError bool - }{ - { - Input: "", - ExpectError: true, - }, - { - Input: "https://my-hsm.managedhsm.azure.net///test", - ExpectError: true, - }, - { - Input: "https://my-hsm.managedhsm.azure.net///RoleAssignment/test", - ExpectError: false, - }, - { - Input: "https://my-hsm.managedhsm.azure.net//keys/RoleAssignment/1492", - ExpectError: false, - }, - { - Input: "https://my-hsm.managedhsm.azure.net//keys/RoleAssignment/1492/suffix", - ExpectError: true, - }, - { - Input: "https://my-hsm.managedhsm.azure.net///RoleDefinition/000-000", - ExpectError: true, - }, - } - - for _, tc := range cases { - warnings, err := ManagedHSMRoleAssignmentId(tc.Input, "example") - if err != nil { - if !tc.ExpectError { - t.Fatalf("Got error for input %q: %+v", tc.Input, err) - } - - continue - } - - 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) - } - } -} diff --git a/internal/services/managedhsm/validate/mhsm_role_definition_id_test.go b/internal/services/managedhsm/validate/mhsm_role_definition_id_test.go deleted file mode 100644 index b2a0bc938c52..000000000000 --- a/internal/services/managedhsm/validate/mhsm_role_definition_id_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package validate - -import ( - "testing" -) - -func TestManagedHSMRoleDefinitionId(t *testing.T) { - cases := []struct { - Input string - ExpectError bool - }{ - { - Input: "", - ExpectError: true, - }, - { - Input: "https://my-hsm.managedhsm.azure.net///test", - ExpectError: true, - }, - { - Input: "https://my-hsm.managedhsm.azure.net///RoleDefinition/test", - ExpectError: false, - }, - { - Input: "https://my-hsm.managedhsm.azure.net//keys/RoleDefinition/1492", - ExpectError: false, - }, - { - Input: "https://my-hsm.managedhsm.azure.net//keys/RoleDefinition/1492/suffix", - ExpectError: true, - }, - { - Input: "https://my-hsm.managedhsm.azure.net///RoleAssignment/000-000", - ExpectError: true, - }, - } - - for _, tc := range cases { - warnings, err := ManagedHSMRoleDefinitionId(tc.Input, "example") - if err != nil { - if !tc.ExpectError { - t.Fatalf("Got error for input %q: %+v", tc.Input, err) - } - - continue - } - - 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) - } - } -} diff --git a/website/docs/d/key_vault_managed_hardware_security_module_role_definition.html.markdown b/website/docs/d/key_vault_managed_hardware_security_module_role_definition.html.markdown index 189f7c4ac68f..1d173171e4c5 100644 --- a/website/docs/d/key_vault_managed_hardware_security_module_role_definition.html.markdown +++ b/website/docs/d/key_vault_managed_hardware_security_module_role_definition.html.markdown @@ -16,7 +16,6 @@ Use this data source to access information about an existing KeyVault Role Defin data "azurerm_key_vault_managed_hardware_security_module_role_definition" "example" { vault_base_url = azurerm_key_vault_managed_hardware_security_module.test.hsm_uri name = "21dbd100-6940-42c2-9190-5d6cb909625b" - scope = "/" } output "id" { diff --git a/website/docs/r/key_vault_managed_hardware_security_module_role_assignment.html.markdown b/website/docs/r/key_vault_managed_hardware_security_module_role_assignment.html.markdown index 946bd7d47c78..1e0041f9d050 100644 --- a/website/docs/r/key_vault_managed_hardware_security_module_role_assignment.html.markdown +++ b/website/docs/r/key_vault_managed_hardware_security_module_role_assignment.html.markdown @@ -21,7 +21,7 @@ data "azurerm_key_vault_managed_hardware_security_module_role_definition" "user" resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "example" { name = "a9dbe818-56e7-5878-c0ce-a1477692c1d6" - vault_base_url = azurerm_key_vault_managed_hardware_security_module.example.hsm_uri + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.example.id scope = "${data.azurerm_key_vault_managed_hardware_security_module_role_definition.user.scope}" role_definition_id = "${data.azurerm_key_vault_managed_hardware_security_module_role_definition.user.resource_id}" principal_id = "${data.azurerm_client_config.current.object_id}" @@ -32,6 +32,8 @@ resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "e The following arguments are supported: +* `managed_hsm_id` - (Required) The ID of a Managed Hardware Security Module resource. Changing this forces a new Managed Hardware Security Module to be created. +* * `name` - (Required) The name in GUID notation which should be used for this Managed Hardware Security Module Role Assignment. Changing this forces a new Managed Hardware Security Module to be created. * `principal_id` - (Required) The principal ID to be assigned to this role. It can point to a user, service principal, or security group. Changing this forces a new Managed Hardware Security Module to be created. @@ -40,7 +42,6 @@ The following arguments are supported: * `scope` - (Required) Specifies the scope to create the role assignment. Changing this forces a new Managed Hardware Security Module to be created. -* `vault_base_url` - (Required) The HSM URI of a Managed Hardware Security Module resource. Changing this forces a new Managed Hardware Security Module to be created. ## Attributes Reference @@ -48,7 +49,7 @@ In addition to the Arguments listed above - the following Attributes are exporte * `id` - The ID of the Managed Hardware Security Module Role Assignment with HSM Base URL. -* `resource_id` - The resource id of created assignment resource. +* `resource_id` - (Deprecated) The resource id of created assignment resource. ## Timeouts