diff --git a/azuread/helpers/graph/credentials.go b/azuread/helpers/graph/credentials.go index a86b2f93be..7900db65f9 100644 --- a/azuread/helpers/graph/credentials.go +++ b/azuread/helpers/graph/credentials.go @@ -90,15 +90,8 @@ func CertificateResourceSchema(object_type string) map[string]*schema.Schema { } // valid types are `application` and `service_principal` -func PasswordResourceSchema(object_type string) map[string]*schema.Schema { - return map[string]*schema.Schema{ - object_type + "_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validate.UUID, - }, - +func PasswordResourceSchema(objectType string) map[string]*schema.Schema { + theSchema := map[string]*schema.Schema{ "key_id": { Type: schema.TypeString, Optional: true, @@ -147,40 +140,87 @@ func PasswordResourceSchema(object_type string) map[string]*schema.Schema { ValidateFunc: validate.NoEmptyStrings, }, } + + switch objectType { + case "application": + theSchema["application_id"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validate.UUID, + Deprecated: "Deprecated in favour of `application_object_id` to prevent confusion", + ExactlyOneOf: []string{"application_object_id"}, + } + theSchema["application_object_id"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validate.UUID, + ExactlyOneOf: []string{"application_id"}, + } + case "service_principal": + theSchema["service_principal_id"] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.UUID, + } + } + + return theSchema } type CredentialId struct { ObjectId string + KeyType string KeyId string } func (id CredentialId) String() string { - return id.ObjectId + "/" + id.KeyId + return id.ObjectId + "/" + id.KeyType + "/" + id.KeyId } func ParseCredentialId(id string) (CredentialId, error) { parts := strings.Split(id, "/") - if len(parts) != 2 { - return CredentialId{}, fmt.Errorf("Password Credential ID should be in the format {objectId}/{keyId} - but got %q", id) + if len(parts) != 3 { + return CredentialId{}, fmt.Errorf("Credential ID should be in the format {objectId}/{keyType}/{keyId} - but got %q", id) } if _, err := uuid.ParseUUID(parts[0]); err != nil { - return CredentialId{}, fmt.Errorf("Object ID isn't a valid UUID (%q): %+v", id[0], err) + return CredentialId{}, fmt.Errorf("Object ID isn't a valid UUID (%q): %+v", parts[0], err) + } + + if parts[1] != "certificate" && parts[1] != "password" { + return CredentialId{}, fmt.Errorf("Key type should be one of: certificate, password. Got: %q", parts[1]) } - if _, err := uuid.ParseUUID(parts[1]); err != nil { - return CredentialId{}, fmt.Errorf("Credential ID isn't a valid UUID (%q): %+v", id[1], err) + if _, err := uuid.ParseUUID(parts[2]); err != nil { + return CredentialId{}, fmt.Errorf("Key ID isn't a valid UUID (%q): %+v", parts[2], err) } return CredentialId{ ObjectId: parts[0], - KeyId: parts[1], + KeyType: parts[1], + KeyId: parts[2], }, nil } -func CredentialIdFrom(objectId, keyId string) CredentialId { +func ParseOldCredentialId(id, keyType string) (CredentialId, error) { + parts := strings.Split(id, "/") + if len(parts) != 2 { + return CredentialId{}, fmt.Errorf("Credential ID expected to be in the format {objectId}/{keyId} - but got %q", id) + } + + newId := parts[0] + "/" + keyType + "/" + parts[1] + return ParseCredentialId(newId) +} + +func CredentialIdFrom(objectId, keyType, keyId string) CredentialId { return CredentialId{ ObjectId: objectId, + KeyType: keyType, KeyId: keyId, } } diff --git a/azuread/resource_application.go b/azuread/resource_application.go index 8f6822311a..4eccd26549 100644 --- a/azuread/resource_application.go +++ b/azuread/resource_application.go @@ -745,6 +745,12 @@ func expandADApplicationOptionalClaim(in []interface{}) *[]graphrbac.OptionalCla } func flattenADApplicationOptionalClaims(in *graphrbac.OptionalClaims) interface{} { + var result []map[string]interface{} + + if in == nil { + return result + } + optionalClaims := make(map[string]interface{}) if claims := flattenADApplicationOptionalClaimsList(in.AccessToken); len(claims) > 0 { optionalClaims["access_token"] = claims @@ -756,7 +762,6 @@ func flattenADApplicationOptionalClaims(in *graphrbac.OptionalClaims) interface{ //if claims := flattenADApplicationOptionalClaimsList(in.SamlToken); len(claims) > 0 { // optionalClaims["saml_token"] = claims //} - var result []map[string]interface{} if len(optionalClaims) == 0 { return result } diff --git a/azuread/resource_application_certificate.go b/azuread/resource_application_certificate.go index 75300ecf7c..751ee4df19 100644 --- a/azuread/resource_application_certificate.go +++ b/azuread/resource_application_certificate.go @@ -37,7 +37,7 @@ func resourceApplicationCertificateCreate(d *schema.ResourceData, meta interface if err != nil { return fmt.Errorf("generating certificate credentials for object ID %q: %+v", objectId, err) } - id := graph.CredentialIdFrom(objectId, *cred.KeyID) + id := graph.CredentialIdFrom(objectId, "certificate", *cred.KeyID) tf.LockByName(resourceApplicationName, id.ObjectId) defer tf.UnlockByName(resourceApplicationName, id.ObjectId) diff --git a/azuread/resource_application_password.go b/azuread/resource_application_password.go index ea6380db6e..5b77d109be 100644 --- a/azuread/resource_application_password.go +++ b/azuread/resource_application_password.go @@ -7,12 +7,10 @@ import ( "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/ar" "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/graph" "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/tf" - "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/validate" ) func resourceApplicationPassword() *schema.Resource { @@ -25,73 +23,14 @@ func resourceApplicationPassword() *schema.Resource { State: schema.ImportStatePassthrough, }, - // Schema: graph.PasswordResourceSchema("application_object"), //todo switch back to this in 1.0 - Schema: map[string]*schema.Schema{ - "application_id": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Computed: true, - ValidateFunc: validate.UUID, - Deprecated: "Deprecated in favour of `application_object_id` to prevent confusion", - ConflictsWith: []string{"application_object_id"}, - }, - - "application_object_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ValidateFunc: validate.UUID, - ConflictsWith: []string{"application_id"}, - }, - - "key_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ValidateFunc: validate.UUID, - }, - - "description": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - - "value": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Sensitive: true, - ValidateFunc: validation.StringLenBetween(1, 863), // Encrypted secret cannot be empty and can be at most 1024 bytes. - }, + Schema: graph.PasswordResourceSchema("application"), - "start_date": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ValidateFunc: validation.IsRFC3339Time, - }, - - "end_date": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"end_date_relative"}, - ValidateFunc: validation.IsRFC3339Time, - }, - - "end_date_relative": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - ConflictsWith: []string{"end_date"}, - ValidateFunc: validate.NoEmptyStrings, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceApplicationPasswordInstanceResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceApplicationPasswordInstanceStateUpgradeV0, + Version: 0, }, }, } @@ -105,15 +44,12 @@ func resourceApplicationPasswordCreate(d *schema.ResourceData, meta interface{}) if objectId == "" { // todo remove in 1.0 objectId = d.Get("application_id").(string) } - if objectId == "" { - return fmt.Errorf("one of `application_object_id` or `application_id` must be specified") - } cred, err := graph.PasswordCredentialForResource(d) if err != nil { return fmt.Errorf("Error generating Application Credentials for Object ID %q: %+v", objectId, err) } - id := graph.CredentialIdFrom(objectId, *cred.KeyID) + id := graph.CredentialIdFrom(objectId, "password", *cred.KeyID) tf.LockByName(resourceApplicationName, id.ObjectId) defer tf.UnlockByName(resourceApplicationName, id.ObjectId) @@ -231,3 +167,20 @@ func resourceApplicationPasswordDelete(d *schema.ResourceData, meta interface{}) return nil } + +func resourceApplicationPasswordInstanceResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: graph.PasswordResourceSchema("application"), + } +} + +func resourceApplicationPasswordInstanceStateUpgradeV0(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + log.Println("[DEBUG] Migrating ID from v0 to v1 format") + newId, err := graph.ParseOldCredentialId(rawState["id"].(string), "password") + if err != nil { + return rawState, fmt.Errorf("generating new ID: %s", err) + } + + rawState["id"] = newId.String() + return rawState, nil +} diff --git a/azuread/resource_application_password_test.go b/azuread/resource_application_password_test.go index f26ffb5ef3..ad31c77664 100644 --- a/azuread/resource_application_password_test.go +++ b/azuread/resource_application_password_test.go @@ -97,6 +97,12 @@ func TestAccAzureADApplicationPassword_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "end_date", "2099-01-01T01:02:03Z"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"value"}, + }, }, }) } @@ -121,6 +127,12 @@ func TestAccAzureADApplicationPassword_basicOld(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "end_date", "2099-01-01T01:02:03Z"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"value"}, + }, }, }) } @@ -174,6 +186,12 @@ func TestAccAzureADApplicationPassword_customKeyId(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "end_date", "2099-01-01T01:02:03Z"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"value"}, + }, }, }) } @@ -197,6 +215,12 @@ func TestAccAzureADApplicationPassword_description(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "end_date", "2099-01-01T01:02:03Z"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"value"}, + }, }, }) } @@ -221,6 +245,12 @@ func TestAccAzureADApplicationPassword_relativeEndDate(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "end_date"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"end_date_relative", "value"}, + }, }, }) } diff --git a/azuread/resource_service_principal_certificate.go b/azuread/resource_service_principal_certificate.go index a1ac524306..72b500c4f0 100644 --- a/azuread/resource_service_principal_certificate.go +++ b/azuread/resource_service_principal_certificate.go @@ -37,7 +37,7 @@ func resourceServicePrincipalCertificateCreate(d *schema.ResourceData, meta inte if err != nil { return fmt.Errorf("generating certificate credentials for object ID %q: %+v", objectId, err) } - id := graph.CredentialIdFrom(objectId, *cred.KeyID) + id := graph.CredentialIdFrom(objectId, "certificate", *cred.KeyID) tf.LockByName(servicePrincipalResourceName, id.ObjectId) defer tf.UnlockByName(servicePrincipalResourceName, id.ObjectId) diff --git a/azuread/resource_service_principal_password.go b/azuread/resource_service_principal_password.go index 6d6407c090..35d3215453 100644 --- a/azuread/resource_service_principal_password.go +++ b/azuread/resource_service_principal_password.go @@ -24,6 +24,15 @@ func resourceServicePrincipalPassword() *schema.Resource { }, Schema: graph.PasswordResourceSchema("service_principal"), + + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceServicePrincipalPasswordInstanceResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceServicePrincipalPasswordInstanceStateUpgradeV0, + Version: 0, + }, + }, } } @@ -37,7 +46,7 @@ func resourceServicePrincipalPasswordCreate(d *schema.ResourceData, meta interfa if err != nil { return fmt.Errorf("Error generating Service Principal Credentials for Object ID %q: %+v", objectId, err) } - id := graph.CredentialIdFrom(objectId, *cred.KeyID) + id := graph.CredentialIdFrom(objectId, "password", *cred.KeyID) tf.LockByName(servicePrincipalResourceName, id.ObjectId) defer tf.UnlockByName(servicePrincipalResourceName, id.ObjectId) @@ -155,3 +164,20 @@ func resourceServicePrincipalPasswordDelete(d *schema.ResourceData, meta interfa return nil } + +func resourceServicePrincipalPasswordInstanceResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: graph.PasswordResourceSchema("service_principal"), + } +} + +func resourceServicePrincipalPasswordInstanceStateUpgradeV0(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + log.Println("[DEBUG] Migrating ID from v0 to v1 format") + newId, err := graph.ParseOldCredentialId(rawState["id"].(string), "password") + if err != nil { + return rawState, fmt.Errorf("generating new ID: %s", err) + } + + rawState["id"] = newId.String() + return rawState, nil +} diff --git a/azuread/resource_service_principal_password_test.go b/azuread/resource_service_principal_password_test.go index e5341b63b0..0d1282fdb0 100644 --- a/azuread/resource_service_principal_password_test.go +++ b/azuread/resource_service_principal_password_test.go @@ -98,6 +98,12 @@ func TestAccAzureADServicePrincipalPassword_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "end_date", "2099-01-01T01:02:03Z"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"value"}, + }, }, }) } @@ -152,6 +158,12 @@ func TestAccAzureADServicePrincipalPassword_customKeyId(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "end_date", "2099-01-01T01:02:03Z"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"value"}, + }, }, }) } @@ -176,6 +188,12 @@ func TestAccAzureADServicePrincipalPassword_description(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "end_date", "2099-01-01T01:02:03Z"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"value"}, + }, }, }) } @@ -200,6 +218,12 @@ func TestAccAzureADServicePrincipalPassword_relativeEndDate(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "end_date"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"end_date_relative", "value"}, + }, }, }) } diff --git a/website/docs/r/application_certificate.html.markdown b/website/docs/r/application_certificate.html.markdown index 4bea90a951..7c68a3c303 100644 --- a/website/docs/r/application_certificate.html.markdown +++ b/website/docs/r/application_certificate.html.markdown @@ -57,10 +57,10 @@ The following attributes are exported: ## Import -Certificates can be imported using the `object id` of an Application, e.g. +Certificates can be imported using the `object id` of an Application and the `key id` of the certificate, e.g. ```shell -terraform import azuread_application_certificate.test 00000000-0000-0000-0000-000000000000/11111111-1111-1111-1111-111111111111 +terraform import azuread_application_certificate.test 00000000-0000-0000-0000-000000000000/certificate/11111111-1111-1111-1111-111111111111 ``` --> **NOTE:** This ID format is unique to Terraform and is composed of the Application's Object ID and the Certificate's Key ID in the format `{ObjectId}/{CertificateKeyId}`. +-> **NOTE:** This ID format is unique to Terraform and is composed of the Application's Object ID, the string "certificate" and the Certificate's Key ID in the format `{ObjectId}/certificate/{CertificateKeyId}`. diff --git a/website/docs/r/application_password.html.markdown b/website/docs/r/application_password.html.markdown index 0dd0732c10..a648e45a84 100644 --- a/website/docs/r/application_password.html.markdown +++ b/website/docs/r/application_password.html.markdown @@ -64,10 +64,10 @@ The following attributes are exported: ## Import -Passwords can be imported using the `object id` of an Application, e.g. +Passwords can be imported using the `object id` of an Application and the `key id` of the password, e.g. ```shell -terraform import azuread_application_password.test 00000000-0000-0000-0000-000000000000/11111111-1111-1111-1111-111111111111 +terraform import azuread_application_password.test 00000000-0000-0000-0000-000000000000/password/11111111-1111-1111-1111-111111111111 ``` --> **NOTE:** This ID format is unique to Terraform and is composed of the Application's Object ID and the Password's Key ID in the format `{ObjectId}/{PasswordKeyId}`. +-> **NOTE:** This ID format is unique to Terraform and is composed of the Application's Object ID, the string "password" and the Password's Key ID in the format `{ObjectId}/password/{PasswordKeyId}`. diff --git a/website/docs/r/service_principal_certificate.html.markdown b/website/docs/r/service_principal_certificate.html.markdown index c8be654210..c2a0cce9ae 100644 --- a/website/docs/r/service_principal_certificate.html.markdown +++ b/website/docs/r/service_principal_certificate.html.markdown @@ -61,10 +61,10 @@ The following attributes are exported: ## Import -Service Principal Certificates can be imported using the `object id`, e.g. +Certificates can be imported using the `object id` of the Service Principal and the `key id` of the certificate, e.g. ```shell -terraform import azuread_service_principal_certificate.test 00000000-0000-0000-0000-000000000000/11111111-1111-1111-1111-111111111111 +terraform import azuread_service_principal_certificate.test 00000000-0000-0000-0000-000000000000/certificate/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 Certificate's Key ID in the format `{ServicePrincipalObjectId}/{ServicePrincipalCertificateKeyId}`. +-> **NOTE:** This ID format is unique to Terraform and is composed of the Service Principal's Object ID, the string "certificate" and the Certificate's Key ID in the format `{ServicePrincipalObjectId}/certificate/{CertificateKeyId}`. diff --git a/website/docs/r/service_principal_password.html.markdown b/website/docs/r/service_principal_password.html.markdown index 04c5d0a680..ad31b94376 100644 --- a/website/docs/r/service_principal_password.html.markdown +++ b/website/docs/r/service_principal_password.html.markdown @@ -68,10 +68,10 @@ The following attributes are exported: ## Import -Service Principal Passwords can be imported using the `object id`, e.g. +PPasswords can be imported using the `object id` of a Service Principal and the `key id` of the password, e.g. ```shell terraform import 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}`. +-> **NOTE:** This ID format is unique to Terraform and is composed of the Service Principal's Object ID, the string "password" and the Password's Key ID in the format `{ServicePrincipalObjectId}/password/{PasswordKeyId}`.