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_service_principal.go b/azurerm/data_source_azuread_service_principal.go index eb9d12b1ac64..b26822a4b2fc 100644 --- a/azurerm/data_source_azuread_service_principal.go +++ b/azurerm/data_source_azuread_service_principal.go @@ -14,7 +14,6 @@ func dataSourceArmActiveDirectoryServicePrincipal() *schema.Resource { Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, - // TODO: customiseDiff to ensure either `object_id` or `display_name` or `application_id` is set Schema: map[string]*schema.Schema{ "object_id": { 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/provider.go b/azurerm/provider.go index 6694ab8d0775..09bd86a32f03 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -120,6 +120,7 @@ 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 index 8c4a3d763829..71beabf650b3 100644 --- a/azurerm/resource_arm_azuread_service_principal.go +++ b/azurerm/resource_arm_azuread_service_principal.go @@ -2,7 +2,6 @@ package azurerm import ( "fmt" - "log" "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" @@ -11,6 +10,8 @@ import ( "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" ) +var servicePrincipalResourceName = "azurerm_service_principal" + func resourceArmActiveDirectoryServicePrincipal() *schema.Resource { return &schema.Resource{ Create: resourceArmActiveDirectoryServicePrincipalCreate, @@ -72,6 +73,7 @@ func resourceArmActiveDirectoryServicePrincipalRead(d *schema.ResourceData, meta 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) 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/website/azurerm.erb b/website/azurerm.erb index 494ee737455c..53c2005f6f17 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -235,9 +235,12 @@ > azurerm_azuread_application - > + > azurerm_azuread_service_principal + > + azurerm_azuread_service_principal_password + diff --git a/website/docs/r/azuread_service_principal.html.markdown b/website/docs/r/azuread_service_principal.html.markdown index 8deea3c359b7..56385d7bcea4 100644 --- a/website/docs/r/azuread_service_principal.html.markdown +++ b/website/docs/r/azuread_service_principal.html.markdown @@ -1,7 +1,7 @@ --- layout: "azurerm" page_title: "Azure Resource Manager: azurerm_azuread_service_principal" -sidebar_current: "docs-azurerm-resource-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. 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}`.