diff --git a/docs/data-sources/service_principal.md b/docs/data-sources/service_principal.md index 9eda478a16..6c98078314 100644 --- a/docs/data-sources/service_principal.md +++ b/docs/data-sources/service_principal.md @@ -74,6 +74,7 @@ The following attributes are exported: * `preferred_single_sign_on_mode` - The single sign-on mode configured for this application. Azure AD uses the preferred single sign-on mode to launch the application from Microsoft 365 or the Azure AD My Apps. * `redirect_uris` - A list of URLs where user tokens are sent for sign-in with the associated application, or the redirect URIs where OAuth 2.0 authorization codes and access tokens are sent for the associated application. * `saml_metadata_url` - The URL where the service exposes SAML metadata for federation. +* `saml_single_sign_on` - A `saml_single_sign_on` block as documented below. * `service_principal_names` - A list of identifier URI(s), copied over from the associated application. * `sign_in_audience` - The Microsoft account types that are supported for the associated application. Possible values include `AzureADMyOrg`, `AzureADMultipleOrgs`, `AzureADandPersonalMicrosoftAccount` or `PersonalMicrosoftAccount`. * `tags` - A list of tags applied to the service principal. @@ -102,3 +103,9 @@ The following attributes are exported: * `user_consent_description` - Delegated permission description that appears in the end user consent experience, intended to be read by a user consenting on their own behalf. * `user_consent_display_name` - Display name for the delegated permission that appears in the end user consent experience. * `value` - The value that is used for the `scp` claim in OAuth 2.0 access tokens. + +--- + +`saml_single_sign_on` exports the following: + +* `relay_state` - The relative URI the service provider would redirect to after completion of the single sign-on flow. diff --git a/docs/resources/service_principal.md b/docs/resources/service_principal.md index 0659ddd3ad..de3e90ad7d 100644 --- a/docs/resources/service_principal.md +++ b/docs/resources/service_principal.md @@ -83,11 +83,18 @@ The following arguments are supported: -> **Ownership of Service Principals** It's recommended to always specify one or more service principal owners, including the principal being used to execute Terraform, such as in the example above. * `preferred_single_sign_on_mode` - (Optional) The single sign-on mode configured for this application. Azure AD uses the preferred single sign-on mode to launch the application from Microsoft 365 or the Azure AD My Apps. Supported values are `oidc`, `password`, `saml` or `notSupported`. Omit this property or specify a blank string to unset. +* `saml_single_sign_on` - (Optional) A `saml_single_sign_on` block as documented below. * `tags` - (Optional) A set of tags to apply to the service principal. * `use_existing` - (Optional) When true, any existing service principal linked to the same application will be automatically imported. When false, an import error will be raised for any pre-existing service principal. -> **Caveats of `use_existing`** Enabling this behaviour is useful for managing existing service principals that may already be installed in your tenant for Microsoft-published APIs, as it allows you to make changes where permitted, and then also reference them in your Terraform configuration. However, the behaviour of delete operations is also affected - when `use_existing` is `true`, Terraform will still attempt to delete the service principal on destroy, although it will not raise an error if the deletion fails (as it often the case for first-party Microsoft applications). +--- + +`saml_single_sign_on` supports the following: + +* `relay_state` - (Optional) The relative URI the service provider would redirect to after completion of the single sign-on flow. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: diff --git a/internal/services/serviceprincipals/service_principal_data_source.go b/internal/services/serviceprincipals/service_principal_data_source.go index c9a12c60cd..ea6bec4abb 100644 --- a/internal/services/serviceprincipals/service_principal_data_source.go +++ b/internal/services/serviceprincipals/service_principal_data_source.go @@ -164,6 +164,21 @@ func servicePrincipalData() *schema.Resource { Computed: true, }, + "saml_single_sign_on": { + Description: "Settings related to SAML single sign-on", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "relay_state": { + Description: "The relative URI the service provider would redirect to after completion of the single sign-on flow", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "service_principal_names": { Description: "A list of identifier URI(s), copied over from the associated application", Type: schema.TypeList, @@ -309,6 +324,7 @@ func servicePrincipalDataSourceRead(ctx context.Context, d *schema.ResourceData, tf.Set(d, "preferred_single_sign_on_mode", servicePrincipal.PreferredSingleSignOnMode) tf.Set(d, "redirect_uris", tf.FlattenStringSlicePtr(servicePrincipal.ReplyUrls)) tf.Set(d, "saml_metadata_url", servicePrincipal.SamlMetadataUrl) + tf.Set(d, "saml_single_sign_on", flattenSamlSingleSignOn(servicePrincipal.SamlSingleSignOnSettings)) tf.Set(d, "service_principal_names", servicePrincipalNames) tf.Set(d, "sign_in_audience", servicePrincipal.SignInAudience) tf.Set(d, "tags", servicePrincipal.Tags) diff --git a/internal/services/serviceprincipals/service_principal_data_source_test.go b/internal/services/serviceprincipals/service_principal_data_source_test.go index 0c7b2b7a12..cbd0cc06ae 100644 --- a/internal/services/serviceprincipals/service_principal_data_source_test.go +++ b/internal/services/serviceprincipals/service_principal_data_source_test.go @@ -70,6 +70,8 @@ func (ServicePrincipalDataSource) testCheckFunc(data acceptance.TestData) resour check.That(data.ResourceName).Key("oauth2_permission_scopes.#").HasValue("2"), check.That(data.ResourceName).Key("object_id").IsUuid(), check.That(data.ResourceName).Key("redirect_uris.#").HasValue("2"), + check.That(data.ResourceName).Key("saml_single_sign_on.#").HasValue("1"), + check.That(data.ResourceName).Key("saml_single_sign_on.0.relay_state").HasValue("/samlHome"), check.That(data.ResourceName).Key("service_principal_names.#").HasValue("2"), check.That(data.ResourceName).Key("sign_in_audience").HasValue("AzureADMyOrg"), check.That(data.ResourceName).Key("tags.#").HasValue("3"), diff --git a/internal/services/serviceprincipals/service_principal_resource.go b/internal/services/serviceprincipals/service_principal_resource.go index a8f62fa0a3..4c5419fefa 100644 --- a/internal/services/serviceprincipals/service_principal_resource.go +++ b/internal/services/serviceprincipals/service_principal_resource.go @@ -216,6 +216,24 @@ func servicePrincipalResource() *schema.Resource { Computed: true, }, + "saml_single_sign_on": { + Description: "Settings related to SAML single sign-on", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + DiffSuppressFunc: servicePrincipalDiffSuppress, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "relay_state": { + Description: "The relative URI the service provider would redirect to after completion of the single sign-on flow", + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + }, + }, + }, + "service_principal_names": { Description: "A list of identifier URI(s), copied over from the associated application", Type: schema.TypeList, @@ -240,6 +258,24 @@ func servicePrincipalResource() *schema.Resource { } } +func servicePrincipalDiffSuppress(k, old, new string, d *schema.ResourceData) bool { + suppress := false + + switch { + case k == "saml_single_sign_on.#" && old == "1" && new == "0": + samlSingleSignOnRaw := d.Get("saml_single_sign_on").([]interface{}) + if len(samlSingleSignOnRaw) == 1 { + suppress = true + samlSingleSignOn := samlSingleSignOnRaw[0].(map[string]interface{}) + if v, ok := samlSingleSignOn["relay_state"]; ok && v.(string) != "" { + suppress = false + } + } + } + + return suppress +} + func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient directoryObjectsClient := meta.(*clients.Client).ServicePrincipals.DirectoryObjectsClient @@ -282,6 +318,7 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, Notes: utils.NullableString(d.Get("notes").(string)), NotificationEmailAddresses: tf.ExpandStringSlicePtr(d.Get("notification_email_addresses").(*schema.Set).List()), PreferredSingleSignOnMode: utils.NullableString(d.Get("preferred_single_sign_on_mode").(string)), + SamlSingleSignOnSettings: expandSamlSingleSignOn(d.Get("saml_single_sign_on").([]interface{})), Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()), } @@ -374,6 +411,7 @@ func servicePrincipalResourceUpdate(ctx context.Context, d *schema.ResourceData, Notes: utils.NullableString(d.Get("notes").(string)), NotificationEmailAddresses: tf.ExpandStringSlicePtr(d.Get("notification_email_addresses").(*schema.Set).List()), PreferredSingleSignOnMode: utils.NullableString(d.Get("preferred_single_sign_on_mode").(string)), + SamlSingleSignOnSettings: expandSamlSingleSignOn(d.Get("saml_single_sign_on").([]interface{})), Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()), } @@ -466,6 +504,7 @@ func servicePrincipalResourceRead(ctx context.Context, d *schema.ResourceData, m tf.Set(d, "preferred_single_sign_on_mode", servicePrincipal.PreferredSingleSignOnMode) tf.Set(d, "redirect_uris", tf.FlattenStringSlicePtr(servicePrincipal.ReplyUrls)) tf.Set(d, "saml_metadata_url", servicePrincipal.SamlMetadataUrl) + tf.Set(d, "saml_single_sign_on", flattenSamlSingleSignOn(servicePrincipal.SamlSingleSignOnSettings)) tf.Set(d, "service_principal_names", servicePrincipalNames) tf.Set(d, "sign_in_audience", servicePrincipal.SignInAudience) tf.Set(d, "tags", servicePrincipal.Tags) diff --git a/internal/services/serviceprincipals/service_principal_resource_test.go b/internal/services/serviceprincipals/service_principal_resource_test.go index 9e2f4b940e..0d9c0b4293 100644 --- a/internal/services/serviceprincipals/service_principal_resource_test.go +++ b/internal/services/serviceprincipals/service_principal_resource_test.go @@ -333,6 +333,10 @@ resource "azuread_service_principal" "test" { "cto@hashitown.net", ] + saml_single_sign_on { + relay_state = "/samlHome" + } + alternative_names = ["foo", "bar"] tags = ["test", "multiple", "CapitalS"] } diff --git a/internal/services/serviceprincipals/serviceprincipals.go b/internal/services/serviceprincipals/serviceprincipals.go new file mode 100644 index 0000000000..7d6fbf24ae --- /dev/null +++ b/internal/services/serviceprincipals/serviceprincipals.go @@ -0,0 +1,35 @@ +package serviceprincipals + +import ( + "github.com/manicminer/hamilton/msgraph" + + "github.com/hashicorp/terraform-provider-azuread/internal/utils" +) + +func expandSamlSingleSignOn(in []interface{}) *msgraph.SamlSingleSignOnSettings { + result := msgraph.SamlSingleSignOnSettings{} + if len(in) == 0 || in[0] == nil { + return &result + } + + samlSingleSignOnSettings := in[0].(map[string]interface{}) + + result.RelayState = utils.String(samlSingleSignOnSettings["relay_state"].(string)) + + return &result +} + +func flattenSamlSingleSignOn(in *msgraph.SamlSingleSignOnSettings) []map[string]interface{} { + if in == nil { + return []map[string]interface{}{} + } + + relayState := "" + if in.RelayState != nil { + relayState = *in.RelayState + } + + return []map[string]interface{}{{ + "relay_state": relayState, + }} +}