From 5d99ce1b2a0368c8dce6175466c09c68b996c2b6 Mon Sep 17 00:00:00 2001 From: tagur87 <43474056+tagur87@users.noreply.github.com> Date: Tue, 10 Jan 2023 15:54:16 -0500 Subject: [PATCH 1/3] Add token signing certificate resource This adds a new resource called `service_principal_token_signing_certificate` that is used to manage the whole lifecycle of token signing certificates used for SAML authentication. This resource makes use of the `AddTokenSigningCertificate` function that was added to hamilton previously here: https://github.com/manicminer/hamilton/pull/158 MS Graphs Docs: https://learn.microsoft.com/en-us/graph/api/serviceprincipal-addtokensigningcertificate?view=graph-rest-1.0&tabs=http As documented in the docs above, when the `AddTokenSigningCertificate` function is invoked, 3 individual objects are created... - Verify `keyCredential` (Public Cert) - Sign `keyCredential` (Private Key) - `passwordCredential` (Private Key Password) When the object is returned, it includes the thumbprint, the public key pem value, and a `keyId`. However, we found an odd behavior that the `keyId` that is returned is actually for the Sign `keyCredential`. Since the Verify certificate is the one that we acutally care about, we used the `customKeyIdentifier`, which is the same for all 3 values, to get the Verify `keyId`, which we then use in building the resource ID. We additionally had to "calculate" the thumbprint value from the actual value of the Verify cert, as this value is not returned from the API, except after initial creation in the Create step. We did this by getting pem value of the Verify cert by adding the `$select=keyCredential` odata query to the GET of the service principal. By combining this value with the PEM header/footer, we can calculate the SHA-1 fingerprint, which matches up to the appropriate thumbprint. Finally, to delete the certificate, we have to PATCH the service principal with all 3 objects mentioned previously removed. To gather this, we used the `customKeyIdentifier` value in a loop. Closes #732 And part of https://github.com/hashicorp/terraform-provider-azuread/issues/823 --- ...ice_principal_token_signing_certificate.md | 87 +++++ internal/helpers/credentials.go | 33 ++ .../serviceprincipals/parse/credentials.go | 13 + .../serviceprincipals/registration.go | 1 + ...ipal_token_signing_certificate_resource.go | 306 ++++++++++++++++++ ...token_signing_certificate_resource_test.go | 124 +++++++ 6 files changed, 564 insertions(+) create mode 100644 docs/resources/service_principal_token_signing_certificate.md create mode 100644 internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go create mode 100644 internal/services/serviceprincipals/service_principal_token_signing_certificate_resource_test.go diff --git a/docs/resources/service_principal_token_signing_certificate.md b/docs/resources/service_principal_token_signing_certificate.md new file mode 100644 index 0000000000..fcabf02ef9 --- /dev/null +++ b/docs/resources/service_principal_token_signing_certificate.md @@ -0,0 +1,87 @@ +--- +subcategory: "Service Principals" +--- + +# Resource: azuread_service_principal_token_signing_certificate + +Manages a token signing certificate associated with a service principal within Azure Active Directory. + +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Application.ReadWrite.All` or `Directory.ReadWrite.All` + +When authenticated with a user principal, this resource requires one of the following directory roles: `Application Administrator` or `Global Administrator` + +## Example Usage + +*Using default settings* + +```terraform +resource "azuread_application" "example" { + display_name = "example" +} + +resource "azuread_service_principal" "example" { + application_id = azuread_application.example.application_id +} + +resource "azuread_service_principal_token_signing_certificate" "example" { + service_principal_id = azuread_service_principal.example.id +} +``` + +*Using custom settings* + +```terraform +resource "azuread_application" "example" { + display_name = "example" +} + +resource "azuread_service_principal" "example" { + application_id = azuread_application.example.application_id +} + +resource "azuread_service_principal_token_signing_certificate" "example" { + service_principal_id = azuread_service_principal.example.id + display_name = "CN=example.com" + end_date = "2023-05-01T01:02:03Z" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `display_name` - (Optional) Specifies a friendly name for the certificate. + Must start with `CN=`. Changing this field forces a new resource to be created. + +~> If not specified, it will default to `CN=Microsoft Azure Federated SSO Certificate`. + +* `end_date` - (Optional) The end date until which the token signing certificate is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). Changing this field forces a new resource to be created. + +* `service_principal_id` - (Required) The object ID of the service principal for which this certificate should be created. Changing this field forces a new resource to be created. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `key_id` - A UUID used to uniquely identify the verify certificate. + +* `thumbprint` - A SHA-1 generated thumbprint of the token signing certificate, which can be used to set the preferred signing certificate for a service principal. + +* `start_date` - The start date from which the certificate is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). + +* `value` - The certificate data, which is pem encoded but does not include the +header `-----BEGIN CERTIFICATE-----\n` or the footer `\n-----END CERTIFICATE-----`. + +## Import + +Token signing certificates can be imported using the object ID of the associated service principal and the key ID of the verify certificate credential, e.g. + +```shell +terraform import azuread_service_principal_token_signing_certificate.test 00000000-0000-0000-0000-000000000000/tokenSigningCertificate/11111111-1111-1111-1111-111111111111 +``` + +-> This ID format is unique to Terraform and is composed of the service principal's object ID, the string "tokenSigningCertificate" and the verify certificate's key ID in the format `{ServicePrincipalObjectId}/tokenSigningCertificate/{CertificateKeyId}`. diff --git a/internal/helpers/credentials.go b/internal/helpers/credentials.go index 0b60b2c0b5..ab86611f4b 100644 --- a/internal/helpers/credentials.go +++ b/internal/helpers/credentials.go @@ -1,6 +1,9 @@ package helpers import ( + "bytes" + "crypto/sha1" + "crypto/x509" "encoding/base64" "encoding/hex" "encoding/pem" @@ -40,6 +43,18 @@ func GetKeyCredential(keyCredentials *[]msgraph.KeyCredential, id string) (crede return } +func GetVerifyKeyCredentialFromCustomKeyId(keyCredentials *[]msgraph.KeyCredential, id string) (credential *msgraph.KeyCredential) { + if keyCredentials != nil { + for _, cred := range *keyCredentials { + if cred.KeyId != nil && strings.EqualFold(*cred.CustomKeyIdentifier, id) && strings.EqualFold(cred.Usage, msgraph.KeyCredentialUsageVerify) { + credential = &cred + break + } + } + } + return +} + func GetPasswordCredential(passwordCredentials *[]msgraph.PasswordCredential, id string) (credential *msgraph.PasswordCredential) { if passwordCredentials != nil { for _, cred := range *passwordCredentials { @@ -52,6 +67,24 @@ func GetPasswordCredential(passwordCredentials *[]msgraph.PasswordCredential, id return } +func GetTokenSigningCertificateThumbprint(certByte []byte) (string, error) { + block, _ := pem.Decode(certByte) + if block == nil { + return "", fmt.Errorf("Failed to decode certificate block") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return "", fmt.Errorf("failed to parse certificate block data: %+v", err) + } + thumbprint := sha1.Sum(cert.Raw) + + var buf bytes.Buffer + for _, f := range thumbprint { + fmt.Fprintf(&buf, "%02X", f) + } + return buf.String(), nil +} + func KeyCredentialForResource(d *schema.ResourceData) (*msgraph.KeyCredential, error) { keyType := d.Get("type").(string) value := d.Get("value").(string) diff --git a/internal/services/serviceprincipals/parse/credentials.go b/internal/services/serviceprincipals/parse/credentials.go index 0e4674b42b..c35e36d1ac 100644 --- a/internal/services/serviceprincipals/parse/credentials.go +++ b/internal/services/serviceprincipals/parse/credentials.go @@ -23,6 +23,19 @@ func (id CredentialId) String() string { return id.ObjectId + "/" + id.KeyType + "/" + id.KeyId } +func SigningCertificateID(idString string) (*CredentialId, error) { + id, err := ObjectSubResourceID(idString, "tokenSigningCertificate") + if err != nil { + return nil, fmt.Errorf("unable to parse signing certificate ID: %v", err) + } + + return &CredentialId{ + ObjectId: id.objectId, + KeyType: id.Type, + KeyId: id.subId, + }, nil +} + func CertificateID(idString string) (*CredentialId, error) { id, err := ObjectSubResourceID(idString, "certificate") if err != nil { diff --git a/internal/services/serviceprincipals/registration.go b/internal/services/serviceprincipals/registration.go index 788a999ce9..4fd72e0f80 100644 --- a/internal/services/serviceprincipals/registration.go +++ b/internal/services/serviceprincipals/registration.go @@ -35,6 +35,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { "azuread_service_principal_claims_mapping_policy_assignment": servicePrincipalClaimsMappingPolicyAssignmentResource(), "azuread_service_principal_delegated_permission_grant": servicePrincipalDelegatedPermissionGrantResource(), "azuread_service_principal_password": servicePrincipalPasswordResource(), + "azuread_service_principal_token_signing_certificate": servicePrincipalTokenSigningCertificateResource(), "azuread_synchronization_job": synchronizationJobResource(), "azuread_synchronization_secret": synchronizationSecretResource(), } diff --git a/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go b/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go new file mode 100644 index 0000000000..44de9e9488 --- /dev/null +++ b/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go @@ -0,0 +1,306 @@ +package serviceprincipals + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "regexp" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/manicminer/hamilton/msgraph" + "github.com/manicminer/hamilton/odata" + + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" + "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/parse" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/hashicorp/terraform-provider-azuread/internal/validate" +) + +func servicePrincipalTokenSigningCertificateResource() *schema.Resource { + return &schema.Resource{ + CreateContext: servicePrincipalTokenSigningCertificateResourceCreate, + ReadContext: servicePrincipalTokenSigningCertificateResourceRead, + DeleteContext: servicePrincipalTokenSigningCertificateResourceDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Importer: tf.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.SigningCertificateID(id) + return err + }), + + Schema: map[string]*schema.Schema{ + "service_principal_id": { + Description: "The object ID of the service principal for which this certificate should be created", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + + "display_name": { + Description: "A friendly name for the certificate", + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateDiagFunc: validate.ValidateDiag(validation.StringMatch(regexp.MustCompile("^CN=.+$|^$"), "")), + }, + + "end_date": { + Description: "The end date until which the certificate is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). Default is 3 years from current date.", + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.IsRFC3339Time, + }, + + "key_id": { + Description: "A UUID used to uniquely identify the verify certificate.", + Type: schema.TypeString, + Computed: true, + }, + + "thumbprint": { + Description: "The thumbprint of the certificate.", + Type: schema.TypeString, + Computed: true, + }, + + "start_date": { + Description: "The start date from which the certificate is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). If this isn't specified, the current date is used", + Type: schema.TypeString, + Computed: true, + }, + + "value": { + Description: "The certificate data, which can be PEM encoded, base64 encoded DER or hexadecimal encoded DER", + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + } +} + +func servicePrincipalTokenSigningCertificateResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + objectId := d.Get("service_principal_id").(string) + + keyCreds := msgraph.KeyCredential{} + if v, ok := d.GetOk("display_name"); ok { + keyCreds.DisplayName = utils.String(v.(string)) + } + + if v, ok := d.GetOk("end_date"); ok { + endDate, err := time.Parse(time.RFC3339, v.(string)) + if err != nil { + tf.ErrorDiagF(err, "Unable to parse the provided end_date %q: %+v", v, err) + } + keyCreds.EndDateTime = &endDate + } + + key, _, err := client.AddTokenSigningCertificate(ctx, objectId, keyCreds) + if err != nil { + return tf.ErrorDiagF(err, "Could not add token signing certificate to service principal with object ID: %q", objectId) + } + + tf.LockByName(servicePrincipalResourceName, objectId) + defer tf.UnlockByName(servicePrincipalResourceName, objectId) + + // Wait for the credential to appear in the service principal manifest, this can take several minutes + timeout, _ := ctx.Deadline() + polledForCredential, err := (&resource.StateChangeConf{ + Pending: []string{"Waiting"}, + Target: []string{"Done"}, + Timeout: time.Until(timeout), + MinTimeout: 1 * time.Second, + ContinuousTargetOccurence: 5, + Refresh: func() (interface{}, string, error) { + servicePrincipal, _, err := client.Get(ctx, objectId, odata.Query{}) + if err != nil { + return nil, "Error", err + } + + if servicePrincipal.KeyCredentials != nil { + for _, cred := range *servicePrincipal.KeyCredentials { + if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, *key.KeyId) { + return &cred, "Done", nil + } + } + } + + return nil, "Waiting", nil + }, + }).WaitForStateContext(ctx) + + if err != nil { + return tf.ErrorDiagF(err, "Waiting for token_signing_certificate credential for service principal with object ID %q", objectId) + } else if polledForCredential == nil { + return tf.ErrorDiagF(errors.New("certificate credential not found in service principal manifest"), "Waiting for certificate credential for service principal with object ID %q", objectId) + } + + // Workaround b/c the returned keyId is for the Sign key, rather than Verify key, + // so we need to get the Verify keyId based on the customKeyIdentifier + servicePrincipal, _, err := client.Get(ctx, objectId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not get service principal with object ID: %q", objectId) + } + credential := helpers.GetVerifyKeyCredentialFromCustomKeyId(servicePrincipal.KeyCredentials, *key.CustomKeyIdentifier) + + id := parse.NewCredentialID(objectId, "tokenSigningCertificate", *credential.KeyId) + + d.SetId(id.String()) + + return servicePrincipalTokenSigningCertificateResourceRead(ctx, d, meta) +} + +func servicePrincipalTokenSigningCertificateResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + + id, err := parse.SigningCertificateID(d.Id()) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Parsing certificate credential with ID %q", d.Id()) + } + + servicePrincipal, status, err := client.Get(ctx, id.ObjectId, odata.Query{ + Select: []string{"keyCredentials"}, + }) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Service Principal with ID %q for %s credential %q was not found - removing from state!", id.ObjectId, id.KeyType, id.KeyId) + d.SetId("") + return nil + } + return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving service principal with object ID %q", id.ObjectId) + } + + credential := helpers.GetKeyCredential(servicePrincipal.KeyCredentials, id.KeyId) + if credential == nil { + log.Printf("[DEBUG] Certificate credential %q (ID %q) was not found - removing from state!", id.KeyId, id.ObjectId) + d.SetId("") + return nil + } + + tf.Set(d, "service_principal_id", id.ObjectId) + tf.Set(d, "key_id", id.KeyId) + tf.Set(d, "display_name", credential.DisplayName) + tf.Set(d, "value", credential.Key) + + startDate := "" + if v := credential.StartDateTime; v != nil { + startDate = v.Format(time.RFC3339) + } + tf.Set(d, "start_date", startDate) + + endDate := "" + if v := credential.EndDateTime; v != nil { + endDate = v.Format(time.RFC3339) + } + tf.Set(d, "end_date", endDate) + + // thumbprint not available when querying service principal, so we generate it from the pem value in the Key field. + thumbprint, err := helpers.GetTokenSigningCertificateThumbprint( + []byte("-----BEGIN CERTIFICATE-----\n" + *credential.Key + "\n-----END CERTIFICATE-----")) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "parsing tokenSigningCertificate key value with ID %q", id.KeyId) + } + + tf.Set(d, "thumbprint", thumbprint) + + return nil +} + +func servicePrincipalTokenSigningCertificateResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + + id, err := parse.SigningCertificateID(d.Id()) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Parsing certificate credential with ID %q", d.Id()) + } + + tf.LockByName(servicePrincipalResourceName, id.ObjectId) + defer tf.UnlockByName(servicePrincipalResourceName, id.ObjectId) + + app, status, err := client.Get(ctx, id.ObjectId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagPathF(fmt.Errorf("Service Principal was not found"), "service_principal_id", "Retrieving service principal with object ID %q", id.ObjectId) + } + return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving service principal with object ID %q", id.ObjectId) + } + + // use CustomKeyIdentifier to determine which certs and passwords + // are associated together. + customKeyId := "" + newKeyCredentials := make([]msgraph.KeyCredential, 0) + if app.KeyCredentials != nil { + for _, cred := range *app.KeyCredentials { + if cred.KeyId != nil && !strings.EqualFold(*cred.KeyId, id.KeyId) { + customKeyId = *cred.CustomKeyIdentifier + } + } + for _, cred := range *app.KeyCredentials { + if cred.KeyId != nil && !strings.EqualFold(*cred.CustomKeyIdentifier, customKeyId) { + newKeyCredentials = append(newKeyCredentials, cred) + } + } + } + log.Printf("[Info] App Password: %v", *app.PasswordCredentials) + + newPasswordCredentials := make([]msgraph.PasswordCredential, 0) + if app.PasswordCredentials != nil { + for _, cred := range *app.PasswordCredentials { + if cred.KeyId != nil && !strings.EqualFold(*cred.CustomKeyIdentifier, customKeyId) { + newPasswordCredentials = append(newPasswordCredentials, cred) + } + } + } + + properties := msgraph.ServicePrincipal{ + DirectoryObject: msgraph.DirectoryObject{ + Id: &id.ObjectId, + }, + KeyCredentials: &newKeyCredentials, + PasswordCredentials: &newPasswordCredentials, + } + if _, err := client.Update(ctx, properties); err != nil { + return tf.ErrorDiagF(err, "Removing token signing certificate credentials %q from service principal with object ID %q", id.KeyId, id.ObjectId) + } + + // Wait for service principal token signing certificate to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + + servicePrincipal, _, err := client.Get(ctx, id.ObjectId, odata.Query{}) + if err != nil { + return nil, err + } + + credential := helpers.GetKeyCredential(servicePrincipal.KeyCredentials, id.KeyId) + if credential == nil { + return utils.Bool(false), nil + } + + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of token signing certificate credential %q from service principal with object ID %q", id.KeyId, id.ObjectId) + } + + return nil +} diff --git a/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource_test.go b/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource_test.go new file mode 100644 index 0000000000..2b1ae04da2 --- /dev/null +++ b/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource_test.go @@ -0,0 +1,124 @@ +package serviceprincipals_test + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/manicminer/hamilton/odata" + + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/parse" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" +) + +type servicePrincipalTokenSigningCertificateResource struct{} + +func TestAccServicePrincipalTokenSigningCertificate_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal_token_signing_certificate", "test") + r := servicePrincipalTokenSigningCertificateResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("display_name").Exists(), + check.That(data.ResourceName).Key("end_date").Exists(), + check.That(data.ResourceName).Key("key_id").Exists(), + check.That(data.ResourceName).Key("thumbprint").Exists(), + check.That(data.ResourceName).Key("value").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccServicePrincipalTokenSigningCertificate_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal_token_signing_certificate", "test") + endDate := time.Now().AddDate(0, 3, 27).UTC().Format(time.RFC3339) + r := servicePrincipalTokenSigningCertificateResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data, endDate), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("display_name").Exists(), + check.That(data.ResourceName).Key("end_date").Exists(), + check.That(data.ResourceName).Key("key_id").Exists(), + check.That(data.ResourceName).Key("thumbprint").Exists(), + check.That(data.ResourceName).Key("value").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func (r servicePrincipalTokenSigningCertificateResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.ServicePrincipals.ServicePrincipalsClient + client.BaseClient.DisableRetries = true + + id, err := parse.SigningCertificateID(state.ID) + if err != nil { + return nil, fmt.Errorf("parsing Service Principal Token Signing Certificate ID: %v", err) + } + + servicePrincipal, status, err := client.Get(ctx, id.ObjectId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return nil, fmt.Errorf("Service Principal with object ID %q does not exist", id.ObjectId) + } + return nil, fmt.Errorf("failed to retrieve Service Principal with object ID %q: %+v", id.ObjectId, err) + } + + if servicePrincipal.KeyCredentials != nil { + for _, cred := range *servicePrincipal.KeyCredentials { + if cred.KeyId != nil && *cred.KeyId == id.KeyId { + return utils.Bool(true), nil + } + } + } + + return nil, fmt.Errorf("Token Signing Key Credential %q was not found for Service Principal %q", id.KeyId, id.ObjectId) +} + +func (servicePrincipalTokenSigningCertificateResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_application" "test" { + display_name = "acctestServicePrincipal-%[1]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id +} +`, data.RandomInteger) +} + +func (r servicePrincipalTokenSigningCertificateResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_service_principal_token_signing_certificate" "test" { + service_principal_id = azuread_service_principal.test.id +} +`, r.template(data)) +} + +func (r servicePrincipalTokenSigningCertificateResource) complete(data acceptance.TestData, endDate string) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_service_principal_token_signing_certificate" "test" { + service_principal_id = azuread_service_principal.test.id + display_name = "CN=acctestTokenSigningCert-%[2]s" + end_date = "%[3]s" +} +`, r.template(data), data.RandomID, endDate) +} From 8f264a8959b6d3a2873a59852492bc99ea54a9e1 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Wed, 18 Jan 2023 23:07:22 +0000 Subject: [PATCH 2/3] Apply suggestions from code review --- .../service_principal_token_signing_certificate.md | 4 ++-- ..._principal_token_signing_certificate_resource.go | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/resources/service_principal_token_signing_certificate.md b/docs/resources/service_principal_token_signing_certificate.md index fcabf02ef9..8453ef646d 100644 --- a/docs/resources/service_principal_token_signing_certificate.md +++ b/docs/resources/service_principal_token_signing_certificate.md @@ -73,7 +73,7 @@ In addition to all arguments above, the following attributes are exported: * `start_date` - The start date from which the certificate is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). -* `value` - The certificate data, which is pem encoded but does not include the +* `value` - The certificate data, which is PEM encoded but does not include the header `-----BEGIN CERTIFICATE-----\n` or the footer `\n-----END CERTIFICATE-----`. ## Import @@ -81,7 +81,7 @@ header `-----BEGIN CERTIFICATE-----\n` or the footer `\n-----END CERTIFICATE---- Token signing certificates can be imported using the object ID of the associated service principal and the key ID of the verify certificate credential, e.g. ```shell -terraform import azuread_service_principal_token_signing_certificate.test 00000000-0000-0000-0000-000000000000/tokenSigningCertificate/11111111-1111-1111-1111-111111111111 +terraform import azuread_service_principal_token_signing_certificate.example 00000000-0000-0000-0000-000000000000/tokenSigningCertificate/11111111-1111-1111-1111-111111111111 ``` -> This ID format is unique to Terraform and is composed of the service principal's object ID, the string "tokenSigningCertificate" and the verify certificate's key ID in the format `{ServicePrincipalObjectId}/tokenSigningCertificate/{CertificateKeyId}`. diff --git a/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go b/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go index 44de9e9488..dc30436312 100644 --- a/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go +++ b/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go @@ -83,13 +83,13 @@ func servicePrincipalTokenSigningCertificateResource() *schema.Resource { }, "start_date": { - Description: "The start date from which the certificate is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). If this isn't specified, the current date is used", + Description: "The start date from which the certificate is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`).", Type: schema.TypeString, Computed: true, }, "value": { - Description: "The certificate data, which can be PEM encoded, base64 encoded DER or hexadecimal encoded DER", + Description: "The certificate data, which is PEM encoded but does not include the header/footer", Type: schema.TypeString, Computed: true, Sensitive: true, @@ -115,13 +115,14 @@ func servicePrincipalTokenSigningCertificateResourceCreate(ctx context.Context, keyCreds.EndDateTime = &endDate } + tf.LockByName(servicePrincipalResourceName, objectId) + defer tf.UnlockByName(servicePrincipalResourceName, objectId) + key, _, err := client.AddTokenSigningCertificate(ctx, objectId, keyCreds) if err != nil { return tf.ErrorDiagF(err, "Could not add token signing certificate to service principal with object ID: %q", objectId) } - tf.LockByName(servicePrincipalResourceName, objectId) - defer tf.UnlockByName(servicePrincipalResourceName, objectId) // Wait for the credential to appear in the service principal manifest, this can take several minutes timeout, _ := ctx.Deadline() @@ -163,6 +164,9 @@ func servicePrincipalTokenSigningCertificateResourceCreate(ctx context.Context, } credential := helpers.GetVerifyKeyCredentialFromCustomKeyId(servicePrincipal.KeyCredentials, *key.CustomKeyIdentifier) + if credential == nil { + return tf.ErrorDiagF(errors.New("returned credential was nil"), "Could not determine key ID for newly added token signing certificate on service principal %q", objectId) + } id := parse.NewCredentialID(objectId, "tokenSigningCertificate", *credential.KeyId) d.SetId(id.String()) @@ -261,7 +265,6 @@ func servicePrincipalTokenSigningCertificateResourceDelete(ctx context.Context, } } } - log.Printf("[Info] App Password: %v", *app.PasswordCredentials) newPasswordCredentials := make([]msgraph.PasswordCredential, 0) if app.PasswordCredentials != nil { From 007f4b467295f6d553dcc1c8079464fa0f69b0f8 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Wed, 18 Jan 2023 23:10:50 +0000 Subject: [PATCH 3/3] linting --- .../service_principal_token_signing_certificate_resource.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go b/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go index dc30436312..8e86bf0a39 100644 --- a/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go +++ b/internal/services/serviceprincipals/service_principal_token_signing_certificate_resource.go @@ -123,7 +123,6 @@ func servicePrincipalTokenSigningCertificateResourceCreate(ctx context.Context, return tf.ErrorDiagF(err, "Could not add token signing certificate to service principal with object ID: %q", objectId) } - // Wait for the credential to appear in the service principal manifest, this can take several minutes timeout, _ := ctx.Deadline() polledForCredential, err := (&resource.StateChangeConf{