diff --git a/docs/data-sources/application.md b/docs/data-sources/application.md index 448bb35ea9..cdf16f9682 100644 --- a/docs/data-sources/application.md +++ b/docs/data-sources/application.md @@ -48,6 +48,7 @@ The following attributes are exported: * `disabled_by_microsoft` - Whether Microsoft has disabled the registered application. If the application is disabled, this will be a string indicating the status/reason, e.g. `DisabledDueToViolationOfServicesAgreement` * `display_name` - The display name for the application. * `fallback_public_client_enabled` - The fallback application type as public client, such as an installed application running on a mobile device. +* `feature_tags` - A `features` block as described below. * `group_membership_claims` - The `groups` claim issued in a user or OAuth 2.0 access token that the app expects. * `identifier_uris` - A list of user-defined URI(s) that uniquely identify a Web application within it's Azure AD tenant, or within a verified custom domain if the application is multi-tenant. * `logo_url` - CDN URL to the application's logo. @@ -64,6 +65,7 @@ The following attributes are exported: * `sign_in_audience` - The Microsoft account types that are supported for the current application. One of `AzureADMyOrg`, `AzureADMultipleOrgs`, `AzureADandPersonalMicrosoftAccount` or `PersonalMicrosoftAccount`. * `single_page_application` - A `single_page_application` block as documented below. * `support_url` - URL of the application's support page. +* `tags` - A list of tags applied to the application. * `terms_of_service_url` - URL of the application's terms of service statement. * `web` - A `web` block as documented below. @@ -102,6 +104,15 @@ The following attributes are exported: --- +`features` block exports the following: + +* `custom_single_sign_on` - Whether this application represents a custom SAML application for linked service principals. +* `enterprise` - Whether this application represents an Enterprise Application for linked service principals. +* `gallery` - Whether this application represents a gallery application for linked service principals. +* `hide` - Whether this app is visible to users in My Apps and Office 365 Launcher. + +--- + `optional_claims` block exports the following: * `access_token` - One or more `access_token` blocks as documented below. diff --git a/docs/resources/application.md b/docs/resources/application.md index 6331990401..5b0fabbf8a 100644 --- a/docs/resources/application.md +++ b/docs/resources/application.md @@ -78,6 +78,11 @@ resource "azuread_application" "example" { value = "User" } + feature_tags { + enterprise = true + gallery = true + } + optional_claims { access_token { name = "myclaim" @@ -162,6 +167,10 @@ The following arguments are supported: * `device_only_auth_enabled` - (Optional) Specifies whether this application supports device authentication without a user. Defaults to `false`. * `display_name` - (Required) The display name for the application. * `fallback_public_client_enabled` - (Optional) Specifies whether the application is a public client. Appropriate for apps using token grant flows that don't use a redirect URI. Defaults to `false`. +* `feature_tags` - (Optional) A `feature_tags` block as described below. Cannot be used together with the `tags` property. + +-> **Features and Tags** Features are configured for an application using tags, and are provided as a shortcut to set the corresponding magic tag value for each feature. You cannot configure `feature_tags` and `tags` for an application at the same time, so if you need to assign additional custom tags it's recommended to use the `tags` property instead. Tag values also propagate to any linked service principals. + * `group_membership_claims` - (Optional) Configures the `groups` claim issued in a user or OAuth 2.0 access token that the app expects. Possible values are `None`, `SecurityGroup`, `DirectoryRole`, `ApplicationGroup` or `All`. * `identifier_uris` - (Optional) A set of user-defined URI(s) that uniquely identify an application within its Azure AD tenant, or within a verified custom domain if the application is multi-tenant. * `logo_image` - (Optional) A logo image to upload for the application, as a raw base64-encoded string. The image should be in gif, jpeg or png format. Note that once an image has been uploaded, it is not possible to remove it without replacing it with another image. @@ -182,6 +191,10 @@ The following arguments are supported: * `single_page_application` - (Optional) A `single_page_application` block as documented below, which configures single-page application (SPA) related settings for this application. * `support_url` - (Optional) URL of the application's support page. +* `tags` - (Optional) A set of tags to apply to the application. Cannot be used together with the `feature_tags` block. + +-> **Tags and Features** Azure Active Directory uses special tag values to configure the behavior of applications. These can be specified using either the `tags` property or with the `feature_tags` block. If you need to set any custom tag values not supported by the `feature_tags` block, it's recommended to use the `tags` property. Tag values also propagate to any linked service principals. + * `template_id` - (Optional) Unique ID for a templated application in the Azure AD App Gallery, from which to create the application. Changing this forces a new resource to be created. * `terms_of_service_url` - (Optional) URL of the application's terms of service statement. * `web` - (Optional) A `web` block as documented below, which configures web related settings for this application. @@ -235,6 +248,15 @@ The following arguments are supported: --- +`feature_tags` block supports the following: + +* `custom_single_sign_on` - (Optional) Whether this application represents a custom SAML application for linked service principals. Enabling this will assign the `WindowsAzureActiveDirectoryCustomSingleSignOnApplication` tag. Defaults to `false`. +* `enterprise` - (Optional) Whether this application represents an Enterprise Application for linked service principals. Enabling this will assign the `WindowsAzureActiveDirectoryIntegratedApp` tag. Defaults to `false`. +* `gallery` - (Optional) Whether this application represents a gallery application for linked service principals. Enabling this will assign the `WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1` tag. Defaults to `false`. +* `hide` - (Optional) Whether this app is invisible to users in My Apps and Office 365 Launcher. Enabling this will assign the `HideApp` tag. Defaults to `false`. + +--- + `optional_claims` block supports the following: * `access_token` - (Optional) One or more `access_token` blocks as documented below. diff --git a/docs/resources/service_principal.md b/docs/resources/service_principal.md index 2b8259d1d1..c195c4365e 100644 --- a/docs/resources/service_principal.md +++ b/docs/resources/service_principal.md @@ -50,9 +50,9 @@ resource "azuread_service_principal" "example" { app_role_assignment_required = false owners = [data.azuread_client_config.current.object_id] - features { - enterprise_application = true - gallery_application = true + feature_tags { + enterprise = true + gallery = true } } ``` @@ -95,7 +95,10 @@ The following arguments are supported: * `app_role_assignment_required` - (Optional) Whether this service principal requires an app role assignment to a user or group before Azure AD will issue a user or access token to the application. Defaults to `false`. * `application_id` - (Required) The application ID (client ID) of the application for which to create a service principal. * `description` - (Optional) A description of the service principal provided for internal end-users. -* `features` - (Optional) A `features` block as described below. Cannot be used together with the `tags` property. +* `feature_tags` - (Optional) A `feature_tags` block as described below. Cannot be used together with the `tags` property. + +-> **Features and Tags** Features are configured for a service principal using tags, and are provided as a shortcut to set the corresponding magic tag value for each feature. You cannot configure `feature_tags` and `tags` for a service principal at the same time, so if you need to assign additional custom tags it's recommended to use the `tags` property instead. Any tags configured for the linked application will propagate to this service principal. + * `login_url` - (Optional) The URL where the service provider redirects the user to Azure AD to authenticate. Azure AD uses the URL to launch the application from Microsoft 365 or the Azure AD My Apps. When blank, Azure AD performs IdP-initiated sign-on for applications configured with SAML-based single sign-on. * `notes` - (Optional) A free text field to capture information about the service principal, typically used for operational purposes. * `notification_email_addresses` - (Optional) A set of email addresses where Azure AD sends a notification when the active certificate is near the expiration date. This is only for the certificates used to sign the SAML token issued for Azure AD Gallery applications. @@ -105,19 +108,22 @@ The following arguments are supported: * `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. Cannot be used together with the `features` block. +* `tags` - (Optional) A set of tags to apply to the service principal. Cannot be used together with the `feature_tags` block. + +-> **Tags and Features** Azure Active Directory uses special tag values to configure the behavior of service principals. These can be specified using either the `tags` property or with the `feature_tags` block. If you need to set any custom tag values not supported by the `feature_tags` block, it's recommended to use the `tags` property. Tag values set for the linked application will also propagate to this 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). --- -`features` block supports the following: +`feature_tags` block supports the following: -* `custom_single_sign_on_app` - (Optional) Whether this service principal represents a custom SAML application. Defaults to `false`. -* `enterprise_application` - (Optional) Whether this service principal represents an Enterprise Application. Defaults to `false`. -* `gallery_application` - (Optional) Whether this service principal represents a gallery application. Defaults to `false`. -* `visible_to_users` - (Optional) Whether this app is visible to users in My Apps and Office 365 Launcher. Defaults to `true`. +* `custom_single_sign_on` - (Optional) Whether this service principal represents a custom SAML application. Enabling this will assign the `WindowsAzureActiveDirectoryCustomSingleSignOnApplication` tag. Defaults to `false`. +* `enterprise` - (Optional) Whether this service principal represents an Enterprise Application. Enabling this will assign the `WindowsAzureActiveDirectoryIntegratedApp` tag. Defaults to `false`. +* `gallery` - (Optional) Whether this service principal represents a gallery application. Enabling this will assign the `WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1` tag. Defaults to `false`. +* `hide` - (Optional) Whether this app is invisible to users in My Apps and Office 365 Launcher. Enabling this will assign the `HideApp` tag. Defaults to `false`. --- diff --git a/internal/helpers/applications.go b/internal/helpers/applications.go index dd054bd48e..3408e03866 100644 --- a/internal/helpers/applications.go +++ b/internal/helpers/applications.go @@ -1,9 +1,47 @@ package helpers import ( + "strings" + "github.com/manicminer/hamilton/msgraph" ) +func ApplicationExpandFeatures(in []interface{}) []string { + out := make([]string, 0) + + if len(in) == 0 || in[0] == nil { + return out + } + + features := in[0].(map[string]interface{}) + + if v, ok := features["custom_single_sign_on"]; ok && v.(bool) { + out = append(out, "WindowsAzureActiveDirectoryCustomSingleSignOnApplication") + } else if v, ok := features["custom_single_sign_on_app"]; ok && v.(bool) { + out = append(out, "WindowsAzureActiveDirectoryCustomSingleSignOnApplication") + } + + if v, ok := features["enterprise"]; ok && v.(bool) { + out = append(out, "WindowsAzureActiveDirectoryIntegratedApp") + } else if v, ok := features["enterprise_application"]; ok && v.(bool) { // TODO: remove in v3.0 + out = append(out, "WindowsAzureActiveDirectoryIntegratedApp") + } + + if v, ok := features["gallery"]; ok && v.(bool) { + out = append(out, "WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1") + } else if v, ok := features["gallery_application"]; ok && v.(bool) { // TODO: remove in v3.0 + out = append(out, "WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1") + } + + if v, ok := features["hide"]; ok && v.(bool) { + out = append(out, "HideApp") + } else if v, ok := features["visible_to_users"]; ok && !v.(bool) { // TODO: remove in v3.0 + out = append(out, "HideApp") + } + + return out +} + func ApplicationFlattenAppRoleIDs(in *[]msgraph.AppRole) map[string]string { result := make(map[string]string) if in != nil { @@ -61,6 +99,67 @@ func ApplicationFlattenAppRoles(in *[]msgraph.AppRole) (result []map[string]inte return //nolint:nakedret } +func ApplicationFlattenFeatures(tags *[]string, deprecated bool) []interface{} { + // TODO: remove this in v3.0 + if deprecated { + result := map[string]bool{ + "custom_single_sign_on_app": false, + "enterprise_application": false, + "gallery_application": false, + "visible_to_users": true, + } + + if tags == nil || len(*tags) == 0 { + return []interface{}{result} + } + + for _, tag := range *tags { + if strings.EqualFold(tag, "WindowsAzureActiveDirectoryCustomSingleSignOnApplication") { + result["custom_single_sign_on_app"] = true + } + if strings.EqualFold(tag, "WindowsAzureActiveDirectoryIntegratedApp") { + result["enterprise_application"] = true + } + if strings.EqualFold(tag, "WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1") { + result["gallery_application"] = true + } + if strings.EqualFold(tag, "HideApp") { + result["visible_to_users"] = false + } + } + + return []interface{}{result} + } + + result := map[string]bool{ + "custom_single_sign_on": false, + "enterprise": false, + "gallery": false, + "hide": false, + } + + if tags == nil || len(*tags) == 0 { + return []interface{}{result} + } + + for _, tag := range *tags { + if strings.EqualFold(tag, "WindowsAzureActiveDirectoryCustomSingleSignOnApplication") { + result["custom_single_sign_on"] = true + } + if strings.EqualFold(tag, "WindowsAzureActiveDirectoryIntegratedApp") { + result["enterprise"] = true + } + if strings.EqualFold(tag, "WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1") { + result["gallery"] = true + } + if strings.EqualFold(tag, "HideApp") { + result["hide"] = true + } + } + + return []interface{}{result} +} + func ApplicationFlattenOAuth2PermissionScopeIDs(in *[]msgraph.PermissionScope) map[string]string { result := make(map[string]string) if in != nil { diff --git a/internal/services/applications/application_data_source.go b/internal/services/applications/application_data_source.go index 4dae8ea964..2279e34dde 100644 --- a/internal/services/applications/application_data_source.go +++ b/internal/services/applications/application_data_source.go @@ -13,6 +13,7 @@ import ( "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/tf" "github.com/hashicorp/terraform-provider-azuread/internal/validate" ) @@ -214,6 +215,40 @@ func applicationDataSource() *schema.Resource { Computed: true, }, + "feature_tags": { + Description: "Block of features configured for this application using tags", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_single_sign_on": { + Description: "Whether this application principal represents a custom SAML application for linked service principals", + Type: schema.TypeBool, + Optional: true, + }, + + "enterprise": { + Description: "Whether this application represents an Enterprise Application for linked service principals", + Type: schema.TypeBool, + Optional: true, + }, + + "gallery": { + Description: "Whether this application represents a gallery application for linked service principals", + Type: schema.TypeBool, + Optional: true, + }, + + "hide": { + Description: "Whether this app is invisible to users in My Apps and Office 365 Launcher", + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + }, + }, + "group_membership_claims": { Description: "The `groups` claim issued in a user or OAuth 2.0 access token that the app expects", Type: schema.TypeList, @@ -373,6 +408,15 @@ func applicationDataSource() *schema.Resource { Computed: true, }, + "tags": { + Description: "A set of tags applied to the application", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "terms_of_service_url": { Description: "URL of the application's terms of service statement", Type: schema.TypeString, @@ -511,6 +555,7 @@ func applicationDataSourceRead(ctx context.Context, d *schema.ResourceData, meta tf.Set(d, "disabled_by_microsoft", fmt.Sprintf("%v", app.DisabledByMicrosoftStatus)) tf.Set(d, "display_name", app.DisplayName) tf.Set(d, "fallback_public_client_enabled", app.IsFallbackPublicClient) + tf.Set(d, "feature_tags", helpers.ApplicationFlattenFeatures(app.Tags, false)) tf.Set(d, "group_membership_claims", tf.FlattenStringSlicePtr(app.GroupMembershipClaims)) tf.Set(d, "identifier_uris", tf.FlattenStringSlicePtr(app.IdentifierUris)) tf.Set(d, "oauth2_post_response_required", app.Oauth2RequirePostResponse) @@ -521,6 +566,7 @@ func applicationDataSourceRead(ctx context.Context, d *schema.ResourceData, meta tf.Set(d, "required_resource_access", flattenApplicationRequiredResourceAccess(app.RequiredResourceAccess)) tf.Set(d, "sign_in_audience", app.SignInAudience) tf.Set(d, "single_page_application", flattenApplicationSpa(app.Spa)) + tf.Set(d, "tags", app.Tags) tf.Set(d, "web", flattenApplicationWeb(app.Web)) if app.Api != nil { diff --git a/internal/services/applications/application_data_source_test.go b/internal/services/applications/application_data_source_test.go index 5d88ee4466..579428dbb4 100644 --- a/internal/services/applications/application_data_source_test.go +++ b/internal/services/applications/application_data_source_test.go @@ -56,6 +56,11 @@ func (ApplicationDataSource) testCheck(data acceptance.TestData) resource.TestCh check.That(data.ResourceName).Key("app_roles.#").HasValue("2"), check.That(data.ResourceName).Key("app_role_ids.%").HasValue("2"), check.That(data.ResourceName).Key("display_name").HasValue(fmt.Sprintf("acctest-APP-complete-%d", data.RandomInteger)), + check.That(data.ResourceName).Key("feature_tags.#").HasValue("1"), + check.That(data.ResourceName).Key("feature_tags.0.custom_single_sign_on").HasValue("true"), + check.That(data.ResourceName).Key("feature_tags.0.enterprise").HasValue("true"), + check.That(data.ResourceName).Key("feature_tags.0.gallery").HasValue("true"), + check.That(data.ResourceName).Key("feature_tags.0.hide").HasValue("true"), check.That(data.ResourceName).Key("group_membership_claims.#").HasValue("1"), check.That(data.ResourceName).Key("group_membership_claims.0").HasValue("All"), check.That(data.ResourceName).Key("identifier_uris.#").HasValue("2"), @@ -65,6 +70,7 @@ func (ApplicationDataSource) testCheck(data acceptance.TestData) resource.TestCh check.That(data.ResourceName).Key("optional_claims.0.id_token.#").HasValue("1"), check.That(data.ResourceName).Key("required_resource_access.#").HasValue("2"), check.That(data.ResourceName).Key("sign_in_audience").HasValue("AzureADandPersonalMicrosoftAccount"), + check.That(data.ResourceName).Key("tags.#").HasValue("4"), check.That(data.ResourceName).Key("web.0.homepage_url").HasValue(fmt.Sprintf("https://app.hashitown-%d.com/", data.RandomInteger)), check.That(data.ResourceName).Key("web.0.logout_url").HasValue(fmt.Sprintf("https://app.hashitown-%[1]d.com/logout", data.RandomInteger)), check.That(data.ResourceName).Key("web.0.redirect_uris.#").HasValue("3"), diff --git a/internal/services/applications/application_resource.go b/internal/services/applications/application_resource.go index fbb6732702..2967076950 100644 --- a/internal/services/applications/application_resource.go +++ b/internal/services/applications/application_resource.go @@ -19,6 +19,7 @@ import ( "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/applications/migrations" applicationsValidate "github.com/hashicorp/terraform-provider-azuread/internal/services/applications/validate" "github.com/hashicorp/terraform-provider-azuread/internal/tf" @@ -269,6 +270,41 @@ func applicationResource() *schema.Resource { Optional: true, }, + "feature_tags": { + Description: "Block of features to configure for this application using tags", + Type: schema.TypeList, + Optional: true, + Computed: true, + ConflictsWith: []string{"tags"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_single_sign_on": { + Description: "Whether this application represents a custom SAML application for linked service principals", + Type: schema.TypeBool, + Optional: true, + }, + + "enterprise": { + Description: "Whether this application represents an Enterprise Application for linked service principals", + Type: schema.TypeBool, + Optional: true, + }, + + "gallery": { + Description: "Whether this application represents a gallery application for linked service principals", + Type: schema.TypeBool, + Optional: true, + }, + + "hide": { + Description: "Whether this application is invisible to users in My Apps and Office 365 Launcher", + Type: schema.TypeBool, + Optional: true, + }, + }, + }, + }, + "group_membership_claims": { Description: "Configures the `groups` claim issued in a user or OAuth 2.0 access token that the app expects", Type: schema.TypeSet, @@ -460,6 +496,18 @@ func applicationResource() *schema.Resource { Optional: true, }, + "tags": { + Description: "A set of tags to apply to the application", + Type: schema.TypeSet, + Optional: true, + Computed: true, + Set: schema.HashString, + ConflictsWith: []string{"feature_tags"}, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "template_id": { Description: "Unique ID of the application template from which this application is created", Type: schema.TypeString, @@ -860,6 +908,13 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta } } + var tags []string + if v, ok := d.GetOk("feature_tags"); ok { + tags = helpers.ApplicationExpandFeatures(v.([]interface{})) + } else { + tags = tf.ExpandStringSlice(d.Get("tags").(*schema.Set).List()) + } + if templateId != "" { // Instantiate application from template gallery and return via the update function properties := msgraph.ApplicationTemplate{ @@ -906,6 +961,7 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta RequiredResourceAccess: expandApplicationRequiredResourceAccess(d.Get("required_resource_access").(*schema.Set).List()), SignInAudience: utils.String(d.Get("sign_in_audience").(string)), Spa: expandApplicationSpa(d.Get("single_page_application").([]interface{})), + Tags: &tags, Web: expandApplicationWeb(d.Get("web").([]interface{})), } @@ -1037,6 +1093,13 @@ func applicationResourceUpdate(ctx context.Context, d *schema.ResourceData, meta } } + var tags []string + if v, ok := d.GetOk("feature_tags"); ok && len(v.([]interface{})) > 0 && d.HasChange("feature_tags") { + tags = helpers.ApplicationExpandFeatures(v.([]interface{})) + } else { + tags = tf.ExpandStringSlice(d.Get("tags").(*schema.Set).List()) + } + properties := msgraph.Application{ DirectoryObject: msgraph.DirectoryObject{ ID: utils.String(applicationId), @@ -1060,6 +1123,7 @@ func applicationResourceUpdate(ctx context.Context, d *schema.ResourceData, meta RequiredResourceAccess: expandApplicationRequiredResourceAccess(d.Get("required_resource_access").(*schema.Set).List()), SignInAudience: utils.String(d.Get("sign_in_audience").(string)), Spa: expandApplicationSpa(d.Get("single_page_application").([]interface{})), + Tags: &tags, Web: expandApplicationWeb(d.Get("web").([]interface{})), } @@ -1152,6 +1216,7 @@ func applicationResourceRead(ctx context.Context, d *schema.ResourceData, meta i tf.Set(d, "disabled_by_microsoft", fmt.Sprintf("%v", app.DisabledByMicrosoftStatus)) tf.Set(d, "display_name", app.DisplayName) tf.Set(d, "fallback_public_client_enabled", app.IsFallbackPublicClient) + tf.Set(d, "feature_tags", helpers.ApplicationFlattenFeatures(app.Tags, false)) tf.Set(d, "group_membership_claims", tf.FlattenStringSlicePtr(app.GroupMembershipClaims)) tf.Set(d, "identifier_uris", tf.FlattenStringSlicePtr(app.IdentifierUris)) tf.Set(d, "oauth2_post_response_required", app.Oauth2RequirePostResponse) @@ -1162,6 +1227,7 @@ func applicationResourceRead(ctx context.Context, d *schema.ResourceData, meta i tf.Set(d, "required_resource_access", flattenApplicationRequiredResourceAccess(app.RequiredResourceAccess)) tf.Set(d, "sign_in_audience", app.SignInAudience) tf.Set(d, "single_page_application", flattenApplicationSpa(app.Spa)) + tf.Set(d, "tags", app.Tags) tf.Set(d, "template_id", app.ApplicationTemplateId) tf.Set(d, "web", flattenApplicationWeb(app.Web)) diff --git a/internal/services/applications/application_resource_test.go b/internal/services/applications/application_resource_test.go index 842df7ae53..d3d1d6dccd 100644 --- a/internal/services/applications/application_resource_test.go +++ b/internal/services/applications/application_resource_test.go @@ -441,6 +441,92 @@ func TestAccApplication_related(t *testing.T) { }) } +func TestAccApplication_featureTags(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application", "test") + r := ApplicationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.featureTags(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplication_featureTagsUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application", "test") + r := ApplicationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.noFeatureTags(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.featureTags(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.featureTags(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.tags(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.featureTags(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.noFeatureTags(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.featureTags(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccApplication_logo(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_application", "test") r := ApplicationResource{} @@ -668,6 +754,13 @@ resource "azuread_application" "test" { ] } + tags = [ + "HideApp", + "WindowsAzureActiveDirectoryCustomSingleSignOnApplication", + "WindowsAzureActiveDirectoryIntegratedApp", + "WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1", + ] + web { homepage_url = "https://app.hashitown-%[1]d.com/" logout_url = "https://app.hashitown-%[1]d.com/logout" @@ -824,6 +917,12 @@ resource "azuread_application" "test" { ] } + tags = [ + "WindowsAzureActiveDirectoryCustomSingleSignOnApplication", + "WindowsAzureActiveDirectoryIntegratedApp", + "WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1", + ] + web { homepage_url = "https://app.templatetown-%[1]d.com/" logout_url = "https://app.templatetown-%[1]d.com/logout" @@ -1302,6 +1401,56 @@ resource "azuread_application" "test" { `, data.RandomInteger) } +func (r ApplicationResource) featureTags(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application" "test" { + display_name = "acctest-APP-%[1]d" + + feature_tags { + custom_single_sign_on = true + enterprise = true + gallery = true + hide = true + } +} +`, data.RandomInteger) +} + +func (r ApplicationResource) noFeatureTags(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application" "test" { + display_name = "acctest-APP-%[1]d" + + feature_tags { + custom_single_sign_on = false + enterprise = false + gallery = false + hide = false + } +} +`, data.RandomInteger) +} + +func (r ApplicationResource) tags(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application" "test" { + display_name = "acctest-APP-%[1]d" + + tags = [ + "WindowsAzureActiveDirectoryCustomSingleSignOnApplication", + "WindowsAzureActiveDirectoryIntegratedApp", + "WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1", + ] +} +`, data.RandomInteger) +} + func (r ApplicationResource) logo(data acceptance.TestData) string { return fmt.Sprintf(` provider "azuread" {} diff --git a/internal/services/serviceprincipals/service_principal_data_source.go b/internal/services/serviceprincipals/service_principal_data_source.go index 508ee63ae3..e56caf8bc7 100644 --- a/internal/services/serviceprincipals/service_principal_data_source.go +++ b/internal/services/serviceprincipals/service_principal_data_source.go @@ -99,7 +99,41 @@ func servicePrincipalData() *schema.Resource { Computed: true, }, + "feature_tags": { + Description: "Block of features configured for this service principal using tags", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_single_sign_on": { + Description: "Whether this service principal represents a custom SAML application", + Type: schema.TypeBool, + Computed: true, + }, + + "enterprise": { + Description: "Whether this service principal represents an Enterprise Application", + Type: schema.TypeBool, + Computed: true, + }, + + "gallery": { + Description: "Whether this service principal represents a gallery application", + Type: schema.TypeBool, + Computed: true, + }, + + "hide": { + Description: "Whether this app is invisible to users in My Apps and Office 365 Launcher", + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "features": { + Deprecated: "This block has been renamed to `feature_tags` and will be removed in version 3.0 of the provider", Description: "Block of features configured for this service principal using tags", Type: schema.TypeList, Computed: true, @@ -346,7 +380,8 @@ func servicePrincipalDataSourceRead(ctx context.Context, d *schema.ResourceData, tf.Set(d, "application_tenant_id", servicePrincipal.AppOwnerOrganizationId) tf.Set(d, "description", servicePrincipal.Description) tf.Set(d, "display_name", servicePrincipal.DisplayName) - tf.Set(d, "features", flattenFeatures(servicePrincipal.Tags)) + tf.Set(d, "feature_tags", helpers.ApplicationFlattenFeatures(servicePrincipal.Tags, false)) + tf.Set(d, "features", helpers.ApplicationFlattenFeatures(servicePrincipal.Tags, true)) tf.Set(d, "homepage_url", servicePrincipal.Homepage) tf.Set(d, "logout_url", servicePrincipal.LogoutUrl) tf.Set(d, "login_url", servicePrincipal.LoginUrl) diff --git a/internal/services/serviceprincipals/service_principal_data_source_test.go b/internal/services/serviceprincipals/service_principal_data_source_test.go index 99970ebf06..5b9b4e55f3 100644 --- a/internal/services/serviceprincipals/service_principal_data_source_test.go +++ b/internal/services/serviceprincipals/service_principal_data_source_test.go @@ -61,11 +61,11 @@ func (ServicePrincipalDataSource) testCheckFunc(data acceptance.TestData) resour check.That(data.ResourceName).Key("application_tenant_id").HasValue(tenantId), check.That(data.ResourceName).Key("description").HasValue("An internal app for testing"), check.That(data.ResourceName).Key("display_name").Exists(), - check.That(data.ResourceName).Key("features.#").HasValue("1"), - check.That(data.ResourceName).Key("features.0.custom_single_sign_on_app").HasValue("true"), - check.That(data.ResourceName).Key("features.0.enterprise_application").HasValue("true"), - check.That(data.ResourceName).Key("features.0.gallery_application").HasValue("true"), - check.That(data.ResourceName).Key("features.0.visible_to_users").HasValue("true"), + check.That(data.ResourceName).Key("feature_tags.#").HasValue("1"), + check.That(data.ResourceName).Key("feature_tags.0.custom_single_sign_on").HasValue("true"), + check.That(data.ResourceName).Key("feature_tags.0.enterprise").HasValue("true"), + check.That(data.ResourceName).Key("feature_tags.0.gallery").HasValue("true"), + check.That(data.ResourceName).Key("feature_tags.0.hide").HasValue("true"), check.That(data.ResourceName).Key("homepage_url").HasValue(fmt.Sprintf("https://test-%d.internal", data.RandomInteger)), check.That(data.ResourceName).Key("login_url").HasValue(fmt.Sprintf("https://test-%d.internal/login", data.RandomInteger)), check.That(data.ResourceName).Key("logout_url").HasValue(fmt.Sprintf("https://test-%d.internal/logout", data.RandomInteger)), @@ -79,7 +79,7 @@ func (ServicePrincipalDataSource) testCheckFunc(data acceptance.TestData) resour 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"), + check.That(data.ResourceName).Key("tags.#").HasValue("4"), check.That(data.ResourceName).Key("type").HasValue("Application"), ) } diff --git a/internal/services/serviceprincipals/service_principal_resource.go b/internal/services/serviceprincipals/service_principal_resource.go index ef7ae84b77..6eda3a3731 100644 --- a/internal/services/serviceprincipals/service_principal_resource.go +++ b/internal/services/serviceprincipals/service_principal_resource.go @@ -85,12 +85,48 @@ func servicePrincipalResource() *schema.Resource { ValidateFunc: validation.StringLenBetween(0, 1024), }, + "feature_tags": { + Description: "Block of features to configure for this service principal using tags", + Type: schema.TypeList, + Optional: true, + Computed: true, + ConflictsWith: []string{"features", "tags"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_single_sign_on": { + Description: "Whether this service principal represents a custom SAML application", + Type: schema.TypeBool, + Optional: true, + }, + + "enterprise": { + Description: "Whether this service principal represents an Enterprise Application", + Type: schema.TypeBool, + Optional: true, + }, + + "gallery": { + Description: "Whether this service principal represents a gallery application", + Type: schema.TypeBool, + Optional: true, + }, + + "hide": { + Description: "Whether this app is invisible to users in My Apps and Office 365 Launcher", + Type: schema.TypeBool, + Optional: true, + }, + }, + }, + }, + "features": { + Deprecated: "This block has been renamed to `feature_tags` and will be removed in version 3.0 of the provider", Description: "Block of features to configure for this service principal using tags", Type: schema.TypeList, Optional: true, Computed: true, - ConflictsWith: []string{"tags"}, + ConflictsWith: []string{"feature_tags", "tags"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "custom_single_sign_on_app": { @@ -175,7 +211,7 @@ func servicePrincipalResource() *schema.Resource { Optional: true, Computed: true, Set: schema.HashString, - ConflictsWith: []string{"features"}, + ConflictsWith: []string{"features", "feature_tags"}, Elem: &schema.Schema{ Type: schema.TypeString, }, @@ -347,8 +383,10 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, } var tags []string - if v, ok := d.GetOk("features"); ok { - tags = expandFeatures(v.([]interface{})) + if v, ok := d.GetOk("feature_tags"); ok { + tags = helpers.ApplicationExpandFeatures(v.([]interface{})) + } else if v, ok := d.GetOk("features"); ok { + tags = helpers.ApplicationExpandFeatures(v.([]interface{})) } else { tags = tf.ExpandStringSlice(d.Get("tags").(*schema.Set).List()) } @@ -456,9 +494,10 @@ func servicePrincipalResourceUpdate(ctx context.Context, d *schema.ResourceData, directoryObjectsClient := meta.(*clients.Client).ServicePrincipals.DirectoryObjectsClient var tags []string - featuresChanged := d.HasChange("features") - if v, ok := d.GetOk("features"); ok && len(v.([]interface{})) > 0 && featuresChanged { - tags = expandFeatures(v.([]interface{})) + if v, ok := d.GetOk("feature_tags"); ok && len(v.([]interface{})) > 0 && d.HasChange("feature_tags") { + tags = helpers.ApplicationExpandFeatures(v.([]interface{})) + } else if v, ok := d.GetOk("features"); ok && len(v.([]interface{})) > 0 && d.HasChange("features") { + tags = helpers.ApplicationExpandFeatures(v.([]interface{})) } else { tags = tf.ExpandStringSlice(d.Get("tags").(*schema.Set).List()) } @@ -563,7 +602,8 @@ func servicePrincipalResourceRead(ctx context.Context, d *schema.ResourceData, m tf.Set(d, "application_tenant_id", servicePrincipal.AppOwnerOrganizationId) tf.Set(d, "description", servicePrincipal.Description) tf.Set(d, "display_name", servicePrincipal.DisplayName) - tf.Set(d, "features", flattenFeatures(servicePrincipal.Tags)) + tf.Set(d, "feature_tags", helpers.ApplicationFlattenFeatures(servicePrincipal.Tags, false)) + tf.Set(d, "features", helpers.ApplicationFlattenFeatures(servicePrincipal.Tags, true)) tf.Set(d, "homepage_url", servicePrincipal.Homepage) tf.Set(d, "logout_url", servicePrincipal.LogoutUrl) tf.Set(d, "login_url", servicePrincipal.LoginUrl) diff --git a/internal/services/serviceprincipals/service_principal_resource_test.go b/internal/services/serviceprincipals/service_principal_resource_test.go index bddb7e16e5..f310d919f0 100644 --- a/internal/services/serviceprincipals/service_principal_resource_test.go +++ b/internal/services/serviceprincipals/service_principal_resource_test.go @@ -104,40 +104,28 @@ func TestAccServicePrincipal_completeUpdate(t *testing.T) { }) } -func TestAccServicePrincipal_features(t *testing.T) { +func TestAccServicePrincipal_featureTags(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_service_principal", "test") r := ServicePrincipalResource{} - tenantId := os.Getenv("ARM_TENANT_ID") data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.features(data), + Config: r.featureTags(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("app_role_ids.%").HasValue("2"), - check.That(data.ResourceName).Key("app_roles.#").HasValue("2"), - check.That(data.ResourceName).Key("application_tenant_id").HasValue(tenantId), - check.That(data.ResourceName).Key("homepage_url").HasValue(fmt.Sprintf("https://test-%d.internal", data.RandomInteger)), - check.That(data.ResourceName).Key("logout_url").HasValue(fmt.Sprintf("https://test-%d.internal/logout", data.RandomInteger)), - check.That(data.ResourceName).Key("oauth2_permission_scope_ids.%").HasValue("2"), - check.That(data.ResourceName).Key("oauth2_permission_scopes.#").HasValue("2"), - check.That(data.ResourceName).Key("service_principal_names.#").HasValue("2"), - check.That(data.ResourceName).Key("redirect_uris.#").HasValue("2"), - check.That(data.ResourceName).Key("sign_in_audience").HasValue("AzureADMyOrg"), - check.That(data.ResourceName).Key("type").HasValue("Application"), ), }, data.ImportStep("use_existing"), }) } -func TestAccServicePrincipal_featuresUpdate(t *testing.T) { +func TestAccServicePrincipal_featureTagsUpdate(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_service_principal", "test") r := ServicePrincipalResource{} data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.noFeatures(data), + Config: r.noFeatureTags(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -151,7 +139,7 @@ func TestAccServicePrincipal_featuresUpdate(t *testing.T) { }, data.ImportStep("use_existing"), { - Config: r.features(data), + Config: r.featureTags(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -165,7 +153,7 @@ func TestAccServicePrincipal_featuresUpdate(t *testing.T) { }, data.ImportStep("use_existing"), { - Config: r.features(data), + Config: r.featureTags(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -179,21 +167,21 @@ func TestAccServicePrincipal_featuresUpdate(t *testing.T) { }, data.ImportStep("use_existing"), { - Config: r.features(data), + Config: r.featureTags(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, data.ImportStep("use_existing"), { - Config: r.noFeatures(data), + Config: r.noFeatureTags(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, data.ImportStep("use_existing"), { - Config: r.features(data), + Config: r.featureTags(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -443,6 +431,7 @@ resource "azuread_service_principal" "test" { } tags = [ + "HideApp", "WindowsAzureActiveDirectoryCustomSingleSignOnApplication", "WindowsAzureActiveDirectoryIntegratedApp", "WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1", @@ -451,7 +440,7 @@ resource "azuread_service_principal" "test" { `, r.templateComplete(data), data.RandomInteger) } -func (r ServicePrincipalResource) features(data acceptance.TestData) string { +func (r ServicePrincipalResource) featureTags(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s @@ -466,11 +455,11 @@ resource "azuread_service_principal" "test" { notes = "Just testing something" preferred_single_sign_on_mode = "saml" - features { - custom_single_sign_on_app = true - enterprise_application = true - gallery_application = true - visible_to_users = false + feature_tags { + custom_single_sign_on = true + enterprise = true + gallery = true + hide = true } notification_email_addresses = [ @@ -485,7 +474,7 @@ resource "azuread_service_principal" "test" { `, r.templateComplete(data), data.RandomInteger) } -func (r ServicePrincipalResource) noFeatures(data acceptance.TestData) string { +func (r ServicePrincipalResource) noFeatureTags(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s @@ -500,10 +489,11 @@ resource "azuread_service_principal" "test" { notes = "Just testing something" preferred_single_sign_on_mode = "saml" - features { - custom_single_sign_on_app = false - enterprise_application = false - gallery_application = false + feature_tags { + custom_single_sign_on = false + enterprise = false + gallery = false + hide = false } notification_email_addresses = [ diff --git a/internal/services/serviceprincipals/serviceprincipals.go b/internal/services/serviceprincipals/serviceprincipals.go index 761142e4e5..7d6fbf24ae 100644 --- a/internal/services/serviceprincipals/serviceprincipals.go +++ b/internal/services/serviceprincipals/serviceprincipals.go @@ -1,40 +1,11 @@ package serviceprincipals import ( - "strings" + "github.com/manicminer/hamilton/msgraph" "github.com/hashicorp/terraform-provider-azuread/internal/utils" - "github.com/manicminer/hamilton/msgraph" ) -func expandFeatures(in []interface{}) []string { - out := make([]string, 0) - - if len(in) == 0 || in[0] == nil { - return out - } - - features := in[0].(map[string]interface{}) - - if v, ok := features["custom_single_sign_on_app"]; ok && v.(bool) { - out = append(out, "WindowsAzureActiveDirectoryCustomSingleSignOnApplication") - } - - if v, ok := features["enterprise_application"]; ok && v.(bool) { - out = append(out, "WindowsAzureActiveDirectoryIntegratedApp") - } - - if v, ok := features["gallery_application"]; ok && v.(bool) { - out = append(out, "WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1") - } - - if v, ok := features["visible_to_users"]; ok && !v.(bool) { - out = append(out, "HideApp") - } - - return out -} - func expandSamlSingleSignOn(in []interface{}) *msgraph.SamlSingleSignOnSettings { result := msgraph.SamlSingleSignOnSettings{} if len(in) == 0 || in[0] == nil { @@ -48,36 +19,6 @@ func expandSamlSingleSignOn(in []interface{}) *msgraph.SamlSingleSignOnSettings return &result } -func flattenFeatures(tags *[]string) []interface{} { - result := map[string]bool{ - "custom_single_sign_on_app": false, - "enterprise_application": false, - "gallery_application": false, - "visible_to_users": true, - } - - if tags == nil || len(*tags) == 0 { - return []interface{}{result} - } - - for _, tag := range *tags { - if strings.EqualFold(tag, "WindowsAzureActiveDirectoryCustomSingleSignOnApplication") { - result["custom_single_sign_on_app"] = true - } - if strings.EqualFold(tag, "WindowsAzureActiveDirectoryIntegratedApp") { - result["enterprise_application"] = true - } - if strings.EqualFold(tag, "WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1") { - result["gallery_application"] = true - } - if strings.EqualFold(tag, "HideApp") { - result["visible_to_users"] = false - } - } - - return []interface{}{result} -} - func flattenSamlSingleSignOn(in *msgraph.SamlSingleSignOnSettings) []map[string]interface{} { if in == nil { return []map[string]interface{}{}