diff --git a/azurerm/internal/services/kusto/registration.go b/azurerm/internal/services/kusto/registration.go index 805981e53a7a..7c6688bda6e9 100644 --- a/azurerm/internal/services/kusto/registration.go +++ b/azurerm/internal/services/kusto/registration.go @@ -21,6 +21,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ "azurerm_kusto_cluster": resourceArmKustoCluster(), "azurerm_kusto_database": resourceArmKustoDatabase(), + "azurerm_kusto_database_principal": resourceArmKustoDatabasePrincipal(), "azurerm_kusto_eventhub_data_connection": resourceArmKustoEventHubDataConnection(), } } diff --git a/azurerm/internal/services/kusto/resource_arm_kusto_database_principal.go b/azurerm/internal/services/kusto/resource_arm_kusto_database_principal.go new file mode 100644 index 000000000000..157745a9dcfe --- /dev/null +++ b/azurerm/internal/services/kusto/resource_arm_kusto_database_principal.go @@ -0,0 +1,324 @@ +package kusto + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/services/kusto/mgmt/2019-05-15/kusto" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmKustoDatabasePrincipal() *schema.Resource { + return &schema.Resource{ + Create: resourceArmKustoDatabasePrincipalCreate, + Read: resourceArmKustoDatabasePrincipalRead, + Delete: resourceArmKustoDatabasePrincipalDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(60 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(60 * time.Minute), + Delete: schema.DefaultTimeout(60 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "resource_group_name": azure.SchemaResourceGroupName(), + + "cluster_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAzureRMKustoClusterName, + }, + + "database_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAzureRMKustoDatabaseName, + }, + + "role": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + string(kusto.Admin), + string(kusto.Ingestor), + string(kusto.Monitor), + string(kusto.User), + string(kusto.UnrestrictedViewers), + string(kusto.Viewer), + }, false), + }, + + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + string(kusto.DatabasePrincipalTypeApp), + string(kusto.DatabasePrincipalTypeGroup), + string(kusto.DatabasePrincipalTypeUser), + }, false), + }, + + "client_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, + }, + + "object_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, + }, + + "fully_qualified_name": { + Type: schema.TypeString, + Computed: true, + }, + + // These must be computed as the values passed in are overwritten by what the `fqn` returns. + // For more info: https://github.com/Azure/azure-sdk-for-go/issues/6547 + "name": { + Type: schema.TypeString, + Computed: true, + }, + + "email": { + Type: schema.TypeString, + Computed: true, + }, + + "app_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceArmKustoDatabasePrincipalCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Kusto.DatabasesClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + log.Printf("[INFO] preparing arguments for Azure Kusto Database Principal creation.") + + resourceGroup := d.Get("resource_group_name").(string) + clusterName := d.Get("cluster_name").(string) + databaseName := d.Get("database_name").(string) + role := d.Get("role").(string) + principalType := d.Get("type").(string) + + clientID := d.Get("client_id").(string) + objectID := d.Get("object_id").(string) + fqn := "" + if principalType == "User" { + fqn = fmt.Sprintf("aaduser=%s;%s", objectID, clientID) + } else if principalType == "Group" { + fqn = fmt.Sprintf("aadgroup=%s;%s", objectID, clientID) + } else if principalType == "App" { + fqn = fmt.Sprintf("aadapp=%s;%s", objectID, clientID) + } + + database, err := client.Get(ctx, resourceGroup, clusterName, databaseName) + if err != nil { + if utils.ResponseWasNotFound(database.Response) { + return fmt.Errorf("Kusto Database %q (Resource Group %q) was not found", databaseName, resourceGroup) + } + + return fmt.Errorf("Error loading Kusto Database %q (Resource Group %q): %+v", databaseName, resourceGroup, err) + } + resourceId := fmt.Sprintf("%s/Role/%s/FQN/%s", *database.ID, role, fqn) + + if features.ShouldResourcesBeImported() && d.IsNewResource() { + resp, err := client.ListPrincipals(ctx, resourceGroup, clusterName, databaseName) + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error checking for presence of existing Kusto Database Principals (Resource Group %q, Cluster %q): %s", resourceGroup, clusterName, err) + } + } + + if principals := resp.Value; principals != nil { + for _, principal := range *principals { + // kusto database principals are unique when looked at with name and role + if string(principal.Role) == role && principal.Fqn != nil && *principal.Fqn == fqn { + return tf.ImportAsExistsError("azurerm_kusto_database_principal", resourceId) + } + } + } + } + + kustoPrincipal := kusto.DatabasePrincipal{ + Type: kusto.DatabasePrincipalType(principalType), + Role: kusto.DatabasePrincipalRole(role), + Fqn: utils.String(fqn), + // These three must be specified or the api returns `The request is invalid.` + // For more info: https://github.com/Azure/azure-sdk-for-go/issues/6547 + Email: utils.String(""), + AppID: utils.String(""), + Name: utils.String(""), + } + + principals := []kusto.DatabasePrincipal{kustoPrincipal} + request := kusto.DatabasePrincipalListRequest{ + Value: &principals, + } + + if _, err = client.AddPrincipals(ctx, resourceGroup, clusterName, databaseName, request); err != nil { + return fmt.Errorf("Error creating Kusto Database Principal (Resource Group %q, Cluster %q): %+v", resourceGroup, clusterName, err) + } + + resp, err := client.ListPrincipals(ctx, resourceGroup, clusterName, databaseName) + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error checking for presence of existing Kusto Database Principals (Resource Group %q, Cluster %q): %s", resourceGroup, clusterName, err) + } + } + + d.SetId(resourceId) + + return resourceArmKustoDatabasePrincipalRead(d, meta) +} + +func resourceArmKustoDatabasePrincipalRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Kusto.DatabasesClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + clusterName := id.Path["Clusters"] + databaseName := id.Path["Databases"] + role := id.Path["Role"] + fqn := id.Path["FQN"] + + databaseResponse, err := client.Get(ctx, resourceGroup, clusterName, databaseName) + if err != nil { + if utils.ResponseWasNotFound(databaseResponse.Response) { + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving Kusto Database %q (Resource Group %q, Cluster %q): %+v", databaseName, resourceGroup, clusterName, err) + } + + resp, err := client.ListPrincipals(ctx, resourceGroup, clusterName, databaseName) + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error checking for presence of existing Kusto Database Principals %q (Resource Group %q, Cluster %q): %s", id, resourceGroup, clusterName, err) + } + } + + principal := kusto.DatabasePrincipal{} + found := false + if principals := resp.Value; principals != nil { + for _, currPrincipal := range *principals { + // kusto database principals are unique when looked at with fqn and role + if string(currPrincipal.Role) == role && currPrincipal.Fqn != nil && *currPrincipal.Fqn == fqn { + principal = currPrincipal + found = true + break + } + } + } + + if !found { + log.Printf("[DEBUG] Kusto Database Principal %q was not found - removing from state", id) + d.SetId("") + return nil + } + + d.Set("resource_group_name", resourceGroup) + d.Set("cluster_name", clusterName) + d.Set("database_name", databaseName) + + d.Set("role", string(principal.Role)) + d.Set("type", string(principal.Type)) + if fqn := principal.Fqn; fqn != nil { + d.Set("fully_qualified_name", fqn) + } + if email := principal.Email; email != nil { + d.Set("email", email) + } + if appID := principal.AppID; appID != nil { + d.Set("app_id", appID) + } + if name := principal.Name; name != nil { + d.Set("name", principal.Name) + } + + splitFQN := strings.Split(fqn, "=") + if len(splitFQN) != 2 { + return fmt.Errorf("Expected `fqn` to be in the format aadtype=objectid:clientid but got: %q", fqn) + } + splitIDs := strings.Split(splitFQN[1], ";") + if len(splitIDs) != 2 { + return fmt.Errorf("Expected `fqn` to be in the format aadtype=objectid:clientid but got: %q", fqn) + } + d.Set("object_id", splitIDs[0]) + d.Set("client_id", splitIDs[1]) + + return nil +} + +func resourceArmKustoDatabasePrincipalDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Kusto.DatabasesClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + clusterName := id.Path["Clusters"] + databaseName := id.Path["Databases"] + role := id.Path["Role"] + fqn := id.Path["FQN"] + + kustoPrincipal := kusto.DatabasePrincipal{ + Role: kusto.DatabasePrincipalRole(role), + Fqn: utils.String(fqn), + Type: kusto.DatabasePrincipalType(d.Get("type").(string)), + // These three must be specified or the api returns `The request is invalid.` + // For more info: https://github.com/Azure/azure-sdk-for-go/issues/6547 + Name: utils.String(""), + Email: utils.String(""), + AppID: utils.String(""), + } + + principals := []kusto.DatabasePrincipal{kustoPrincipal} + request := kusto.DatabasePrincipalListRequest{ + Value: &principals, + } + + if _, err = client.RemovePrincipals(ctx, resGroup, clusterName, databaseName, request); err != nil { + return fmt.Errorf("Error deleting Kusto Database Principal %q (Resource Group %q, Cluster %q, Database %q): %+v", id, resGroup, clusterName, databaseName, err) + } + + return nil +} diff --git a/azurerm/internal/services/kusto/tests/resource_arm_kusto_database_principal_test.go b/azurerm/internal/services/kusto/tests/resource_arm_kusto_database_principal_test.go new file mode 100644 index 000000000000..99941605fd79 --- /dev/null +++ b/azurerm/internal/services/kusto/tests/resource_arm_kusto_database_principal_test.go @@ -0,0 +1,168 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMKustoDatabasePrincipal_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_kusto_database_principal", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMKustoDatabasePrincipalDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMKustoDatabasePrincipal_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMKustoDatabasePrincipalExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func testCheckAzureRMKustoDatabasePrincipalDestroy(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).Kusto.DatabasesClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_kusto_database_principal" { + continue + } + + resourceGroup := rs.Primary.Attributes["resource_group_name"] + clusterName := rs.Primary.Attributes["cluster_name"] + databaseName := rs.Primary.Attributes["database_name"] + role := rs.Primary.Attributes["role"] + fqn := rs.Primary.Attributes["fully_qualified_name"] + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + resp, err := client.ListPrincipals(ctx, resourceGroup, clusterName, databaseName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + return err + } + + found := false + if principals := resp.Value; principals != nil { + for _, currPrincipal := range *principals { + // kusto database principals are unique when looked at with fqn and role + if string(currPrincipal.Role) == role && currPrincipal.Fqn != nil && *currPrincipal.Fqn == fqn { + found = true + break + } + } + } + if found { + return fmt.Errorf("Kusto Database Principal %q still exists", fqn) + } + + return nil + } + + return nil +} + +func testCheckAzureRMKustoDatabasePrincipalExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + role := rs.Primary.Attributes["role"] + fqn := rs.Primary.Attributes["fully_qualified_name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for Kusto Database Principal: %s", fqn) + } + + clusterName, hasClusterName := rs.Primary.Attributes["cluster_name"] + if !hasClusterName { + return fmt.Errorf("Bad: no cluster name found in state for Kusto Database Principal: %s", fqn) + } + + databaseName, hasDatabaseName := rs.Primary.Attributes["database_name"] + if !hasDatabaseName { + return fmt.Errorf("Bad: no database name found in state for Kusto Database Principal: %s", fqn) + } + + client := acceptance.AzureProvider.Meta().(*clients.Client).Kusto.DatabasesClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + resp, err := client.ListPrincipals(ctx, resourceGroup, clusterName, databaseName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Kusto Database %q (resource group: %q, cluster: %q) does not exist", fqn, resourceGroup, clusterName) + } + + return fmt.Errorf("Bad: Get on DatabasesClient: %+v", err) + } + + found := false + if principals := resp.Value; principals != nil { + for _, currPrincipal := range *principals { + // kusto database principals are unique when looked at with fqn and role + if string(currPrincipal.Role) == role && currPrincipal.Fqn != nil && *currPrincipal.Fqn == fqn { + found = true + break + } + } + } + if !found { + return fmt.Errorf("Unable to find Kusto Database Principal %q", fqn) + } + + return nil + } +} + +func testAccAzureRMKustoDatabasePrincipal_basic(data acceptance.TestData) string { + return fmt.Sprintf(` +data "azurerm_client_config" "current" {} + + +resource "azurerm_resource_group" "rg" { + name = "acctestRG-kusto-%d" + location = "%s" +} + +resource "azurerm_kusto_cluster" "cluster" { + name = "acctestkc%s" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + + sku { + name = "Dev(No SLA)_Standard_D11_v2" + capacity = 1 + } +} + +resource "azurerm_kusto_database" "test" { + name = "acctestkd-%d" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + cluster_name = azurerm_kusto_cluster.cluster.name +} + +resource "azurerm_kusto_database_principal" "test" { + resource_group_name = azurerm_resource_group.rg.name + cluster_name = azurerm_kusto_cluster.cluster.name + database_name = azurerm_kusto_database.test.name + + role = "Viewer" + type = "App" + client_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.client_id +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomInteger) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index ad6b83f0c2e8..f3506a70cbc4 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -1061,6 +1061,9 @@
  • azurerm_kusto_database
  • +
  • + azurerm_kusto_database_principal +
  • azurerm_kusto_eventhub_data_connection
  • diff --git a/website/docs/r/kusto_database_principal.html.markdown b/website/docs/r/kusto_database_principal.html.markdown new file mode 100644 index 000000000000..4d012b5a839c --- /dev/null +++ b/website/docs/r/kusto_database_principal.html.markdown @@ -0,0 +1,97 @@ +--- +subcategory: "Data Explorer" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_kusto_database_principal" +sidebar_current: "docs-azurerm-resource-kusto-database-principal" +description: |- + Manages Kusto / Data Explorer Database Principal +--- + +# azurerm_kusto_database_principal + +Manages a Kusto (also known as Azure Data Explorer) Database Principal + +## Example Usage + +```hcl +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "rg" { + name = "my-kusto-rg" + location = "East US" +} + +resource "azurerm_kusto_cluster" "cluster" { + name = "kustocluster" + location = "${azurerm_resource_group.rg.location}" + resource_group_name = "${azurerm_resource_group.rg.name}" + + sku { + name = "Standard_D13_v2" + capacity = 2 + } +} + +resource "azurerm_kusto_database" "database" { + name = "my-kusto-database" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + cluster_name = azurerm_kusto_cluster.cluster.name + + hot_cache_period = "P7D" + soft_delete_period = "P31D" +} + +resource "azurerm_kusto_database_principal" "principal" { + resource_group_name = azurerm_resource_group.rg.name + cluster_name = azurerm_kusto_cluster.cluster.name + database_name = azurerm_kusto_database.test.name + + role = "Viewer" + type = "User" + client_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.client_id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `resource_group_name` - (Required) Specifies the Resource Group where the Kusto Database Principal should exist. Changing this forces a new resource to be created. + +* `cluster_name` - (Required) Specifies the name of the Kusto Cluster this database principal will be added to. Changing this forces a new resource to be created. + +* `database_name` - (Required) Specified the name of the Kusto Database this principal will be added to. Changing this forces a new resource to be created. + +* `role` - (Required) Specifies the permissions the Principal will have. Valid values include `Admin`, `Ingestor`, `Monitor`, `UnrestrictedViewers`, `User`, `Viewer`. Changing this forces a new resource to be created. + +* `type` - (Required) Specifies the type of object the principal is. Valid values include `App`, `Group`, `User`. Changing this forces a new resource to be created. + +* `object_id` - (Required) An Object ID of a User, Group, or App. Changing this forces a new resource to be created. + +* `client_id` - (Required) The Client ID that owns the specified `object_id`. Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Kusto Database Principal ID. + +* `app_id` - The app id, if not empty, of the principal. + +* `email` - The email, if not empty, of the principal. + +* `fully_qualified_name` - The fully qualified name of the principal. + +* `name` - The name of the Kusto Database Principal. + +--- + +## Import + +Kusto Database Principals can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_kusto_database_principal.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Kusto/Clusters/cluster1/Databases/database1/Role/role1/FQN/some-guid +```