From e963ed6e91de181a228a9c1cb60fc79a11d84253 Mon Sep 17 00:00:00 2001 From: Tom Harvey Date: Thu, 19 Jul 2018 13:43:04 +0200 Subject: [PATCH] Azure Active Directory Service Principals (#1564) * New Resource: `azurerm_azuread_service_principal` Tests pass: ``` $ acctests azurerm TestAccAzureRMActiveDirectoryServicePrincipal_ === RUN TestAccAzureRMActiveDirectoryServicePrincipal_importBasic --- PASS: TestAccAzureRMActiveDirectoryServicePrincipal_importBasic (24.04s) === RUN TestAccAzureRMActiveDirectoryServicePrincipal_basic --- PASS: TestAccAzureRMActiveDirectoryServicePrincipal_basic (17.61s) PASS ok github.com/terraform-providers/terraform-provider-azurerm/azurerm 41.701s ``` * New Data Source: `azurerm_azuread_service_principal` Tests pass: ``` $ acctests azurerm TestAccDataSourceAzureRMAzureADServicePrincipal_ === RUN TestAccDataSourceAzureRMAzureADServicePrincipal_byApplicationId --- PASS: TestAccDataSourceAzureRMAzureADServicePrincipal_byApplicationId (34.96s) === RUN TestAccDataSourceAzureRMAzureADServicePrincipal_byDisplayName --- PASS: TestAccDataSourceAzureRMAzureADServicePrincipal_byDisplayName (23.48s) === RUN TestAccDataSourceAzureRMAzureADServicePrincipal_byObjectId --- PASS: TestAccDataSourceAzureRMAzureADServicePrincipal_byObjectId (62.43s) PASS ok github.com/terraform-providers/terraform-provider-azurerm/azurerm 120.900s ``` * New Resource: `azurerm_azuread_service_principal_password` Tests pass: ``` $ acctests azurerm TestAccAzureRMActiveDirectoryServicePrincipalPassword_ === RUN TestAccAzureRMActiveDirectoryServicePrincipalPassword_basic --- PASS: TestAccAzureRMActiveDirectoryServicePrincipalPassword_basic (36.08s) === RUN TestAccAzureRMActiveDirectoryServicePrincipalPassword_customKeyId --- PASS: TestAccAzureRMActiveDirectoryServicePrincipalPassword_customKeyId (26.22s) PASS ok github.com/terraform-providers/terraform-provider-azurerm/azurerm 62.335s ``` * Fixing the broken objectID test ``` $ acctests azurerm TestAccDataSourceAzureRMAzureADApplication_byObjectIdComplete === RUN TestAccDataSourceAzureRMAzureADApplication_byObjectIdComplete --- PASS: TestAccDataSourceAzureRMAzureADApplication_byObjectIdComplete (33.13s) PASS ok github.com/terraform-providers/terraform-provider-azurerm/azurerm 33.179s ``` --- azurerm/data_source_azuread_application.go | 1 - .../data_source_azuread_application_test.go | 10 +- .../data_source_azuread_service_principal.go | 111 ++++++++ ...a_source_azuread_service_principal_test.go | 111 ++++++++ azurerm/helpers/validate/uuid.go | 21 ++ azurerm/helpers/validate/uuid_test.go | 37 +++ ...port_arm_azuread_service_principal_test.go | 31 +++ azurerm/provider.go | 3 + .../resource_arm_azuread_service_principal.go | 101 ++++++++ ..._arm_azuread_service_principal_password.go | 241 ++++++++++++++++++ ...azuread_service_principal_password_test.go | 157 ++++++++++++ ...urce_arm_azuread_service_principal_test.go | 91 +++++++ website/azurerm.erb | 18 +- .../docs/d/azuread_application.html.markdown | 2 + .../d/azuread_service_principal.html.markdown | 55 ++++ .../docs/r/azuread_application.html.markdown | 2 +- .../r/azuread_service_principal.html.markdown | 53 ++++ ...d_service_principal_password.html.markdown | 68 +++++ 18 files changed, 1104 insertions(+), 9 deletions(-) create mode 100644 azurerm/data_source_azuread_service_principal.go create mode 100644 azurerm/data_source_azuread_service_principal_test.go create mode 100644 azurerm/helpers/validate/uuid.go create mode 100644 azurerm/helpers/validate/uuid_test.go create mode 100644 azurerm/import_arm_azuread_service_principal_test.go create mode 100644 azurerm/resource_arm_azuread_service_principal.go create mode 100644 azurerm/resource_arm_azuread_service_principal_password.go create mode 100644 azurerm/resource_arm_azuread_service_principal_password_test.go create mode 100644 azurerm/resource_arm_azuread_service_principal_test.go create mode 100644 website/docs/d/azuread_service_principal.html.markdown create mode 100644 website/docs/r/azuread_service_principal.html.markdown create mode 100644 website/docs/r/azuread_service_principal_password.html.markdown diff --git a/azurerm/data_source_azuread_application.go b/azurerm/data_source_azuread_application.go index 8d7727d6cdf3..dc2e5011fe83 100644 --- a/azurerm/data_source_azuread_application.go +++ b/azurerm/data_source_azuread_application.go @@ -14,7 +14,6 @@ func dataSourceArmAzureADApplication() *schema.Resource { Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, - // TODO: customizeDiff for validation of either name or object_id. Schema: map[string]*schema.Schema{ "object_id": { diff --git a/azurerm/data_source_azuread_application_test.go b/azurerm/data_source_azuread_application_test.go index 4b28ab07d65e..451fe9c2a804 100644 --- a/azurerm/data_source_azuread_application_test.go +++ b/azurerm/data_source_azuread_application_test.go @@ -18,6 +18,9 @@ func TestAccDataSourceAzureRMAzureADApplication_byObjectId(t *testing.T) { Providers: testAccProviders, CheckDestroy: testCheckAzureRMActiveDirectoryApplicationDestroy, Steps: []resource.TestStep{ + { + Config: testAccAzureRMActiveDirectoryApplication_basic(id), + }, { Config: config, Check: resource.ComposeTestCheckFunc( @@ -37,15 +40,16 @@ func TestAccDataSourceAzureRMAzureADApplication_byObjectId(t *testing.T) { func TestAccDataSourceAzureRMAzureADApplication_byObjectIdComplete(t *testing.T) { dataSourceName := "data.azurerm_azuread_application.test" id := uuid.New().String() - config := testAccDataSourceAzureRMAzureADApplication_objectIdComplete(id) - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testCheckAzureRMActiveDirectoryApplicationDestroy, Steps: []resource.TestStep{ { - Config: config, + Config: testAccAzureRMActiveDirectoryApplication_complete(id), + }, + { + Config: testAccDataSourceAzureRMAzureADApplication_objectIdComplete(id), Check: resource.ComposeTestCheckFunc( testCheckAzureRMActiveDirectoryApplicationExists(dataSourceName), resource.TestCheckResourceAttr(dataSourceName, "name", fmt.Sprintf("acctest%s", id)), diff --git a/azurerm/data_source_azuread_service_principal.go b/azurerm/data_source_azuread_service_principal.go new file mode 100644 index 000000000000..b26822a4b2fc --- /dev/null +++ b/azurerm/data_source_azuread_service_principal.go @@ -0,0 +1,111 @@ +package azurerm + +import ( + "fmt" + + "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" + "github.com/hashicorp/terraform/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func dataSourceArmActiveDirectoryServicePrincipal() *schema.Resource { + return &schema.Resource{ + Read: dataSourceArmActiveDirectoryServicePrincipalRead, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "object_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"display_name", "application_id"}, + }, + + "display_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"object_id", "application_id"}, + }, + + "application_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"object_id", "display_name"}, + }, + }, + } +} + +func dataSourceArmActiveDirectoryServicePrincipalRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).servicePrincipalsClient + ctx := meta.(*ArmClient).StopContext + + var servicePrincipal *graphrbac.ServicePrincipal + + if v, ok := d.GetOk("object_id"); ok { + objectId := v.(string) + app, err := client.Get(ctx, objectId) + if err != nil { + if utils.ResponseWasNotFound(app.Response) { + return fmt.Errorf("Service Principal with Object ID %q was not found!", objectId) + } + + return fmt.Errorf("Error retrieving Service Principal ID %q: %+v", objectId, err) + } + + servicePrincipal = &app + } else { + apps, err := client.ListComplete(ctx, "") + if err != nil { + return fmt.Errorf("Error listing Service Principals: %+v", err) + } + + if v, ok := d.GetOk("display_name"); ok { + displayName := v.(string) + + for _, app := range *apps.Response().Value { + if app.DisplayName == nil { + continue + } + + if *app.DisplayName == displayName { + servicePrincipal = &app + break + } + } + + if servicePrincipal == nil { + return fmt.Errorf("A Service Principal with the Display Name %q was not found", displayName) + } + } else { + applicationId := d.Get("application_id").(string) + + for _, app := range *apps.Response().Value { + if app.AppID == nil { + continue + } + + if *app.AppID == applicationId { + servicePrincipal = &app + break + } + } + + if servicePrincipal == nil { + return fmt.Errorf("A Service Principal for Application ID %q was not found", applicationId) + } + } + } + + d.SetId(*servicePrincipal.ObjectID) + + d.Set("application_id", servicePrincipal.AppID) + d.Set("display_name", servicePrincipal.DisplayName) + d.Set("object_id", servicePrincipal.ObjectID) + + return nil +} diff --git a/azurerm/data_source_azuread_service_principal_test.go b/azurerm/data_source_azuread_service_principal_test.go new file mode 100644 index 000000000000..a1ef2b857b98 --- /dev/null +++ b/azurerm/data_source_azuread_service_principal_test.go @@ -0,0 +1,111 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDataSourceAzureRMAzureADServicePrincipal_byApplicationId(t *testing.T) { + dataSourceName := "data.azurerm_azuread_service_principal.test" + id := uuid.New().String() + config := testAccDataSourceAzureRMAzureADServicePrincipal_byApplicationId(id) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMActiveDirectoryServicePrincipalDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMActiveDirectoryServicePrincipalExists(dataSourceName), + resource.TestCheckResourceAttrSet(dataSourceName, "application_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "object_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "display_name"), + ), + }, + }, + }) +} + +func TestAccDataSourceAzureRMAzureADServicePrincipal_byDisplayName(t *testing.T) { + dataSourceName := "data.azurerm_azuread_service_principal.test" + id := uuid.New().String() + config := testAccDataSourceAzureRMAzureADServicePrincipal_byDisplayName(id) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMActiveDirectoryServicePrincipalDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMActiveDirectoryServicePrincipalExists(dataSourceName), + resource.TestCheckResourceAttrSet(dataSourceName, "application_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "object_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "display_name"), + ), + }, + }, + }) +} + +func TestAccDataSourceAzureRMAzureADServicePrincipal_byObjectId(t *testing.T) { + dataSourceName := "data.azurerm_azuread_service_principal.test" + id := uuid.New().String() + config := testAccDataSourceAzureRMAzureADServicePrincipal_byObjectId(id) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMActiveDirectoryServicePrincipalDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMActiveDirectoryServicePrincipalExists(dataSourceName), + resource.TestCheckResourceAttrSet(dataSourceName, "application_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "object_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "display_name"), + ), + }, + }, + }) +} + +func testAccDataSourceAzureRMAzureADServicePrincipal_byApplicationId(id string) string { + template := testAccAzureRMActiveDirectoryServicePrincipal_basic(id) + return fmt.Sprintf(` +%s + +data "azurerm_azuread_service_principal" "test" { + application_id = "${azurerm_azuread_service_principal.test.application_id}" +} +`, template) +} + +func testAccDataSourceAzureRMAzureADServicePrincipal_byDisplayName(id string) string { + template := testAccAzureRMActiveDirectoryServicePrincipal_basic(id) + return fmt.Sprintf(` +%s + +data "azurerm_azuread_service_principal" "test" { + display_name = "${azurerm_azuread_service_principal.test.display_name}" +} +`, template) +} + +func testAccDataSourceAzureRMAzureADServicePrincipal_byObjectId(id string) string { + template := testAccAzureRMActiveDirectoryServicePrincipal_basic(id) + return fmt.Sprintf(` +%s + +data "azurerm_azuread_service_principal" "test" { + object_id = "${azurerm_azuread_service_principal.test.id}" +} +`, template) +} diff --git a/azurerm/helpers/validate/uuid.go b/azurerm/helpers/validate/uuid.go new file mode 100644 index 000000000000..e4820c569d1a --- /dev/null +++ b/azurerm/helpers/validate/uuid.go @@ -0,0 +1,21 @@ +package validate + +import ( + "fmt" + + "github.com/hashicorp/go-uuid" +) + +func UUID(i interface{}, k string) (_ []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := uuid.ParseUUID(v); err != nil { + errors = append(errors, fmt.Errorf("%q isn't a valid UUID (%q): %+v", k, v, err)) + } + + return +} diff --git a/azurerm/helpers/validate/uuid_test.go b/azurerm/helpers/validate/uuid_test.go new file mode 100644 index 000000000000..15f3a5ea9cd0 --- /dev/null +++ b/azurerm/helpers/validate/uuid_test.go @@ -0,0 +1,37 @@ +package validate + +import "testing" + +func TestUUID(t *testing.T) { + cases := []struct { + Input string + Errors int + }{ + { + Input: "", + Errors: 1, + }, + { + Input: "hello-world", + Errors: 1, + }, + { + Input: "00000000-0000-111-0000-000000000000", + Errors: 1, + }, + { + Input: "00000000-0000-0000-0000-000000000000", + Errors: 0, + }, + } + + for _, tc := range cases { + t.Run(tc.Input, func(t *testing.T) { + _, errors := UUID(tc.Input, "test") + + if len(errors) != tc.Errors { + t.Fatalf("Expected UUID to have %d not %d errors for %q", tc.Errors, len(errors), tc.Input) + } + }) + } +} diff --git a/azurerm/import_arm_azuread_service_principal_test.go b/azurerm/import_arm_azuread_service_principal_test.go new file mode 100644 index 000000000000..015fd997cb35 --- /dev/null +++ b/azurerm/import_arm_azuread_service_principal_test.go @@ -0,0 +1,31 @@ +package azurerm + +import ( + "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAzureRMActiveDirectoryServicePrincipal_importBasic(t *testing.T) { + resourceName := "azurerm_azuread_service_principal.test" + + id := uuid.New().String() + config := testAccAzureRMActiveDirectoryServicePrincipal_basic(id) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMActiveDirectoryServicePrincipalDestroy, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/azurerm/provider.go b/azurerm/provider.go index cc9f0d98927d..38f66315cbcc 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -79,6 +79,7 @@ func Provider() terraform.ResourceProvider { DataSourcesMap: map[string]*schema.Resource{ "azurerm_azuread_application": dataSourceArmAzureADApplication(), + "azurerm_azuread_service_principal": dataSourceArmActiveDirectoryServicePrincipal(), "azurerm_application_security_group": dataSourceArmApplicationSecurityGroup(), "azurerm_app_service": dataSourceArmAppService(), "azurerm_app_service_plan": dataSourceAppServicePlan(), @@ -121,6 +122,8 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "azurerm_azuread_application": resourceArmActiveDirectoryApplication(), + "azurerm_azuread_service_principal": resourceArmActiveDirectoryServicePrincipal(), + "azurerm_azuread_service_principal_password": resourceArmActiveDirectoryServicePrincipalPassword(), "azurerm_application_gateway": resourceArmApplicationGateway(), "azurerm_application_insights": resourceArmApplicationInsights(), "azurerm_application_security_group": resourceArmApplicationSecurityGroup(), diff --git a/azurerm/resource_arm_azuread_service_principal.go b/azurerm/resource_arm_azuread_service_principal.go new file mode 100644 index 000000000000..71beabf650b3 --- /dev/null +++ b/azurerm/resource_arm_azuread_service_principal.go @@ -0,0 +1,101 @@ +package azurerm + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" + "github.com/hashicorp/terraform/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/response" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +var servicePrincipalResourceName = "azurerm_service_principal" + +func resourceArmActiveDirectoryServicePrincipal() *schema.Resource { + return &schema.Resource{ + Create: resourceArmActiveDirectoryServicePrincipalCreate, + Read: resourceArmActiveDirectoryServicePrincipalRead, + Delete: resourceArmActiveDirectoryServicePrincipalDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "application_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "display_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceArmActiveDirectoryServicePrincipalCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).servicePrincipalsClient + ctx := meta.(*ArmClient).StopContext + + applicationId := d.Get("application_id").(string) + + properties := graphrbac.ServicePrincipalCreateParameters{ + AppID: utils.String(applicationId), + // there's no way of retrieving this, and there's no way of changing it + // given there's no way to change it - we'll just default this to true + AccountEnabled: utils.Bool(true), + } + + app, err := client.Create(ctx, properties) + if err != nil { + return fmt.Errorf("Error creating Service Principal %q: %+v", applicationId, err) + } + + objectId := *app.ObjectID + resp, err := client.Get(ctx, objectId) + if err != nil { + return fmt.Errorf("Error retrieving Service Principal ID %q: %+v", objectId, err) + } + + d.SetId(*resp.ObjectID) + + return resourceArmActiveDirectoryServicePrincipalRead(d, meta) +} + +func resourceArmActiveDirectoryServicePrincipalRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).servicePrincipalsClient + ctx := meta.(*ArmClient).StopContext + + objectId := d.Id() + app, err := client.Get(ctx, objectId) + if err != nil { + if utils.ResponseWasNotFound(app.Response) { + log.Printf("[DEBUG] Service Principal with Object ID %q was not found - removing from state!", objectId) + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving Service Principal ID %q: %+v", objectId, err) + } + + d.Set("application_id", app.AppID) + d.Set("display_name", app.DisplayName) + + return nil +} + +func resourceArmActiveDirectoryServicePrincipalDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).servicePrincipalsClient + ctx := meta.(*ArmClient).StopContext + + applicationId := d.Id() + app, err := client.Delete(ctx, applicationId) + if err != nil { + if !response.WasNotFound(app.Response) { + return fmt.Errorf("Error deleting Service Principal ID %q: %+v", applicationId, err) + } + } + + return nil +} diff --git a/azurerm/resource_arm_azuread_service_principal_password.go b/azurerm/resource_arm_azuread_service_principal_password.go new file mode 100644 index 000000000000..f1c3bdf3f57c --- /dev/null +++ b/azurerm/resource_arm_azuread_service_principal_password.go @@ -0,0 +1,241 @@ +package azurerm + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" + "github.com/Azure/go-autorest/autorest/date" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmActiveDirectoryServicePrincipalPassword() *schema.Resource { + return &schema.Resource{ + Create: resourceArmActiveDirectoryServicePrincipalPasswordCreate, + Read: resourceArmActiveDirectoryServicePrincipalPasswordRead, + Delete: resourceArmActiveDirectoryServicePrincipalPasswordDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "service_principal_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.UUID, + }, + + "key_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validate.UUID, + }, + + "value": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Sensitive: true, + }, + + "start_date": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validate.RFC3339Time, + }, + + "end_date": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.RFC3339Time, + }, + }, + } +} + +func resourceArmActiveDirectoryServicePrincipalPasswordCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).servicePrincipalsClient + ctx := meta.(*ArmClient).StopContext + + objectId := d.Get("service_principal_id").(string) + value := d.Get("value").(string) + // errors will be handled by the validation + endDate, _ := time.Parse(time.RFC3339, d.Get("end_date").(string)) + + var keyId string + if v, ok := d.GetOk("key_id"); ok { + keyId = v.(string) + } else { + kid, err := uuid.GenerateUUID() + if err != nil { + return err + } + + keyId = kid + } + + credential := graphrbac.PasswordCredential{ + KeyID: utils.String(keyId), + Value: utils.String(value), + EndDate: &date.Time{Time: endDate}, + } + + if v, ok := d.GetOk("start_date"); ok { + // errors will be handled by the validation + startDate, _ := time.Parse(time.RFC3339, v.(string)) + credential.StartDate = &date.Time{Time: startDate} + } + + azureRMLockByName(objectId, servicePrincipalResourceName) + defer azureRMUnlockByName(objectId, servicePrincipalResourceName) + + existingCredentials, err := client.ListPasswordCredentials(ctx, objectId) + if err != nil { + return fmt.Errorf("Error Listing Password Credentials for Service Principal %q: %+v", objectId, err) + } + + updatedCredentials := make([]graphrbac.PasswordCredential, 0) + if existingCredentials.Value != nil { + updatedCredentials = *existingCredentials.Value + } + + updatedCredentials = append(updatedCredentials, credential) + + parameters := graphrbac.PasswordCredentialsUpdateParameters{ + Value: &updatedCredentials, + } + _, err = client.UpdatePasswordCredentials(ctx, objectId, parameters) + if err != nil { + return fmt.Errorf("Error creating Password Credential %q for Service Principal %q: %+v", keyId, objectId, err) + } + + d.SetId(fmt.Sprintf("%s/%s", objectId, keyId)) + + return resourceArmActiveDirectoryServicePrincipalPasswordRead(d, meta) +} + +func resourceArmActiveDirectoryServicePrincipalPasswordRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).servicePrincipalsClient + ctx := meta.(*ArmClient).StopContext + + id := strings.Split(d.Id(), "/") + if len(id) != 2 { + return fmt.Errorf("ID should be in the format {objectId}/{keyId} - but got %q", d.Id()) + } + + objectId := id[0] + keyId := id[1] + + // ensure the parent Service Principal exists + servicePrincipal, err := client.Get(ctx, objectId) + if err != nil { + // the parent Service Principal has been removed - skip it + if utils.ResponseWasNotFound(servicePrincipal.Response) { + log.Printf("[DEBUG] Service Principal with Object ID %q was not found - removing from state!", objectId) + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving Service Principal ID %q: %+v", objectId, err) + } + + credentials, err := client.ListPasswordCredentials(ctx, objectId) + if err != nil { + return fmt.Errorf("Error Listing Password Credentials for Service Principal with Object ID %q: %+v", objectId, err) + } + + var credential *graphrbac.PasswordCredential + for _, c := range *credentials.Value { + if c.KeyID == nil { + continue + } + + if *c.KeyID == keyId { + credential = &c + break + } + } + + if credential == nil { + log.Printf("[DEBUG] Service Principal Password %q (Object ID %q) was not found - removing from state!", keyId, objectId) + d.SetId("") + return nil + } + + // value is available in the SDK but isn't returned from the API + d.Set("key_id", credential.KeyID) + d.Set("service_principal_id", objectId) + + if endDate := credential.EndDate; endDate != nil { + d.Set("end_date", endDate.Format(time.RFC3339)) + } + + if startDate := credential.StartDate; startDate != nil { + d.Set("start_date", startDate.Format(time.RFC3339)) + } + + return nil +} + +func resourceArmActiveDirectoryServicePrincipalPasswordDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).servicePrincipalsClient + ctx := meta.(*ArmClient).StopContext + + id := strings.Split(d.Id(), "/") + if len(id) != 2 { + return fmt.Errorf("ID should be in the format {objectId}/{keyId} - but got %q", d.Id()) + } + + objectId := id[0] + keyId := id[1] + + azureRMLockByName(objectId, servicePrincipalResourceName) + defer azureRMUnlockByName(objectId, servicePrincipalResourceName) + + // ensure the parent Service Principal exists + servicePrincipal, err := client.Get(ctx, objectId) + if err != nil { + // the parent Service Principal was removed - skip it + if utils.ResponseWasNotFound(servicePrincipal.Response) { + return nil + } + + return fmt.Errorf("Error retrieving Service Principal ID %q: %+v", objectId, err) + } + + existing, err := client.ListPasswordCredentials(ctx, objectId) + if err != nil { + return fmt.Errorf("Error Listing Password Credentials for Service Principal with Object ID %q: %+v", objectId, err) + } + + updatedCredentials := make([]graphrbac.PasswordCredential, 0) + for _, credential := range *existing.Value { + if credential.KeyID == nil { + continue + } + + if *credential.KeyID != keyId { + updatedCredentials = append(updatedCredentials, credential) + } + } + + parameters := graphrbac.PasswordCredentialsUpdateParameters{ + Value: &updatedCredentials, + } + _, err = client.UpdatePasswordCredentials(ctx, objectId, parameters) + if err != nil { + return fmt.Errorf("Error removing Password %q from Service Principal %q: %+v", keyId, objectId, err) + } + + return nil +} diff --git a/azurerm/resource_arm_azuread_service_principal_password_test.go b/azurerm/resource_arm_azuread_service_principal_password_test.go new file mode 100644 index 000000000000..386b0c9d5679 --- /dev/null +++ b/azurerm/resource_arm_azuread_service_principal_password_test.go @@ -0,0 +1,157 @@ +package azurerm + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMActiveDirectoryServicePrincipalPassword_basic(t *testing.T) { + resourceName := "azurerm_azuread_service_principal_password.test" + applicationId, err := uuid.GenerateUUID() + if err != nil { + t.Fatal(err) + } + value, err := uuid.GenerateUUID() + if err != nil { + t.Fatal(err) + } + + config := testAccAzureRMActiveDirectoryServicePrincipalPassword_basic(applicationId, value) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMActiveDirectoryServicePrincipalDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + // can't assert on Value since it's not returned + testCheckAzureRMActiveDirectoryServicePrincipalPasswordExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "start_date"), + resource.TestCheckResourceAttrSet(resourceName, "key_id"), + resource.TestCheckResourceAttr(resourceName, "end_date", "2020-01-01T01:02:03Z"), + ), + }, + }, + }) +} + +func TestAccAzureRMActiveDirectoryServicePrincipalPassword_customKeyId(t *testing.T) { + resourceName := "azurerm_azuread_service_principal_password.test" + applicationId, err := uuid.GenerateUUID() + if err != nil { + t.Fatal(err) + } + keyId, err := uuid.GenerateUUID() + if err != nil { + t.Fatal(err) + } + value, err := uuid.GenerateUUID() + if err != nil { + t.Fatal(err) + } + config := testAccAzureRMActiveDirectoryServicePrincipalPassword_customKeyId(applicationId, keyId, value) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMActiveDirectoryServicePrincipalDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + // can't assert on Value since it's not returned + testCheckAzureRMActiveDirectoryServicePrincipalPasswordExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "start_date"), + resource.TestCheckResourceAttr(resourceName, "key_id", keyId), + resource.TestCheckResourceAttr(resourceName, "end_date", "2020-01-01T01:02:03Z"), + ), + }, + }, + }) +} + +func testCheckAzureRMActiveDirectoryServicePrincipalPasswordExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %q", name) + } + + client := testAccProvider.Meta().(*ArmClient).servicePrincipalsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + id := strings.Split(rs.Primary.ID, "/") + objectId := id[0] + keyId := id[1] + resp, err := client.Get(ctx, objectId) + + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Azure AD Service Principal %q does not exist", objectId) + } + return fmt.Errorf("Bad: Get on Azure AD servicePrincipalsClient: %+v", err) + } + + credentials, err := client.ListPasswordCredentials(ctx, objectId) + if err != nil { + return fmt.Errorf("Error Listing Password Credentials for Service Principal %q: %+v", objectId, err) + } + + for _, credential := range *credentials.Value { + if credential.KeyID == nil { + continue + } + + if *credential.KeyID == keyId { + return nil + } + } + + return fmt.Errorf("Password Credential %q was not found in Service Principal %q", keyId, objectId) + } +} + +func testAccAzureRMActiveDirectoryServicePrincipalPassword_basic(applicationId, value string) string { + return fmt.Sprintf(` +resource "azurerm_azuread_application" "test" { + name = "acctestspa%s" +} + +resource "azurerm_azuread_service_principal" "test" { + application_id = "${azurerm_azuread_application.test.application_id}" +} + +resource "azurerm_azuread_service_principal_password" "test" { + service_principal_id = "${azurerm_azuread_service_principal.test.id}" + value = "%s" + end_date = "2020-01-01T01:02:03Z" +} +`, applicationId, value) +} + +func testAccAzureRMActiveDirectoryServicePrincipalPassword_customKeyId(applicationId, keyId, value string) string { + return fmt.Sprintf(` +resource "azurerm_azuread_application" "test" { + name = "acctestspa%s" +} + +resource "azurerm_azuread_service_principal" "test" { + application_id = "${azurerm_azuread_application.test.application_id}" +} + +resource "azurerm_azuread_service_principal_password" "test" { + service_principal_id = "${azurerm_azuread_service_principal.test.id}" + key_id = "%s" + value = "%s" + end_date = "2020-01-01T01:02:03Z" +} +`, applicationId, keyId, value) +} diff --git a/azurerm/resource_arm_azuread_service_principal_test.go b/azurerm/resource_arm_azuread_service_principal_test.go new file mode 100644 index 000000000000..cea7ebf6624b --- /dev/null +++ b/azurerm/resource_arm_azuread_service_principal_test.go @@ -0,0 +1,91 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMActiveDirectoryServicePrincipal_basic(t *testing.T) { + resourceName := "azurerm_azuread_service_principal.test" + id := uuid.New().String() + config := testAccAzureRMActiveDirectoryServicePrincipal_basic(id) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMActiveDirectoryServicePrincipalDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMActiveDirectoryServicePrincipalExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "display_name"), + resource.TestCheckResourceAttrSet(resourceName, "application_id"), + ), + }, + }, + }) +} + +func testCheckAzureRMActiveDirectoryServicePrincipalExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %q", name) + } + + client := testAccProvider.Meta().(*ArmClient).servicePrincipalsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + resp, err := client.Get(ctx, rs.Primary.ID) + + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Azure AD Service Principal %q does not exist", rs.Primary.ID) + } + return fmt.Errorf("Bad: Get on Azure AD servicePrincipalsClient: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMActiveDirectoryServicePrincipalDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_azuread_service_principal" { + continue + } + + client := testAccProvider.Meta().(*ArmClient).servicePrincipalsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + resp, err := client.Get(ctx, rs.Primary.ID) + + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + + return err + } + + return fmt.Errorf("Azure AD Service Principal still exists:\n%#v", resp) + } + + return nil +} + +func testAccAzureRMActiveDirectoryServicePrincipal_basic(id string) string { + return fmt.Sprintf(` +resource "azurerm_azuread_application" "test" { + name = "acctestspa%s" +} + +resource "azurerm_azuread_service_principal" "test" { + application_id = "${azurerm_azuread_application.test.application_id}" +} +`, id) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 313b5d32abc2..3c6a93370c37 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -27,10 +27,6 @@ > Data Sources diff --git a/website/docs/d/azuread_application.html.markdown b/website/docs/d/azuread_application.html.markdown index 817466243ba9..dbf9f7b32da9 100644 --- a/website/docs/d/azuread_application.html.markdown +++ b/website/docs/d/azuread_application.html.markdown @@ -10,6 +10,8 @@ description: |- Gets information about an Application within Azure Active Directory. +-> **NOTE:** If you're authenticating using a Service Principal then it must have permissions to both `Read and write all applications` and `Sign in and read user profile` within the `Windows Azure Active Directory` API. + ## Example Usage ```hcl diff --git a/website/docs/d/azuread_service_principal.html.markdown b/website/docs/d/azuread_service_principal.html.markdown new file mode 100644 index 000000000000..3f1399b92d78 --- /dev/null +++ b/website/docs/d/azuread_service_principal.html.markdown @@ -0,0 +1,55 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_azuread_service_principal" +sidebar_current: "docs-azurerm-datasource-azuread-service-principal" +description: |- + Gets information about a Service Principal associated with an Application within Azure Active Directory. + +--- + +# Data Source: azurerm_azuread_service_principal + +Gets information about a Service Principal associated with an Application within Azure Active Directory. + +-> **NOTE:** If you're authenticating using a Service Principal then it must have permissions to both `Read and write all applications` and `Sign in and read user profile` within the `Windows Azure Active Directory` API. + +## Example Usage (by Application Display Name) + +```hcl +data "azurerm_azuread_service_principal" "test" { + display_name = "my-awesome-application" +} + +## Example Usage (by Application ID) + +```hcl +data "azurerm_azuread_service_principal" "test" { + application_id = "00000000-0000-0000-0000-000000000000" +} +``` + +## Example Usage (by Object ID) + +```hcl +data "azurerm_azuread_service_principal" "test" { + object_id = "00000000-0000-0000-0000-000000000000" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `application_id` - (Optional) The ID of the Azure AD Application for which to create a Service Principal. + +* `object_id` - (Optional) The ID of the Azure AD Service Principal. + +* `display_name` - (Optional) The Display Name of the Azure AD Application associated with this Service Principal. + +-> **NOTE:** At least one of `application_id`, `display_name` or `object_id` must be specified. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Object ID for the Service Principal. diff --git a/website/docs/r/azuread_application.html.markdown b/website/docs/r/azuread_application.html.markdown index 8125688540a1..a6a5bfe4e2c7 100644 --- a/website/docs/r/azuread_application.html.markdown +++ b/website/docs/r/azuread_application.html.markdown @@ -16,7 +16,7 @@ Manages an Application within Azure Active Directory. ## Example Usage ```hcl -resource "azurerm_azuread_application" "example" { +resource "azurerm_azuread_application" "test" { name = "example" homepage = "http://homepage" identifier_uris = ["http://uri"] diff --git a/website/docs/r/azuread_service_principal.html.markdown b/website/docs/r/azuread_service_principal.html.markdown new file mode 100644 index 000000000000..56385d7bcea4 --- /dev/null +++ b/website/docs/r/azuread_service_principal.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_azuread_service_principal" +sidebar_current: "docs-azurerm-resource-azuread-service-principal-x" +description: |- + Manages a Service Principal associated with an Application within Azure Active Directory. + +--- + +# azurerm_azuread_service_principal + +Manages a Service Principal associated with an Application within Azure Active Directory. + +-> **NOTE:** If you're authenticating using a Service Principal then it must have permissions to both `Read and write all applications` and `Sign in and read user profile` within the `Windows Azure Active Directory` API. + +## Example Usage + +```hcl +resource "azurerm_azuread_application" "test" { + name = "example" + homepage = "http://homepage" + identifier_uris = ["http://uri"] + reply_urls = ["http://replyurl"] + available_to_other_tenants = false + oauth2_allow_implicit_flow = true +} + +resource "azurerm_azuread_service_principal" "test" { + application_id = "${azurerm_azuread_application.test.application_id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `application_id` - (Required) The ID of the Azure AD Application for which to create a Service Principal. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Object ID for the Service Principal. + +* `display_name` - The Display Name of the Azure Active Directory Application associated with this Service Principal. + +## Import + +Azure Active Directory Service Principals can be imported using the `object id`, e.g. + +```shell +terraform import azurerm_azuread_service_principal.test 00000000-0000-0000-0000-000000000000 +``` diff --git a/website/docs/r/azuread_service_principal_password.html.markdown b/website/docs/r/azuread_service_principal_password.html.markdown new file mode 100644 index 000000000000..a9168bc717b6 --- /dev/null +++ b/website/docs/r/azuread_service_principal_password.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_azuread_service_principal_password" +sidebar_current: "docs-azurerm-resource-azuread-service-principal-password" +description: |- + Manages a Password associated with a Service Principal within Azure Active Directory. + +--- + +# azurerm_azuread_service_principal_password + +Manages a Password associated with a Service Principal within Azure Active Directory. + +-> **NOTE:** If you're authenticating using a Service Principal then it must have permissions to both `Read and write all applications` and `Sign in and read user profile` within the `Windows Azure Active Directory` API. + +## Example Usage + +```hcl +resource "azurerm_azuread_application" "test" { + name = "example" + homepage = "http://homepage" + identifier_uris = ["http://uri"] + reply_urls = ["http://replyurl"] + available_to_other_tenants = false + oauth2_allow_implicit_flow = true +} + +resource "azurerm_azuread_service_principal" "test" { + application_id = "${azurerm_azuread_application.test.application_id}" +} + +resource "azurerm_azuread_service_principal_password" "test" { + service_principal_id = "${azurerm_azuread_service_principal.test.id}" + value = "VT=uSgbTanZhyz@%nL9Hpd+Tfay_MRV#" + end_date = "2020-01-01T01:02:03Z" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `service_principal_id` - (Required) The ID of the Service Principal for which this password should be created. Changing this field forces a new resource to be created. + +* `value` - (Required) The Password for this Service Principal. + +* `end_date` - (Required) The End Date which the Password is valid until, formatted as a RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). Changing this field forces a new resource to be created. + +* `key_id` - (Optional) A GUID used to uniquely identify this Key. If not specified a GUID will be created. Changing this field forces a new resource to be created. + +* `start_date` - (Optional) The Start Date which the Password is valid from, formatted as a RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). If this isn't specified, the current date is used. Changing this field forces a new resource to be created. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Key ID for the Service Principal Password. + +## Import + +Service Principal Passwords can be imported using the `object id`, e.g. + +```shell +terraform import azurerm_azuread_service_principal_password.test 00000000-0000-0000-0000-000000000000/11111111-1111-1111-1111-111111111111 +``` + +-> **NOTE:** This ID format is unique to Terraform and is composed of the Service Principal's Object ID and the Service Principal Password's Key ID in the format `{ServicePrincipalObjectId}/{ServicePrincipalPasswordKeyId}`.