Skip to content

azuread_app_role_assignment - fix assignments orphaned by the create retry - #1896

Open
charlesbalderson wants to merge 3 commits into
hashicorp:mainfrom
charlesbalderson:b-app-role-assignment-duplicate-on-retry
Open

azuread_app_role_assignment - fix assignments orphaned by the create retry#1896
charlesbalderson wants to merge 3 commits into
hashicorp:mainfrom
charlesbalderson:b-app-role-assignment-duplicate-on-retry

Conversation

@charlesbalderson

Copy link
Copy Markdown

Description

azuread_app_role_assignment can create an assignment in Entra and then fail, leaving it
unmanaged and the configuration permanently unappliable.

The create request carries a RetryFunc that replays the request when Graph reports the service
principal or app role as not found, or answers Not a valid reference update. Both happen while a
newly created object replicates, so the retry is doing useful work.

The problem is that the request is a POST and is not idempotent. When an attempt has actually
landed, the replay is answered with:

Error: Could not create app role assignment

unexpected status 400 (400 Bad Request) with error: Request_BadRequest:
Permission being assigned already exists on the object

That is returned before d.SetId(), so the assignment exists in Entra but not in state. Because
the assignment ID is server generated, there is nothing the practitioner can import without
querying Graph by hand. Every subsequent apply plans the same create and fails identically.

This reproduces reliably when the app role and the assignment are created in the same apply, which
is the common case for azuread_application_app_role plus a for_each over a set of groups. We hit
it building ephemeral environments: one assignment out of ~25 is orphaned, and that environment can
never be applied again.

Fixes #763

Changes

recover from duplicate assignments created by the create retry — the actual fix.

RetryFunc now records whether it fired. On Permission being assigned already exists on the object the assignment is looked up on the resource service principal, and:

  • if a retry happened, the provider created it, so it is adopted
  • if no retry happened, it pre-dates this request, so tf.ImportAsExistsDiag is returned — now
    with a usable ID, which it previously was not possible to obtain

The lookup pages appRoleAssignedTo and matches locally, since Graph does not support $filter on
appRoleId or principalId. It only runs on the error path.

Adopting is deliberately limited to the case the provider caused. A blanket adopt-on-conflict would
silently take over assignments made outside Terraform, which is not how this provider behaves
elsewhere.

wait for the app role to replicate before assigning it — prevention, separable.

Waits for the app role to appear in the resource service principal's appRoles before sending the
request, using the existing consistency helper, in the spirit of #1844 and #1845. Bounded at one
minute and non-fatal on purpose: an app_role_id that is simply wrong should be reported by the
API, not as a timeout here. Skipped for the default all-zeros role, which never appears in
appRoles.

This is the second commit and can be dropped without affecting the fix.

add a requiresImport acceptance test

Testing

make fmtcheck        # pass
make test            # pass
golangci-lint run    # clean
scripts/terrafmt-acctests.sh  # clean

The new TestAccAppRoleAssignment_requiresImport covers the branch where the assignment already
existed and no retry occurred.

I could not construct a deterministic test for the adopt branch — it needs Graph to accept a write
and then report it as missing, which I have no way to force. It is reached by reasoning about the
retry path rather than by a repro, and I would rather flag that than imply coverage that is not
there. Happy to take a suggestion if there is an established way to fake the transport in this
repo.

…eated by the create retry

The create request carries a RetryFunc that replays the request when Graph reports the
service principal or app role as not found, or answers "Not a valid reference update",
both of which happen while a newly created object replicates.

That request is a POST and is not idempotent. When an attempt has actually landed, the
replay is answered with 400 Request_BadRequest "Permission being assigned already exists
on the object", which the provider surfaced as a hard error before setting an ID. The
assignment then exists in Entra but not in state, and because the assignment ID is server
generated there is nothing for the practitioner to import without querying Graph by hand.
Every subsequent apply plans the same create and fails the same way, so the configuration
cannot be applied again.

Track whether the retry fired, and on that error look the assignment up. If a retry
happened the provider created it, so adopt it. If no retry happened the assignment
pre-dates this request, so return the usual "already exists" diagnostic - now with a
usable ID to import.

Fixes hashicorp#763
…fore assigning it

Assignments are commonly created in the same apply as the app role they reference, and a
role that Graph has only just accepted is not yet visible on every replica. That is what
puts the create request onto its retry path in the first place.

Wait for the role to appear in the resource service principal's appRoles before sending
the request. The wait is bounded and non-fatal: an app_role_id that is genuinely wrong
should be reported by the API, not as a timeout here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Azuread_app_role_assignment: Provider produced inconsistent result after apply

1 participant