Closed AdamCoulterOz closed 1 year ago
Hi @AdamCoulterOz, thanks for the suggestion! I can see how this would solve this particular dependency. That said, whilst the portal has this default behavior, I don't believe it's a suitable default for Terraform. In my experience most people don't use the portal default as it's not especially usable when developing against the MS Identity Platform. There are also characteristics of the App ID URI that make it worthwhile to consider upfront what it should be, in particular:
So on balance, I believe the least problems are caused by not having a default value for this property, although I acknowledge this makes it infeasible to achieve in a single apply operation a configuration that mimics the portal for this property.
@manicminer sure ... I can agree to that ... can you suggest how to use this resource in this very common use case then?
Immutability concerns don't appear to conflict with the suggestion of incorporating the Application ID as a default, as the portal does when exposing an API by hand. If someone has a reason to define a distinct identifier_uris
array, they can continue to do so at resource creation time.
Whilst we try to provide equivalent functionality to the Portal, the two workflows (interactive and stateful infrastructure-as-config) don't always match up.
Providing this as a default would allow us to embed the app ID in the URI, but it wouldn't allow users to customise this, to add additional URIs, or to return to that value after changing it to something else. The portal achieves this by patching the application as you progress through the settings blade, but Terraform currently cannot follow the [arguably anti] pattern of including a generated value of the resource, in another property of the same resource.
After upgrading to version 2 of the provider our application identifier_uris
were removed and now see no way to revert them back to the previous values (which default to https://<AAD Primary Domain>/<Application ID>
in version 1) except to update via portal (in App registrations) and add a lifecycle { ignore_changes = [identifier_uris] }
block to prevent them from being removed on next apply.
Could adding a resource to specifically manage application identifier_uris or app registration be a way to create URIs with an application id?
@abergie The removal of Computed behavior from many properties is covered in our Upgrade Guide, as documented you'll need to explicitly specify these properties or Terraform will set them to their default values.
When you have an existing application with the app ID embedded in properties like identifier_uris
, at this time you'll need to hardcode the app ID in your Terraform configuration - or use a lifecycle block to stop managing the field as you have done.
@manicminer Thanks for the response. Adding a lifecycle block is certainly a stop-gap for existing applications, but not a long-term solution for us. When adding new applications which retain the previous behavior of application ids in the URI, identifier_uris
will now need to be managed outside Terraform.
Instead of removing the computed value after version 2, you could add it as an option to compute it or not.
Hi all, I appreciate this gap in functionality is frustrating when trying to manage apps that require an identifier URI containing their own client ID. It is a feature that we would like to support, however, given the stateful and idempotent architecture of Terraform, this is difficult - to the extent that we have been unable to implement this to date without breaking existing configurations and losing some functionality.
We have not forgotten about this request, and I'm hopeful that new developments within Terraform may enable us to support this use case in some form or another in the near future. That said, this still requires investigation and at this time there are no guarantees that we'll be able to do so.
In the meantime, I believe this should be possible to work around using a provisioner to manually set the identifierUris field (e.g. via Azure CLI or your own script), and adding identifier_uris
to the ignore_changes
lifecycle property so that Terraform does not overwrite your scripted changes.
@alex-oswald Unfortunately it's not possible to have a conditional Terraform schema
Hi @manicminer, I've got a little bit of time coming up where I could work on a potential solution, and I'm keen for your thoughts before I start:
Instead of trying to make this work under the current single azuread_application
resource, we could move this field into a new azuread_application_identifier_uris
resource. This new resource could then PATCH the existing application.
I know it's not keeping with the convention of 1 API == 1 resource, but unless Microsoft add a new API, this seems to be a limitation that will prevent us provisioning 'correctly'.
There are also similar fields where the same could be done to remove cyclic dependencies: for example, when provisioning a Function/App Service with SSO, the azuread_application
web.redirect_uris
needs to be set after the Function/App Service has been provisioned and the service's URL is known, but I can't cleanly provision the Function/App Service until I have the azuread_application.auth.application_id
.
Hi @jlaundry, thanks for offering to work on this. That is fairly consistent with what we have in mind for a solution, and it's on our backlog to architect across the azuread_application resource - as it needs careful consideration about which attributes could comprise a separate resource, whether those resources are singular or plural, and ensuring that in all cases potential SDK bugs do not cause a regression (this alas is complex and depends on the circular interaction between the API behavior, Hamilton SDK limitations and Terraform Plugin SDK known issues).
Off hand if I were to guess, I'd say that this probably warrants a pluralized resource pretty much as you've suggested. If you'd like to work on it, a PR would be warmly welcomed :)
Same problem when in the "required_resource_access" block "resource_app_id" should be specified as self "application_id".
Thanks for the discourse on this issue. As I've mentioned previously, there are impracticalities around changing the default value behavior for the identifier_uris
field in the existing application resource. We've decided to create a new set of resources for applications that approach application management somewhat differently. These will be smaller in scope, allowing practitioners to select the functionality they want and obtain additional functionality not afforded by the existing resource. Accordingly I've linked this issue to #1214 and it will be closed as resolved shortly.
Our recommendation will be to consider adopting the soon-to-be-introduced azuread_application_registration
and azuread_application_identifier_uri
resources. These will permit referencing the client ID due to the separation in the dependency graph, and will open up additional scenarios to be managed via Terraform, without resorting to two-step applies, ignore_changes
, or similar workarounds.
For clarity this is complete solution as of today:
terraform {
# <-----... Add other required fields here ... ------->
required_providers {
# https://registry.terraform.io/providers/hashicorp/azuread/
azuread = {
source = "hashicorp/azuread"
version = "2.45.0"
}
}
}
resource "azuread_application" "app" {
# <-----... Add other required fields here ... ------->
lifecycle {
ignore_changes = [
# This parameter is managed by `azuread_application_identifier_uri`.
# Details: https://github.com/hashicorp/terraform-provider-azuread/issues/428#issuecomment-1788737766
identifier_uris,
]
}
}
# If you need more than one URI, create more resources like this or use `for_each`
resource "azuread_application_identifier_uri" "app" {
application_id = azuread_application.app.id
identifier_uri = "api://${azuread_application.app.client_id}"
}
I just tested this approach by:
terraform apply
terraform apply
Result:
As you see ignore_changes
are still required (not a big deal) to avoid the configuration drift on azuread_application
, but two step apply is not needed this way (which is great!).
Hi,
I used @asinitson 's solution to assign the application's client_id
to the identifier_uri
. This works correctly for creating and cleaning up.
If I then also create an azuread_service_principal
for my application, the creation works, but the clean up of the resources fails with an error unexpected status 404 with OData error: Request_ResourceNotFound
.
The reproduction example:
terraform {
required_version = ">= 1.6"
required_providers {
azuread = {
source = "hashicorp/azuread"
version = "~>2.47.0"
}
}
}
resource "azuread_application" "main" {
display_name = "test"
lifecycle {
ignore_changes = [
identifier_uris,
]
}
}
resource "azuread_service_principal" "main" {
client_id = azuread_application.main.client_id
}
resource "azuread_application_identifier_uri" "main_client_id" {
application_id = azuread_application.main.id
identifier_uri = "api://${azuread_application.main.client_id}"
}
The error log:
$ terraform apply
azuread_application_identifier_uri.main_client_id: Refreshing state... [id=/applications/b27be14c-e181-49e5-b17e-db9eac6fce01/identifierUris/YXBpOi8vZjI5MmIwOGYtMjg0My00NDkyLTlkYzctY2Q4ZjVkM2ZkYWU2]
azuread_service_principal.main: Refreshing state... [id=ad2049b5-1c61-44d8-a2aa-d6ff15f289c4]
azuread_application.main: Refreshing state... [id=/applications/b27be14c-e181-49e5-b17e-db9eac6fce01]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# azuread_application.main will be destroyed
# (because azuread_application.main is not in configuration)
- resource "azuread_application" "main" {
- app_role_ids = {} -> null
- application_id = "f292b08f-2843-4492-9dc7-cd8f5d3fdae6" -> null
- client_id = "f292b08f-2843-4492-9dc7-cd8f5d3fdae6" -> null
- device_only_auth_enabled = false -> null
- disabled_by_microsoft = "<nil>" -> null
- display_name = "test" -> null
- fallback_public_client_enabled = false -> null
- group_membership_claims = [] -> null
- id = "/applications/b27be14c-e181-49e5-b17e-db9eac6fce01" -> null
- identifier_uris = [
- "api://f292b08f-2843-4492-9dc7-cd8f5d3fdae6",
] -> null
- oauth2_permission_scope_ids = {} -> null
- oauth2_post_response_required = false -> null
- object_id = "b27be14c-e181-49e5-b17e-db9eac6fce01" -> null
- owners = [] -> null
- prevent_duplicate_names = false -> null
- publisher_domain = "my-lab.org" -> null
- sign_in_audience = "AzureADMyOrg" -> null
- tags = [] -> null
- api {
- known_client_applications = [] -> null
- mapped_claims_enabled = false -> null
- requested_access_token_version = 1 -> null
}
- feature_tags {
- custom_single_sign_on = false -> null
- enterprise = false -> null
- gallery = false -> null
- hide = false -> null
}
- optional_claims {
}
- public_client {
- redirect_uris = [] -> null
}
- single_page_application {
- redirect_uris = [] -> null
}
- web {
- redirect_uris = [] -> null
- implicit_grant {
- access_token_issuance_enabled = false -> null
- id_token_issuance_enabled = false -> null
}
}
}
# azuread_application_identifier_uri.main_client_id will be destroyed
# (because azuread_application_identifier_uri.main_client_id is not in configuration)
- resource "azuread_application_identifier_uri" "main_client_id" {
- application_id = "/applications/b27be14c-e181-49e5-b17e-db9eac6fce01" -> null
- id = "/applications/b27be14c-e181-49e5-b17e-db9eac6fce01/identifierUris/YXBpOi8vZjI5MmIwOGYtMjg0My00NDkyLTlkYzctY2Q4ZjVkM2ZkYWU2" -> null
- identifier_uri = "api://f292b08f-2843-4492-9dc7-cd8f5d3fdae6" -> null
}
# azuread_service_principal.main will be destroyed
# (because azuread_service_principal.main is not in configuration)
- resource "azuread_service_principal" "main" {
- account_enabled = true -> null
- alternative_names = [] -> null
- app_role_assignment_required = false -> null
- app_role_ids = {} -> null
- app_roles = [] -> null
- application_id = "f292b08f-2843-4492-9dc7-cd8f5d3fdae6" -> null
- application_tenant_id = "8c450195-9397-4658-8296-0361b9220f6b" -> null
- client_id = "f292b08f-2843-4492-9dc7-cd8f5d3fdae6" -> null
- display_name = "test" -> null
- id = "ad2049b5-1c61-44d8-a2aa-d6ff15f289c4" -> null
- notification_email_addresses = [] -> null
- oauth2_permission_scope_ids = {} -> null
- oauth2_permission_scopes = [] -> null
- object_id = "ad2049b5-1c61-44d8-a2aa-d6ff15f289c4" -> null
- owners = [] -> null
- redirect_uris = [] -> null
- service_principal_names = [
- "api://f292b08f-2843-4492-9dc7-cd8f5d3fdae6",
] -> null
- sign_in_audience = "AzureADMyOrg" -> null
- tags = [] -> null
- type = "Application" -> null
- feature_tags {
- custom_single_sign_on = false -> null
- enterprise = false -> null
- gallery = false -> null
- hide = false -> null
}
- features {
- custom_single_sign_on_app = false -> null
- enterprise_application = false -> null
- gallery_application = false -> null
- visible_to_users = true -> null
}
- saml_single_sign_on {}
}
Plan: 0 to add, 0 to change, 3 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
azuread_application_identifier_uri.main_client_id: Destroying... [id=/applications/b27be14c-e181-49e5-b17e-db9eac6fce01/identifierUris/YXBpOi8vZjI5MmIwOGYtMjg0My00NDkyLTlkYzctY2Q4ZjVkM2ZkYWU2]
azuread_service_principal.main: Destroying... [id=ad2049b5-1c61-44d8-a2aa-d6ff15f289c4]
azuread_service_principal.main: Still destroying... [id=ad2049b5-1c61-44d8-a2aa-d6ff15f289c4, 10s elapsed]
azuread_service_principal.main: Still destroying... [id=ad2049b5-1c61-44d8-a2aa-d6ff15f289c4, 20s elapsed]
azuread_service_principal.main: Destruction complete after 20s
╷
│ Error: deleting Application IdentifierUri (Application ID: "b27be14c-e181-49e5-b17e-db9eac6fce01", IdentifierUri ID: "YXBpOi8vZjI5MmIwOGYtMjg0My00NDkyLTlkYzctY2Q4ZjVkM2ZkYWU2"): ApplicationsClient.BaseClient.Patch(): unexpected status 404 with OData error: Request_ResourceNotFound: Resource 'Application_b27be14c-e181-49e5-b17e-db9eac6fce01' does not exist or one of its queried reference-property objects are not present.
│
│ deleting Application IdentifierUri (Application ID: "b27be14c-e181-49e5-b17e-db9eac6fce01", IdentifierUri ID:
│ "YXBpOi8vZjI5MmIwOGYtMjg0My00NDkyLTlkYzctY2Q4ZjVkM2ZkYWU2"): ApplicationsClient.BaseClient.Patch(): unexpected status 404 with OData error:
│ Request_ResourceNotFound: Resource 'Application_b27be14c-e181-49e5-b17e-db9eac6fce01' does not exist or one of its queried reference-property
│ objects are not present.
If I then re-run terraform apply
, the resources are correctly cleaned up.
Any ideas about that?
application_id = azuread_application.main.id
sets implicit dependency on azuread_application.main
, so I would expect it to be destroyed before azuread_application.main
is destroyed if you use this piece of code:
resource "azuread_application_identifier_uri" "main_client_id" {
application_id = azuread_application.main.id
identifier_uri = "api://${azuread_application.main.client_id}"
}
@joachimBurket: Maybe setting explicit dependency also on a service principal will help? Something like this (untested!):
resource "azuread_application_identifier_uri" "main_client_id" {
application_id = azuread_application.main.id
identifier_uri = "api://${azuread_application.main.client_id}"
depends_on = [azuread_service_principal.main]
}
From the error message:
or one of its queried reference-property objects are not present.
Maybe service principal gets queried in this call which causes the dependency.
This works like a charm! 🎉
I'm quite new to Terraform and didn't know about this depends_on
argument.
Thanks for your help @asinitson !
Community Note
Description
When creating an Azure AD Application in the Azure Portal, the
identifier_uris
field (Application ID URI) defaults toapi://<application_id>
, which is required for using an application to expose APIs. This causes a circular dependency to set it explicitly using the terraform resource, as we don't know the application_id until the application is created.Proposal is to default the (Application ID URI)
identifier_uris
field toapi://<application_id>
if the attribute isn't set in the resource. If it is set, even to emptyidentifier_uris = []
it will not default toapi://....
.New or Affected Resource(s)