diff --git a/docs/data-sources/application_published_app_ids.md b/docs/data-sources/application_published_app_ids.md new file mode 100644 index 0000000000..cfe30f992d --- /dev/null +++ b/docs/data-sources/application_published_app_ids.md @@ -0,0 +1,62 @@ +--- +subcategory: "Applications" +--- + +# Data Source: azuread_application_published_app_ids + +Use this data source to discover application IDs for APIs published by Microsoft. + +This data source uses an [unofficial source of application IDs](https://github.com/manicminer/hamilton/blob/main/environments/published.go), as there is currently no available official indexed source for applications or APIs published by Microsoft. + +The app IDs returned by this data source are sourced from the Azure Global (Public) Cloud, however some of them are known to work in government and national clouds. + +## Example Usage + +*Listing well-known application IDs* + +```terraform +data "azuread_application_published_app_ids" "well_known" {} + +output "published_app_ids" { + value = data.azuread_application_published_app_ids.well_known.result +} +``` + +*Granting access to an application* + +```terraform +data "azuread_application_published_app_ids" "well_known" {} + +resource "azuread_service_principal" "msgraph" { + application_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + use_existing = true +} + +resource "azuread_application" "example" { + display_name = "example" + + required_resource_access { + resource_app_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + + resource_access { + id = azuread_service_principal.msgraph.app_role_ids["User.Read.All"] + type = "Role" + } + + resource_access { + id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["User.ReadWrite"] + type = "Scope" + } + } +} +``` + +## Argument Reference + +This data source does not have any arguments. + +## Attributes Reference + +The following attributes are exported: + +* `result` - A map of application names to application IDs. diff --git a/docs/data-sources/service_principal.md b/docs/data-sources/service_principal.md index 1a2515e0fa..4afef5fefa 100644 --- a/docs/data-sources/service_principal.md +++ b/docs/data-sources/service_principal.md @@ -6,7 +6,9 @@ subcategory: "Service Principals" Gets information about an existing service principal associated with an application within Azure Active Directory. -## Example Usage (by Application Display Name) +## Example Usage + +**Look up by application display name** ```terraform data "azuread_service_principal" "example" { @@ -14,7 +16,7 @@ data "azuread_service_principal" "example" { } ``` -## Example Usage (by Application ID) +**Look up by application ID** ```terraform data "azuread_service_principal" "example" { @@ -22,7 +24,7 @@ data "azuread_service_principal" "example" { } ``` -## Example Usage (by Object ID) +**Look up by service principal object ID** ```terraform data "azuread_service_principal" "example" { @@ -44,9 +46,27 @@ The following arguments are supported: The following attributes are exported: -* `app_roles` - A collection of `app_roles` blocks as documented below. For more information [official documentation](https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles). +* `account_enabled` - - Whether or not the service principal account is enabled. +* `alternative_names` - A list of alternative names, used to retrieve service principals by subscription, identify resource group and full resource ids for managed identities. +* `app_role_assignment_required` - 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. +* `app_role_ids` - A mapping of app role values to app role IDs, as published by the associated application, intended to be useful when referencing app roles in other resources in your configuration. +* `app_roles` - A list of app roles published by the associated application, as documented below. For more information [official documentation](https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles). +* `application_tenant_id` - The tenant ID where the associated application is registered. +* `description` - A description of the service principal provided for internal end-users. +* `homepage_url` - Home page or landing page of the associated application. +* `login_url` - 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. +* `logout_url` - The URL that will be used by Microsoft's authorization service to logout an user using OpenId Connect front-channel, back-channel or SAML logout protocols, taken from the associated application. +* `notes` - A free text field to capture information about the service principal, typically used for operational purposes. +* `notification_email_addresses` - A list 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. * `object_id` - The object ID for the service principal. +* `oauth2_permission_scope_ids` - A mapping of OAuth2.0 permission scope values to scope IDs, as exposed by the associated application, intended to be useful when referencing permission scopes in other resources in your configuration. * `oauth2_permission_scopes` - A collection of OAuth 2.0 delegated permissions exposed by the associated application. Each permission is covered by an `oauth2_permission_scopes` block as documented below. +* `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. +* `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. +* `type` - Identifies whether the service principal represents an application or a managed identity. Possible values include `Application` or `ManagedIdentity`. --- diff --git a/docs/guides/microsoft-graph.md b/docs/guides/microsoft-graph.md index d49c51c8f2..4b0bbb495d 100644 --- a/docs/guides/microsoft-graph.md +++ b/docs/guides/microsoft-graph.md @@ -104,7 +104,7 @@ The `mail_enabled` and `security_enabled` fields are no longer read-only, and at ## Removal of deprecated fields -The following attributes/properties have been deprecated in the AzureAD provider, and has been removed in version 2.0. +The following attributes/properties were deprecated in the AzureAD provider, and have now been removed in version 2.0. ~> **Compatibility Note** You will need to update your Terraform configuration in the latest v1.x release to use the new fields, prior to upgrading to 2.0. @@ -249,12 +249,14 @@ The `id` field in the `app_role` block was previously currently Computed (read-o The `id` field in the deprecated `oauth2_permissions` block was previously Computed (read-only) but its replacement field `id` in the `oauth2_permission_scope` block is Required. -## Computed fields +## Computed fields and other breaking changes In previous version of the provider, many fields were introduced as Optional + Computed fields. This meant that omitting such fields would cause Terraform to ignore them and not attempt to manage them. However, this approach has many side effects including the inability to unset or clear these fields, and sometimes being forced to accept an undesired default value. To resolve these issues, many of these fields are no longer Computed in version 2.0 of the provider. This means that Terraform will manage these fields and if you do not specify their values in your configuration, they will be unset or set to their default or zero values. In some cases it's appropriate for a field to be Computed, particularly where it helps prevent disruption to services or users. +Additionally, some fields have been updated in ways that may break existing configurations, for example changing their type. + Accordingly, in version 2.0 of the provider the following fields have changed. ### Resource: `azuread_application` @@ -265,7 +267,7 @@ The `value` field in the `app_role` block is no longer Computed, omitting this f The `fallback_public_client_enabled` field is no longer Computed, omitting this field will cause Terraform to default this value to `false`. -The `identifier_uris` field is no longer Computed, omitting this field will cause Terraform to remove any identifier URIs configured for an application. +The `identifier_uris` field was previously a List type field and is now a Set type field. This is due to API ordering and means you can no longer reference this field and index it sequentially without first converting it to a list. Additional, this field is no longer Computed, so omitting this field will cause Terraform to remove any identifier URIs configured for an application. The `oauth2_permission_scope` block is no longer Computed, omitting this block will cause Terraform to remove any OAuth2 permission scopes published by an application. diff --git a/docs/resources/application.md b/docs/resources/application.md index d49537e77b..2d980d045a 100644 --- a/docs/resources/application.md +++ b/docs/resources/application.md @@ -131,7 +131,7 @@ The following arguments are supported: * `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`. * `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) The 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. +* `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. * `marketing_url` - (Optional) URL of the application's marketing page. * `oauth2_post_response_required` - (Optional) Specifies whether, as part of OAuth 2.0 token requests, Azure AD allows POST requests, as opposed to GET requests. Defaults to `false`, which specifies that only GET requests are allowed. * `optional_claims` - (Optional) An `optional_claims` block as documented below. diff --git a/docs/resources/application_password.md b/docs/resources/application_password.md index b9e1116b0d..a35e6d66e7 100644 --- a/docs/resources/application_password.md +++ b/docs/resources/application_password.md @@ -8,13 +8,34 @@ Manages a password credential associated with an application within Azure Active ## Example Usage +*Basic example* + +```terraform +resource "azuread_application" "example" { + display_name = "example" +} + +resource "azuread_application_password" "example" { + application_object_id = azuread_application.example.object_id +} +``` + +*Time-based rotation* + ```terraform resource "azuread_application" "example" { display_name = "example" } +resource "time_rotating" "example" { + rotation_days = 7 +} + resource "azuread_application_password" "example" { application_object_id = azuread_application.example.object_id + keepers = { + rotation = time_rotating.example.id + } } ``` diff --git a/docs/resources/service_principal.md b/docs/resources/service_principal.md index eb3a2d5501..850cdfb6f4 100644 --- a/docs/resources/service_principal.md +++ b/docs/resources/service_principal.md @@ -25,18 +25,38 @@ resource "azuread_service_principal" "example" { The following arguments are supported: +* `account_enabled` - (Optional) Whether or not the service principal account is enabled. Defaults to `true`. +* `alternative_names` - (Optional) A set of alternative names, used to retrieve service principals by subscription, identify resource group and full resource ids for managed identities. * `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. +* `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. +* `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. * `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). ## Attributes Reference In addition to all arguments above, the following attributes are exported: -* `app_roles` - A list of app roles published b the associated application, as documented below. For more information [official documentation](https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles). +* `app_role_ids` - A mapping of app role values to app role IDs, as published by the associated application, intended to be useful when referencing app roles in other resources in your configuration. +* `app_roles` - A list of app roles published by the associated application, as documented below. For more information [official documentation](https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles). +* `application_tenant_id` - The tenant ID where the associated application is registered. * `display_name` - The display name of the application associated with this service principal. -* `oauth2_permission_scopes` - A list of OAuth 2.0 delegated permission scopes published by the associated application, as documented below. +* `homepage_url` - Home page or landing page of the associated application. +* `logout_url` - The URL that will be used by Microsoft's authorization service to logout an user using OpenId Connect front-channel, back-channel or SAML logout protocols, taken from the associated application. +* `oauth2_permission_scope_ids` - A mapping of OAuth2.0 permission scope values to scope IDs, as exposed by the associated application, intended to be useful when referencing permission scopes in other resources in your configuration. +* `oauth2_permission_scopes` - A list of OAuth 2.0 delegated permission scopes exposed by the associated application, as documented below. * `object_id` - The object ID of the service principal. +* `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. +* `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`. +* `type` - Identifies whether the service principal represents an application or a managed identity. Possible values include `Application` or `ManagedIdentity`. --- diff --git a/docs/resources/service_principal_password.md b/docs/resources/service_principal_password.md index f88cfc4b8c..100042461a 100644 --- a/docs/resources/service_principal_password.md +++ b/docs/resources/service_principal_password.md @@ -8,6 +8,8 @@ Manages a password credential associated with a service principal within Azure A ## Example Usage +*Basic example* + ```terraform resource "azuread_application" "example" { display_name = "example" @@ -22,6 +24,30 @@ resource "azuread_service_principal_password" "example" { } ``` +*Time-based rotation* + +```terraform +resource "azuread_application" "example" { + display_name = "example" +} + +resource "azuread_service_principal" "example" { + application_id = azuread_application.example.application_id +} + +resource "time_rotating" "example" { + rotation_days = 7 +} + +resource "azuread_service_principal_password" "example" { + service_principal_id = azuread_service_principal.example.object_id + keepers = { + rotation = time_rotating.example.id + } +} +``` + + ## Argument Reference The following arguments are supported: diff --git a/go.mod b/go.mod index fd55183504..b7f9000226 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.6.1 github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 // indirect github.com/klauspost/compress v1.12.2 // indirect - github.com/manicminer/hamilton v0.21.0 + github.com/manicminer/hamilton v0.22.0 github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect diff --git a/go.sum b/go.sum index 912e6c86c5..2026d0a5e2 100644 --- a/go.sum +++ b/go.sum @@ -281,8 +281,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/manicminer/hamilton v0.21.0 h1:TsvOK9GkUQVVostAuWA6b67kI7TW7TdGVDDFCe2baac= -github.com/manicminer/hamilton v0.21.0/go.mod h1:y0lB5Ey1UesBkFa9NAtybwWPoN4v1SbY1Chp3OqGtN4= +github.com/manicminer/hamilton v0.22.0 h1:qsoUquW//kQoxZtq8zTnHCVO7jzp9+iYoDsBfkQ3G50= +github.com/manicminer/hamilton v0.22.0/go.mod h1:y0lB5Ey1UesBkFa9NAtybwWPoN4v1SbY1Chp3OqGtN4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= diff --git a/internal/acceptance/testcase.go b/internal/acceptance/testcase.go index e5298543aa..a510df9cd0 100644 --- a/internal/acceptance/testcase.go +++ b/internal/acceptance/testcase.go @@ -35,6 +35,15 @@ func (td TestData) ResourceTest(t *testing.T, testResource types.TestResource, s td.runAcceptanceTest(t, testCase) } +func (td TestData) ResourceTestIgnoreDangling(t *testing.T, _ types.TestResource, steps []resource.TestStep) { + testCase := resource.TestCase{ + PreCheck: func() { PreCheck(t) }, + Steps: steps, + } + + td.runAcceptanceTest(t, testCase) +} + func (td TestData) runAcceptanceTest(t *testing.T, testCase resource.TestCase) { testCase.ProviderFactories = map[string]func() (*schema.Provider, error){ "azuread": func() (*schema.Provider, error) { diff --git a/internal/clients/builder.go b/internal/clients/builder.go index 0595cd6d84..a4c036a57a 100644 --- a/internal/clients/builder.go +++ b/internal/clients/builder.go @@ -42,7 +42,7 @@ func (b *ClientBuilder) Build(ctx context.Context) (*Client, error) { } client.TenantID = cli.TenantID if clientId, ok := environments.PublishedApis["MicrosoftAzureCli"]; ok && clientId != "" { - client.ClientID = string(clientId) + client.ClientID = clientId } } diff --git a/internal/services/applications/application_data_source_test.go b/internal/services/applications/application_data_source_test.go index 2ba7e3b728..8bdf1ceb6c 100644 --- a/internal/services/applications/application_data_source_test.go +++ b/internal/services/applications/application_data_source_test.go @@ -58,8 +58,7 @@ func (ApplicationDataSource) testCheck(data acceptance.TestData) resource.TestCh check.That(data.ResourceName).Key("display_name").HasValue(fmt.Sprintf("acctest-APP-complete-%d", data.RandomInteger)), 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("1"), - check.That(data.ResourceName).Key("identifier_uris.0").HasValue(fmt.Sprintf("api://hashicorptestapp-%d", data.RandomInteger)), + check.That(data.ResourceName).Key("identifier_uris.#").HasValue("2"), check.That(data.ResourceName).Key("oauth2_permission_scope_ids.%").HasValue("2"), check.That(data.ResourceName).Key("optional_claims.#").HasValue("1"), check.That(data.ResourceName).Key("optional_claims.0.access_token.#").HasValue("2"), diff --git a/internal/services/applications/application_password_resource.go b/internal/services/applications/application_password_resource.go index 08ddb2dede..e3b7f7c71c 100644 --- a/internal/services/applications/application_password_resource.go +++ b/internal/services/applications/application_password_resource.go @@ -52,12 +52,6 @@ func applicationPasswordResource() *schema.Resource { ValidateDiagFunc: validate.UUID, }, - "key_id": { - Description: "A UUID used to uniquely identify this password credential", - Type: schema.TypeString, - Computed: true, - }, - "display_name": { Description: "A display name for the password", Type: schema.TypeString, @@ -94,6 +88,22 @@ func applicationPasswordResource() *schema.Resource { ValidateDiagFunc: validate.NoEmptyStrings, }, + "keepers": { + Description: "Arbitrary map of values that, when changed, will trigger rotation of the password", + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "key_id": { + Description: "A UUID used to uniquely identify this password credential", + Type: schema.TypeString, + Computed: true, + }, + "value": { Description: "The password for this application, which is generated by Azure Active Directory", Type: schema.TypeString, diff --git a/internal/services/applications/application_published_app_ids_data_source.go b/internal/services/applications/application_published_app_ids_data_source.go new file mode 100644 index 0000000000..dc2b850d3b --- /dev/null +++ b/internal/services/applications/application_published_app_ids_data_source.go @@ -0,0 +1,37 @@ +package applications + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/manicminer/hamilton/environments" + + "github.com/hashicorp/terraform-provider-azuread/internal/tf" +) + +func applicationPublishedAppIdsDataSource() *schema.Resource { + return &schema.Resource{ + ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + tf.Set(d, "result", environments.PublishedApis) + d.SetId("appIds") + return nil + }, + + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "result": { + Description: "A mapping of application names and application IDs", + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} diff --git a/internal/services/applications/application_published_app_ids_data_source_test.go b/internal/services/applications/application_published_app_ids_data_source_test.go new file mode 100644 index 0000000000..4db84b595a --- /dev/null +++ b/internal/services/applications/application_published_app_ids_data_source_test.go @@ -0,0 +1,30 @@ +package applications_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" +) + +type ApplicationPublishedAppIdsDataSource struct{} + +func TestAccApplicationPublishedAppIdsDataSource_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azuread_application_published_app_ids", "test") + r := ApplicationPublishedAppIdsDataSource{} + + data.DataSourceTest(t, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("result.%").Exists(), + ), + }, + }) +} + +func (ApplicationPublishedAppIdsDataSource) basic(data acceptance.TestData) string { + return `provider azuread {} +data "azuread_application_published_app_ids" "test" {}` +} diff --git a/internal/services/applications/application_resource.go b/internal/services/applications/application_resource.go index f729d3f768..c1362622a7 100644 --- a/internal/services/applications/application_resource.go +++ b/internal/services/applications/application_resource.go @@ -286,7 +286,7 @@ func applicationResource() *schema.Resource { "identifier_uris": { Description: "The 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", - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, @@ -593,7 +593,7 @@ func applicationResourceCustomizeDiff(ctx context.Context, diff *schema.Resource // applications that change from AAD (corporate) account sign-ins to personal account sign-ins if s := diff.Get("sign_in_audience").(string); s == msgraph.SignInAudienceAzureADandPersonalMicrosoftAccount || s == msgraph.SignInAudiencePersonalMicrosoftAccount { oauth2PermissionScopes := diff.Get("api.0.oauth2_permission_scope").(*schema.Set).List() - identifierUris := diff.Get("identifier_uris").([]interface{}) + identifierUris := diff.Get("identifier_uris").(*schema.Set).List() pubRedirectUris := diff.Get("public_client.0.redirect_uris").(*schema.Set).List() spaRedirectUris := diff.Get("single_page_application.0.redirect_uris").(*schema.Set).List() webRedirectUris := diff.Get("web.0.redirect_uris").(*schema.Set).List() @@ -622,6 +622,23 @@ func applicationResourceCustomizeDiff(ctx context.Context, diff *schema.Resource } } + // maximum number of scopes is 100 with personal account sign-ins + if len(oauth2PermissionScopes) > 100 { + return fmt.Errorf("maximum of 100 `oauth2_permission_scope` blocks are supported when `sign_in_audience` is %q or %q", + msgraph.SignInAudienceAzureADandPersonalMicrosoftAccount, msgraph.SignInAudiencePersonalMicrosoftAccount) + } + + // scope name maximum length is 40 characters with personal account sign-ins + for _, raw := range oauth2PermissionScopes { + scope := raw.(map[string]interface{}) + if v, ok := scope["value"]; ok { + if len(v.(string)) > 40 { + return fmt.Errorf("`value` property in the `oauth2_permission_scope` block must be 40 characters or less when `sign_in_audience` is %q or %q", + msgraph.SignInAudienceAzureADandPersonalMicrosoftAccount, msgraph.SignInAudiencePersonalMicrosoftAccount) + } + } + } + // urn scheme not supported with personal account sign-ins for _, v := range identifierUris { if diags := validate.IsURIFunc([]string{"http", "https", "api", "ms-appx"}, false, false)(v, cty.Path{}); diags.HasError() { @@ -811,7 +828,7 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta AppRoles: expandApplicationAppRoles(d.Get("app_role").(*schema.Set).List()), DisplayName: utils.String(displayName), GroupMembershipClaims: expandApplicationGroupMembershipClaims(d.Get("group_membership_claims").(*schema.Set).List()), - IdentifierUris: tf.ExpandStringSlicePtr(d.Get("identifier_uris").([]interface{})), + IdentifierUris: tf.ExpandStringSlicePtr(d.Get("identifier_uris").(*schema.Set).List()), Info: &msgraph.InformationalUrl{ MarketingUrl: utils.String(d.Get("marketing_url").(string)), PrivacyStatementUrl: utils.String(d.Get("privacy_statement_url").(string)), @@ -878,7 +895,7 @@ func applicationResourceUpdate(ctx context.Context, d *schema.ResourceData, meta AppRoles: expandApplicationAppRoles(d.Get("app_role").(*schema.Set).List()), DisplayName: utils.String(displayName), GroupMembershipClaims: expandApplicationGroupMembershipClaims(d.Get("group_membership_claims").(*schema.Set).List()), - IdentifierUris: tf.ExpandStringSlicePtr(d.Get("identifier_uris").([]interface{})), + IdentifierUris: tf.ExpandStringSlicePtr(d.Get("identifier_uris").(*schema.Set).List()), Info: &msgraph.InformationalUrl{ MarketingUrl: utils.String(d.Get("marketing_url").(string)), PrivacyStatementUrl: utils.String(d.Get("privacy_statement_url").(string)), diff --git a/internal/services/applications/application_resource_test.go b/internal/services/applications/application_resource_test.go index a4ef4bd75b..7f23926c84 100644 --- a/internal/services/applications/application_resource_test.go +++ b/internal/services/applications/application_resource_test.go @@ -394,10 +394,14 @@ resource "azuread_application" "known2" { resource "azuread_application" "test" { display_name = "acctest-APP-complete-%[1]d" - identifier_uris = ["api://hashicorptestapp-%[1]d"] group_membership_claims = ["All"] sign_in_audience = "AzureADandPersonalMicrosoftAccount" + identifier_uris = [ + "api://hashicorptestapp-%[1]d", + "api://acctest-APP-complete-%[1]d", + ] + device_only_auth_enabled = true fallback_public_client_enabled = true oauth2_post_response_required = true diff --git a/internal/services/applications/applications.go b/internal/services/applications/applications.go index 0cc7705bbe..c1c6f67c98 100644 --- a/internal/services/applications/applications.go +++ b/internal/services/applications/applications.go @@ -455,7 +455,7 @@ func expandApplicationGroupMembershipClaims(in []interface{}) *[]msgraph.GroupMe func expandApplicationImplicitGrantSettings(input []interface{}) *msgraph.ImplicitGrantSettings { var enableAccessTokenIssuance, enableIdTokenIssuance bool - if input != nil || len(input) > 0 { + if len(input) > 0 { in := input[0].(map[string]interface{}) enableAccessTokenIssuance = in["access_token_issuance_enabled"].(bool) enableIdTokenIssuance = in["id_token_issuance_enabled"].(bool) diff --git a/internal/services/applications/registration.go b/internal/services/applications/registration.go index 724977be1f..d1080efd40 100644 --- a/internal/services/applications/registration.go +++ b/internal/services/applications/registration.go @@ -21,7 +21,8 @@ func (r Registration) WebsiteCategories() []string { // SupportedDataSources returns the supported Data Sources supported by this Service func (r Registration) SupportedDataSources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azuread_application": applicationDataSource(), + "azuread_application": applicationDataSource(), + "azuread_application_published_app_ids": applicationPublishedAppIdsDataSource(), } } diff --git a/internal/services/serviceprincipals/service_principal_data_source.go b/internal/services/serviceprincipals/service_principal_data_source.go index 5db0598781..55ac4d6fe2 100644 --- a/internal/services/serviceprincipals/service_principal_data_source.go +++ b/internal/services/serviceprincipals/service_principal_data_source.go @@ -54,9 +54,144 @@ func servicePrincipalData() *schema.Resource { ValidateDiagFunc: validate.UUID, }, + "account_enabled": { + Description: "Whether or not the service principal account is enabled", + Type: schema.TypeBool, + Computed: true, + }, + + "alternative_names": { + Description: "A list of alternative names, used to retrieve service principals by subscription, identify resource group and full resource ids for managed identities", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "app_role_assignment_required": { + Description: "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", + Type: schema.TypeBool, + Computed: true, + }, + + "application_tenant_id": { + Description: "The tenant ID where the associated application is registered", + Type: schema.TypeString, + Computed: true, + }, + "app_roles": schemaAppRolesComputed(), + "app_role_ids": { + Description: "Mapping of app role names to UUIDs", + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "description": { + Description: "Description of the service principal provided for internal end-users", + Type: schema.TypeString, + Computed: true, + }, + + "homepage_url": { + Description: "Home page or landing page of the application", + Type: schema.TypeString, + Computed: true, + }, + + "login_url": { + Description: "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", + Type: schema.TypeString, + Computed: true, + }, + + "logout_url": { + Description: "The URL that will be used by Microsoft's authorization service to sign out a user using front-channel, back-channel or SAML logout protocols", + Type: schema.TypeString, + Computed: true, + }, + + "notes": { + Description: "Free text field to capture information about the service principal, typically used for operational purposes", + Type: schema.TypeString, + Computed: true, + }, + + "notification_email_addresses": { + Description: "List 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", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "oauth2_permission_scopes": schemaOauth2PermissionScopesComputed(), + + "oauth2_permission_scope_ids": { + Description: "Mapping of OAuth2.0 permission scope names to UUIDs", + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "preferred_single_sign_on_mode": { + Description: "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", + Type: schema.TypeString, + Computed: true, + }, + + "redirect_uris": { + Description: "The 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", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "saml_metadata_url": { + Description: "The URL where the service exposes SAML metadata for federation", + Type: schema.TypeString, + Computed: true, + }, + + "service_principal_names": { + Description: "A list of identifier URI(s), copied over from the associated application", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "sign_in_audience": { + Description: "The Microsoft account types that are supported for the associated application", + Type: schema.TypeString, + Computed: true, + }, + + "tags": { + Description: "A set of tags to apply to the service principal", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "type": { + Description: "Identifies whether the service principal represents an application or a managed identity", + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -142,11 +277,40 @@ func servicePrincipalDataSourceRead(ctx context.Context, d *schema.ResourceData, d.SetId(*servicePrincipal.ID) + servicePrincipalNames := make([]string, 0) + if servicePrincipal.ServicePrincipalNames != nil { + for _, name := range *servicePrincipal.ServicePrincipalNames { + // Exclude the app ID from the list of service principal names + if servicePrincipal.AppId == nil || name != *servicePrincipal.AppId { + servicePrincipalNames = append(servicePrincipalNames, name) + } + } + } + + tf.Set(d, "account_enabled", servicePrincipal.AccountEnabled) + tf.Set(d, "alternative_names", tf.FlattenStringSlicePtr(servicePrincipal.AlternativeNames)) + tf.Set(d, "app_role_assignment_required", servicePrincipal.AppRoleAssignmentRequired) + tf.Set(d, "app_role_ids", helpers.ApplicationFlattenAppRoleIDs(servicePrincipal.AppRoles)) tf.Set(d, "app_roles", helpers.ApplicationFlattenAppRoles(servicePrincipal.AppRoles)) tf.Set(d, "application_id", servicePrincipal.AppId) + tf.Set(d, "application_tenant_id", servicePrincipal.AppOwnerOrganizationId) + tf.Set(d, "description", servicePrincipal.Description) tf.Set(d, "display_name", servicePrincipal.DisplayName) + tf.Set(d, "homepage_url", servicePrincipal.Homepage) + tf.Set(d, "logout_url", servicePrincipal.LogoutUrl) + tf.Set(d, "login_url", servicePrincipal.LoginUrl) + tf.Set(d, "notes", servicePrincipal.Notes) + tf.Set(d, "notification_email_addresses", tf.FlattenStringSlicePtr(servicePrincipal.NotificationEmailAddresses)) + tf.Set(d, "oauth2_permission_scope_ids", helpers.ApplicationFlattenOAuth2PermissionScopeIDs(servicePrincipal.PublishedPermissionScopes)) tf.Set(d, "oauth2_permission_scopes", helpers.ApplicationFlattenOAuth2PermissionScopes(servicePrincipal.PublishedPermissionScopes)) tf.Set(d, "object_id", servicePrincipal.ID) + 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, "service_principal_names", servicePrincipalNames) + tf.Set(d, "sign_in_audience", servicePrincipal.SignInAudience) + tf.Set(d, "tags", servicePrincipal.Tags) + tf.Set(d, "type", servicePrincipal.ServicePrincipalType) return nil } diff --git a/internal/services/serviceprincipals/service_principal_data_source_test.go b/internal/services/serviceprincipals/service_principal_data_source_test.go index 7881cb7287..0c7b2b7a12 100644 --- a/internal/services/serviceprincipals/service_principal_data_source_test.go +++ b/internal/services/serviceprincipals/service_principal_data_source_test.go @@ -2,6 +2,7 @@ package serviceprincipals_test import ( "fmt" + "os" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -19,13 +20,7 @@ func TestAccServicePrincipalDataSource_byApplicationId(t *testing.T) { data.DataSourceTest(t, []resource.TestStep{ { Config: r.byApplicationId(data), - Check: resource.ComposeTestCheckFunc( - check.That(data.ResourceName).Key("application_id").Exists(), - check.That(data.ResourceName).Key("object_id").Exists(), - check.That(data.ResourceName).Key("display_name").Exists(), - check.That(data.ResourceName).Key("app_roles.#").HasValue("2"), - check.That(data.ResourceName).Key("oauth2_permission_scopes.#").HasValue("2"), - ), + Check: r.testCheckFunc(data), }, }) } @@ -37,13 +32,7 @@ func TestAccServicePrincipalDataSource_byDisplayName(t *testing.T) { data.DataSourceTest(t, []resource.TestStep{ { Config: r.byDisplayName(data), - Check: resource.ComposeTestCheckFunc( - check.That(data.ResourceName).Key("application_id").Exists(), - check.That(data.ResourceName).Key("object_id").Exists(), - check.That(data.ResourceName).Key("display_name").Exists(), - check.That(data.ResourceName).Key("app_roles.#").HasValue("2"), - check.That(data.ResourceName).Key("oauth2_permission_scopes.#").HasValue("2"), - ), + Check: r.testCheckFunc(data), }, }) } @@ -55,17 +44,39 @@ func TestAccServicePrincipalDataSource_byObjectId(t *testing.T) { data.DataSourceTest(t, []resource.TestStep{ { Config: r.byObjectId(data), - Check: resource.ComposeTestCheckFunc( - check.That(data.ResourceName).Key("application_id").Exists(), - check.That(data.ResourceName).Key("object_id").Exists(), - check.That(data.ResourceName).Key("display_name").Exists(), - check.That(data.ResourceName).Key("app_roles.#").HasValue("2"), - check.That(data.ResourceName).Key("oauth2_permission_scopes.#").HasValue("2"), - ), + Check: r.testCheckFunc(data), }, }) } +func (ServicePrincipalDataSource) testCheckFunc(data acceptance.TestData) resource.TestCheckFunc { + tenantId := os.Getenv("ARM_TENANT_ID") + return resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("account_enabled").HasValue("false"), + check.That(data.ResourceName).Key("alternative_names.#").HasValue("2"), + check.That(data.ResourceName).Key("app_role_assignment_required").HasValue("true"), + 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_id").IsUuid(), + 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("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)), + check.That(data.ResourceName).Key("notes").HasValue("Just testing something"), + check.That(data.ResourceName).Key("notification_email_addresses.#").HasValue("2"), + 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("object_id").IsUuid(), + check.That(data.ResourceName).Key("redirect_uris.#").HasValue("2"), + 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("type").HasValue("Application"), + ) +} + func (ServicePrincipalDataSource) byApplicationId(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s diff --git a/internal/services/serviceprincipals/service_principal_password_resource.go b/internal/services/serviceprincipals/service_principal_password_resource.go index d16957c0ac..be423cb8b0 100644 --- a/internal/services/serviceprincipals/service_principal_password_resource.go +++ b/internal/services/serviceprincipals/service_principal_password_resource.go @@ -52,6 +52,16 @@ func servicePrincipalPasswordResource() *schema.Resource { ValidateDiagFunc: validate.UUID, }, + "keepers": { + Description: "Arbitrary map of values that, when changed, will trigger rotation of the password", + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "key_id": { Description: "A UUID used to uniquely identify this password credential", Type: schema.TypeString, diff --git a/internal/services/serviceprincipals/service_principal_resource.go b/internal/services/serviceprincipals/service_principal_resource.go index 6f717ab0aa..569c50737f 100644 --- a/internal/services/serviceprincipals/service_principal_resource.go +++ b/internal/services/serviceprincipals/service_principal_resource.go @@ -6,8 +6,13 @@ import ( "fmt" "log" "net/http" + "strings" "time" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/manicminer/hamilton/odata" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -52,37 +57,176 @@ func servicePrincipalResource() *schema.Resource { ValidateDiagFunc: validate.UUID, }, + "account_enabled": { + Description: "Whether or not the service principal account is enabled", + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "alternative_names": { + Description: "A list of alternative names, used to retrieve service principals by subscription, identify resource group and full resource ids for managed identities", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + }, + "app_role_assignment_required": { Description: "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", Type: schema.TypeBool, Optional: true, }, + "description": { + Description: "Description of the service principal provided for internal end-users", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 1024), + }, + + "login_url": { + Description: "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", + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validate.IsHTTPOrHTTPSURL, + }, + + "notes": { + Description: "Free text field to capture information about the service principal, typically used for operational purposes", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 1024), + }, + + "notification_email_addresses": { + Description: "List 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", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + }, + + "preferred_single_sign_on_mode": { + Description: "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", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(msgraph.PreferredSingleSignOnModeNone), + string(msgraph.PreferredSingleSignOnModeNotSupported), + string(msgraph.PreferredSingleSignOnModeOidc), + string(msgraph.PreferredSingleSignOnModePassword), + string(msgraph.PreferredSingleSignOnModeSaml), + }, false), + }, + + "tags": { + Description: "A set of tags to apply to the service principal", + Type: schema.TypeSet, + Optional: true, + Set: schema.HashString, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "use_existing": { + Description: "When true, the resource will return an existing service principal instead of failing with an error", + Type: schema.TypeBool, + Optional: true, + }, + + "app_roles": schemaAppRolesComputed(), + + "app_role_ids": { + Description: "Mapping of app role names to UUIDs", + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "application_tenant_id": { + Description: "The tenant ID where the associated application is registered", + Type: schema.TypeString, + Computed: true, + }, + "display_name": { Description: "The display name of the application associated with this service principal", Type: schema.TypeString, Computed: true, }, + "homepage_url": { + Description: "Home page or landing page of the application", + Type: schema.TypeString, + Computed: true, + }, + + "logout_url": { + Description: "The URL that will be used by Microsoft's authorization service to sign out a user using front-channel, back-channel or SAML logout protocols", + Type: schema.TypeString, + Computed: true, + }, + + "oauth2_permission_scopes": schemaOauth2PermissionScopesComputed(), + + "oauth2_permission_scope_ids": { + Description: "Mapping of OAuth2.0 permission scope names to UUIDs", + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "object_id": { Description: "The object ID of the service principal", Type: schema.TypeString, Computed: true, }, - "app_roles": schemaAppRolesComputed(), + "redirect_uris": { + Description: "The 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", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, - "oauth2_permission_scopes": schemaOauth2PermissionScopesComputed(), + "saml_metadata_url": { + Description: "The URL where the service exposes SAML metadata for federation", + Type: schema.TypeString, + Computed: true, + }, - "tags": { - Description: "A set of tags to apply to the service principal", - Type: schema.TypeSet, - Optional: true, - Set: schema.HashString, + "service_principal_names": { + Description: "A list of identifier URI(s), copied over from the associated application", + Type: schema.TypeList, + Computed: true, Elem: &schema.Schema{ Type: schema.TypeString, }, }, + + "sign_in_audience": { + Description: "The Microsoft account types that are supported for the associated application", + Type: schema.TypeString, + Computed: true, + }, + + "type": { + Description: "Identifies whether the service principal represents an application or a managed identity", + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -90,17 +234,51 @@ func servicePrincipalResource() *schema.Resource { func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + appId := d.Get("application_id").(string) + result, _, err := client.List(ctx, odata.Query{Filter: fmt.Sprintf("appId eq '%s'", appId)}) + if err != nil { + return tf.ErrorDiagF(err, "Could not list existing service principals") + } + var servicePrincipal *msgraph.ServicePrincipal + if result != nil { + for _, r := range *result { + if r.AppId != nil && strings.EqualFold(*r.AppId, appId) { + servicePrincipal = &r + break + } + } + } + + if servicePrincipal != nil { + if servicePrincipal.ID == nil || *servicePrincipal.ID == "" { + return tf.ErrorDiagF(fmt.Errorf("service principal returned with nil or empty object ID"), "API error") + } + if !d.Get("use_existing").(bool) { + return tf.ImportAsExistsDiag("azuread_service_principal", *servicePrincipal.ID) + } + + d.SetId(*servicePrincipal.ID) + return servicePrincipalResourceUpdate(ctx, d, meta) + } + properties := msgraph.ServicePrincipal{ - AccountEnabled: utils.Bool(true), - AppId: utils.String(d.Get("application_id").(string)), - AppRoleAssignmentRequired: utils.Bool(d.Get("app_role_assignment_required").(bool)), - Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()), + AccountEnabled: utils.Bool(d.Get("account_enabled").(bool)), + AlternativeNames: tf.ExpandStringSlicePtr(d.Get("alternative_names").(*schema.Set).List()), + AppId: utils.String(d.Get("application_id").(string)), + AppRoleAssignmentRequired: utils.Bool(d.Get("app_role_assignment_required").(bool)), + Description: utils.NullableString(d.Get("description").(string)), + LoginUrl: utils.NullableString(d.Get("login_url").(string)), + 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)), + Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()), } - servicePrincipal, _, err := client.Create(ctx, properties) + servicePrincipal, _, err = client.Create(ctx, properties) if err != nil { return tf.ErrorDiagF(err, "Could not create service principal") } + if servicePrincipal.ID == nil || *servicePrincipal.ID == "" { return tf.ErrorDiagF(errors.New("Object ID returned for service principal is nil"), "Bad API response") } @@ -113,9 +291,16 @@ func servicePrincipalResourceUpdate(ctx context.Context, d *schema.ResourceData, client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient properties := msgraph.ServicePrincipal{ - ID: utils.String(d.Id()), - AppRoleAssignmentRequired: utils.Bool(d.Get("app_role_assignment_required").(bool)), - Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()), + ID: utils.String(d.Id()), + AlternativeNames: tf.ExpandStringSlicePtr(d.Get("alternative_names").(*schema.Set).List()), + AccountEnabled: utils.Bool(d.Get("account_enabled").(bool)), + AppRoleAssignmentRequired: utils.Bool(d.Get("app_role_assignment_required").(bool)), + Description: utils.NullableString(d.Get("description").(string)), + LoginUrl: utils.NullableString(d.Get("login_url").(string)), + 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)), + Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()), } if _, err := client.Update(ctx, properties); err != nil { @@ -140,13 +325,40 @@ func servicePrincipalResourceRead(ctx context.Context, d *schema.ResourceData, m return tf.ErrorDiagF(err, "retrieving service principal with object ID: %q", d.Id()) } + servicePrincipalNames := make([]string, 0) + if servicePrincipal.ServicePrincipalNames != nil { + for _, name := range *servicePrincipal.ServicePrincipalNames { + // Exclude the app ID from the list of service principal names + if servicePrincipal.AppId == nil || name != *servicePrincipal.AppId { + servicePrincipalNames = append(servicePrincipalNames, name) + } + } + } + + tf.Set(d, "account_enabled", servicePrincipal.AccountEnabled) + tf.Set(d, "alternative_names", tf.FlattenStringSlicePtr(servicePrincipal.AlternativeNames)) tf.Set(d, "app_role_assignment_required", servicePrincipal.AppRoleAssignmentRequired) + tf.Set(d, "app_role_ids", helpers.ApplicationFlattenAppRoleIDs(servicePrincipal.AppRoles)) tf.Set(d, "app_roles", helpers.ApplicationFlattenAppRoles(servicePrincipal.AppRoles)) tf.Set(d, "application_id", servicePrincipal.AppId) + tf.Set(d, "application_tenant_id", servicePrincipal.AppOwnerOrganizationId) + tf.Set(d, "description", servicePrincipal.Description) tf.Set(d, "display_name", servicePrincipal.DisplayName) + tf.Set(d, "homepage_url", servicePrincipal.Homepage) + tf.Set(d, "logout_url", servicePrincipal.LogoutUrl) + tf.Set(d, "login_url", servicePrincipal.LoginUrl) + tf.Set(d, "notes", servicePrincipal.Notes) + tf.Set(d, "notification_email_addresses", tf.FlattenStringSlicePtr(servicePrincipal.NotificationEmailAddresses)) + tf.Set(d, "oauth2_permission_scope_ids", helpers.ApplicationFlattenOAuth2PermissionScopeIDs(servicePrincipal.PublishedPermissionScopes)) tf.Set(d, "oauth2_permission_scopes", helpers.ApplicationFlattenOAuth2PermissionScopes(servicePrincipal.PublishedPermissionScopes)) tf.Set(d, "object_id", servicePrincipal.ID) + 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, "service_principal_names", servicePrincipalNames) + tf.Set(d, "sign_in_audience", servicePrincipal.SignInAudience) tf.Set(d, "tags", servicePrincipal.Tags) + tf.Set(d, "type", servicePrincipal.ServicePrincipalType) return nil } @@ -163,8 +375,9 @@ func servicePrincipalResourceDelete(ctx context.Context, d *schema.ResourceData, return tf.ErrorDiagPathF(err, "id", "Retrieving service principal with object ID %q", d.Id()) } + useExisting := d.Get("use_existing").(bool) status, err = client.Delete(ctx, d.Id()) - if err != nil { + if err != nil && !useExisting { return tf.ErrorDiagPathF(err, "id", "Deleting service principal with object ID %q, got status %d", d.Id(), status) } diff --git a/internal/services/serviceprincipals/service_principal_resource_test.go b/internal/services/serviceprincipals/service_principal_resource_test.go index 4cc64e6a21..402105d844 100644 --- a/internal/services/serviceprincipals/service_principal_resource_test.go +++ b/internal/services/serviceprincipals/service_principal_resource_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "os" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -28,24 +29,34 @@ func TestAccServicePrincipal_basic(t *testing.T) { check.That(data.ResourceName).ExistsInAzure(r), ), }, - data.ImportStep(), + data.ImportStep("use_existing"), }) } func TestAccServicePrincipal_complete(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.complete(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(), + data.ImportStep("use_existing"), }) } @@ -58,23 +69,54 @@ func TestAccServicePrincipal_update(t *testing.T) { Config: r.basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("app_roles.#").HasValue("0"), + check.That(data.ResourceName).Key("app_role_ids.%").HasValue("0"), + check.That(data.ResourceName).Key("oauth2_permission_scope_ids.%").HasValue("0"), + check.That(data.ResourceName).Key("oauth2_permission_scopes.#").HasValue("0"), ), }, - data.ImportStep(), + data.ImportStep("use_existing"), { Config: r.complete(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("app_roles.#").HasValue("2"), + check.That(data.ResourceName).Key("app_role_ids.%").HasValue("2"), + check.That(data.ResourceName).Key("oauth2_permission_scope_ids.%").HasValue("2"), + check.That(data.ResourceName).Key("oauth2_permission_scopes.#").HasValue("2"), ), }, - data.ImportStep(), + data.ImportStep("use_existing"), { Config: r.basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("app_roles.#").HasValue("0"), + check.That(data.ResourceName).Key("app_role_ids.%").HasValue("0"), + check.That(data.ResourceName).Key("oauth2_permission_scope_ids.%").HasValue("0"), + check.That(data.ResourceName).Key("oauth2_permission_scopes.#").HasValue("0"), + ), + }, + data.ImportStep("use_existing"), + }) +} + +func TestAccServicePrincipal_useExisting(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal", "msgraph") + r := ServicePrincipalResource{} + + data.ResourceTestIgnoreDangling(t, r, []resource.TestStep{ + { + Config: r.useExisting(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("app_roles.#").Exists(), + check.That(data.ResourceName).Key("app_role_ids.%").Exists(), + check.That(data.ResourceName).Key("oauth2_permission_scope_ids.%").Exists(), + check.That(data.ResourceName).Key("oauth2_permission_scopes.#").Exists(), ), }, - data.ImportStep(), + data.ImportStep("use_existing"), }) } @@ -107,7 +149,13 @@ resource "azuread_service_principal" "test" { func (ServicePrincipalResource) complete(data acceptance.TestData) string { return fmt.Sprintf(` resource "azuread_application" "test" { - display_name = "acctestServicePrincipal-%[1]d" + display_name = "acctestServicePrincipal-%[1]d" + sign_in_audience = "AzureADMyOrg" + + identifier_uris = [ + "api://acctestServicePrincipal-%[1]d", + "https://acctestServicePrincipal-%[1]d.net", + ] api { oauth2_permission_scope { @@ -148,13 +196,44 @@ resource "azuread_application" "test" { id = "%[5]s" value = "readOnlyUser" } + + web { + homepage_url = "https://test-%[1]d.internal" + logout_url = "https://test-%[1]d.internal/logout" + + redirect_uris = [ + "https://test-%[1]d.internal/dashboard", + "https://test-%[1]d.internal/account", + ] + } } resource "azuread_service_principal" "test" { - application_id = azuread_application.test.application_id - app_role_assignment_required = true + application_id = azuread_application.test.application_id - tags = ["test", "multiple", "CapitalS"] + account_enabled = false + app_role_assignment_required = true + description = "An internal app for testing" + login_url = "https://test-%[1]d.internal/login" + notes = "Just testing something" + preferred_single_sign_on_mode = "saml" + + notification_email_addresses = [ + "alerts.internal@hashitown.net", + "cto@hashitown.net", + ] + + alternative_names = ["foo", "bar"] + tags = ["test", "multiple", "CapitalS"] } `, data.RandomInteger, data.UUID(), data.UUID(), data.UUID(), data.UUID()) } + +func (ServicePrincipalResource) useExisting(_ acceptance.TestData) string { + return ` +resource "azuread_service_principal" "msgraph" { + application_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph + use_existing = true +} +` +} diff --git a/vendor/github.com/manicminer/hamilton/environments/published.go b/vendor/github.com/manicminer/hamilton/environments/published.go index 3d2ef91966..a33374e1ed 100644 --- a/vendor/github.com/manicminer/hamilton/environments/published.go +++ b/vendor/github.com/manicminer/hamilton/environments/published.go @@ -1,6 +1,6 @@ package environments -type ApiAppId string +type ApiAppId = string // PublishedApis is a map containing Application IDs for well known APIs published by Microsoft. // They can be used to acquire access tokens, but are primarily described here for easy inclusion in diff --git a/vendor/github.com/manicminer/hamilton/msgraph/models.go b/vendor/github.com/manicminer/hamilton/msgraph/models.go index 8f422790e2..91959ec060 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/models.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/models.go @@ -854,19 +854,22 @@ type ServicePrincipal struct { AppRoleAssignmentRequired *bool `json:"appRoleAssignmentRequired,omitempty"` AppRoles *[]AppRole `json:"appRoles,omitempty"` DeletedDateTime *time.Time `json:"deletedDateTime,omitempty"` + Description *StringNullWhenEmpty `json:"description,omitempty"` DisplayName *string `json:"displayName,omitempty"` Homepage *string `json:"homepage,omitempty"` Info *InformationalUrl `json:"info,omitempty"` KeyCredentials *[]KeyCredential `json:"keyCredentials,omitempty"` - LoginUrl *string `json:"loginUrl,omitempty"` + LoginUrl *StringNullWhenEmpty `json:"loginUrl,omitempty"` LogoutUrl *string `json:"logoutUrl,omitempty"` + Notes *StringNullWhenEmpty `json:"notes,omitempty"` NotificationEmailAddresses *[]string `json:"notificationEmailAddresses,omitempty"` PasswordCredentials *[]PasswordCredential `json:"passwordCredentials,omitempty"` PasswordSingleSignOnSettings *PasswordSingleSignOnSettings `json:"passwordSingleSignOnSettings,omitempty"` - PreferredSingleSignOnMode *string `json:"preferredSingleSignOnMode,omitempty"` + PreferredSingleSignOnMode *PreferredSingleSignOnMode `json:"preferredSingleSignOnMode,omitempty"` PreferredTokenSigningKeyEndDateTime *time.Time `json:"preferredTokenSigningKeyEndDateTime,omitempty"` PublishedPermissionScopes *[]PermissionScope `json:"publishedPermissionScopes,omitempty"` ReplyUrls *[]string `json:"replyUrls,omitempty"` + SamlMetadataUrl *StringNullWhenEmpty `json:"samlMetadataUrl,omitempty"` SamlSingleSignOnSettings *SamlSingleSignOnSettings `json:"samlSingleSignOnSettings,omitempty"` ServicePrincipalNames *[]string `json:"servicePrincipalNames,omitempty"` ServicePrincipalType *string `json:"servicePrincipalType,omitempty"` diff --git a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go index 94447ab59f..2a04c89600 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go @@ -164,6 +164,16 @@ const ( PermissionScopeTypeUser PermissionScopeType = "User" ) +type PreferredSingleSignOnMode = StringNullWhenEmpty + +const ( + PreferredSingleSignOnModeNone PreferredSingleSignOnMode = "" + PreferredSingleSignOnModeNotSupported PreferredSingleSignOnMode = "notSupported" + PreferredSingleSignOnModeOidc PreferredSingleSignOnMode = "oidc" + PreferredSingleSignOnModePassword PreferredSingleSignOnMode = "password" + PreferredSingleSignOnModeSaml PreferredSingleSignOnMode = "saml" +) + type ResourceAccessType = string const ( diff --git a/vendor/modules.txt b/vendor/modules.txt index fd986a457b..fe40afd1a3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -187,7 +187,7 @@ github.com/klauspost/compress/fse github.com/klauspost/compress/huff0 github.com/klauspost/compress/zstd github.com/klauspost/compress/zstd/internal/xxhash -# github.com/manicminer/hamilton v0.21.0 +# github.com/manicminer/hamilton v0.22.0 ## explicit github.com/manicminer/hamilton/auth github.com/manicminer/hamilton/environments