Move ADR certificate management to the certificate-authority model - #447
Move ADR certificate management to the certificate-authority model#447Ewerton Scaboro da Silva (ewertons) wants to merge 29 commits into
Conversation
Certificate management provisioning no longer works: the ADR object model it targeted was public preview and has been replaced. A certificate policy used to hang off a namespace credential and be referenced from an enrollment by a single name. It now hangs off an issuing CA, and an enrollment references it by three names that must travel together. The hub and DPS used to be pointed at a namespace and a shared user-assigned identity when they were created; the relationship is now expressed on the namespace, as endpoints linked to it afterwards, with each resource authenticating as its own system-assigned identity. - Create the root -> issuing CA -> certificate policy chain, after linking, so ADR has a hub to sync the issuing CA certificate to. A policy created under a root is rejected with PolicyRequiresIssuingCa. - Link the hub and DPS to the namespace and wait for the endpoints, retrying while the namespace identity's grants replicate and healing a namespace left Failed by an otherwise successful link. - Write enrollments over the DPS service API, carrying the three policy names. The CLI cannot express them: it only ever offered --credential-policy. - Push the linked namespace into the DPS data plane with a tags-only update. Committing a link does not push scale-unit configuration, so without it every enrollment write fails with errorCode 400004. - Skip linked-hub creation under certificate management, where ADR chooses the provisioning targets and the DPS's own list is read-only. - Read linked hubs from the namespace endpoints. They were read from the DPS, which is captured before the hub is attached, so the list was always empty. - Drop the extension version pin, the credential sync, the Contributor grant to the IoT Hub first-party application and the Onboarding role, all of which belonged to the previous model. TestEnvironmentInfo.AzureAdrPolicyName is replaced by AdrPolicy, and the generated test configuration exports the namespace and certificate authority names alongside ADR_CERT_MGMT_POLICY_NAME. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The ADR namespace linking payload appears to use the IoT Hub endpoint type for the DPS endpoint, which is likely to break provisioning links at runtime.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR updates the PowerShell provisioning/test module to use the post-public-preview Azure Device Registry (ADR) certificate-authority model for certificate management, replacing the previous CLI-driven ADR surface area with direct ARM + DPS service-API calls.
Changes:
- Removes the azure-iot CLI extension version pin and routes ADR operations through
az rest(ARM) instead ofaz iot adr. - Adds ADR provisioning/linking helpers (namespace creation, endpoint linking, CA chain + policy creation) and waits/polling utilities.
- Writes DPS enrollments via the DPS service REST API to carry the three-part ADR policy reference (namespace/CA/policy).
File summaries
| File | Description |
|---|---|
| scripts/Azure.Iot.Sdk.Test.psm1 | Reworks ADR certificate management provisioning and DPS enrollment creation to match the new certificate-authority model. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The DPS provisioning endpoint was sent with the IoT Hub resource type, which mis-models the link. Three further problems in the same function, found while confirming that one: - The hub messaging endpoint was missing its provisioning availability, so it was linked but not offered as a provisioning target. - The link was submitted as a properties-only update. The saga starts on a namespace write carrying location and identity. - Reconciling a namespace left Failed re-sent the endpoints, which is rejected as immutable, and judged the result on the first read, which still reports Failed for a while. It is now a tags-only update, polled to Succeeded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The updated E2E config generators dereference TestEnvInfo.AdrPolicy.* unconditionally, which will throw when certificate management isn’t enabled (AdrPolicy is null), breaking config generation for non-ADR environments.
Review details
Suppressed comments (7)
Previously missed (2) — in code that hasn't changed since the last review.
scripts/Azure.Iot.Sdk.Test.psm1:1577
- The ADR/DPS
api-versionconstants are future-dated relative to the current date (e.g.2026-11-02-preview). If these versions aren't available in the target cloud/subscription yet, everyaz restcall will fail with an invalid/unsupported api-version error. Consider allowing these to be overridden via environment variables (or module parameters) so runs can be unblocked without code changes.
scripts/Azure.Iot.Sdk.Test.psm1:3150 TestEnvInfo.AdrPolicycan be$nullwhen certificate management wasn't enabled (e.g., environments created without-EnableCertificateManagementor JSON loaded from older configs). These lines dereference.NamespaceName/etc unconditionally, which will throw and prevent config generation even for non-ADR runs. Consider emitting empty strings whenAdrPolicyis null (matching the previous string-based behavior).
This issue also appears in the following locations of the same file:
- line 3179
- line 3256
- line 3278
- line 3340
- line 3370
scripts/Azure.Iot.Sdk.Test.psm1:3181
- Same null-dereference issue as the PowerShell block above: this bash config generation path also assumes
TestEnvInfo.AdrPolicyis non-null and will throw when it isn't. Emit empty strings whenAdrPolicyis null to keep config generation working for non-ADR environments.
"export ADR_CERT_MGMT_NAMESPACE_NAME=`"$($TestEnvInfo.AdrPolicy.NamespaceName)`""
"export ADR_CERT_MGMT_CERTIFICATE_AUTHORITY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificateAuthorityName)`""
"export ADR_CERT_MGMT_POLICY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificatePolicyName)`""
scripts/Azure.Iot.Sdk.Test.psm1:3258
New-AzIotNetSDKE2ETestConfigdereferencesTestEnvInfo.AdrPolicy.*unconditionally. If the environment was created without certificate management,AdrPolicyis$nulland config generation will throw. Use empty strings (or conditionally omit these variables) whenAdrPolicyis not set.
"`$env:ADR_CERT_MGMT_NAMESPACE_NAME = `"$($TestEnvInfo.AdrPolicy.NamespaceName)`""
"`$env:ADR_CERT_MGMT_CERTIFICATE_AUTHORITY_NAME = `"$($TestEnvInfo.AdrPolicy.CertificateAuthorityName)`""
"`$env:ADR_CERT_MGMT_POLICY_NAME = `"$($TestEnvInfo.AdrPolicy.CertificatePolicyName)`""
scripts/Azure.Iot.Sdk.Test.psm1:3280
- Same null-dereference issue as the PowerShell block above: the bash path assumes
TestEnvInfo.AdrPolicyis non-null and will throw otherwise. Emit empty strings whenAdrPolicyis$nullto preserve backward compatibility.
"export ADR_CERT_MGMT_NAMESPACE_NAME=`"$($TestEnvInfo.AdrPolicy.NamespaceName)`""
"export ADR_CERT_MGMT_CERTIFICATE_AUTHORITY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificateAuthorityName)`""
"export ADR_CERT_MGMT_POLICY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificatePolicyName)`""
scripts/Azure.Iot.Sdk.Test.psm1:3342
New-AzIotPythonSDKE2ETestConfigdereferencesTestEnvInfo.AdrPolicy.*unconditionally. IfAdrPolicyis$null(non-certificate-management environment), this will throw and block config generation. Consider emitting empty strings whenAdrPolicyis not present.
"`$env:ADR_CERT_MGMT_NAMESPACE_NAME = `"$($TestEnvInfo.AdrPolicy.NamespaceName)`""
"`$env:ADR_CERT_MGMT_CERTIFICATE_AUTHORITY_NAME = `"$($TestEnvInfo.AdrPolicy.CertificateAuthorityName)`""
"`$env:ADR_CERT_MGMT_POLICY_NAME = `"$($TestEnvInfo.AdrPolicy.CertificatePolicyName)`""
scripts/Azure.Iot.Sdk.Test.psm1:3372
- Same null-dereference issue as the PowerShell block above: this bash block also assumes
TestEnvInfo.AdrPolicyis non-null and will throw otherwise. Emit empty strings whenAdrPolicyis$null.
"export ADR_CERT_MGMT_NAMESPACE_NAME=`"$($TestEnvInfo.AdrPolicy.NamespaceName)`""
"export ADR_CERT_MGMT_CERTIFICATE_AUTHORITY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificateAuthorityName)`""
"export ADR_CERT_MGMT_POLICY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificatePolicyName)`""
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
The generated test configuration reads the three policy names directly. With no certificate management the reference was null, which renders as empty strings normally but throws under Set-StrictMode, so config generation could break for environments that never asked for ADR. An environment now always carries a reference; it is simply incomplete when certificate management is off. Callers already gate on IsComplete(), so nothing else changes, and the emitted values match the previous empty strings. The ADR and DPS api-versions can also be overridden from the environment, so a cloud or region where one is not registered can be unblocked without a code change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed both points from the second review in 4fdd94c. Null Future-dated api-versions. These are the versions the certificate-authority model exists at, so they cannot be lowered, but they are now overridable via |
There was a problem hiding this comment.
🔵 Needs a closer look
It makes large, high-impact changes to live Azure provisioning behavior (preview ARM api-versions, role/linking sagas, and DPS service-API enrollment writes) that require human validation in a real subscription.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
Removing the version pin means the stable azure-iot extension is installed, and 'az iot dps create' accepts identity flags only in preview builds of it. The DPS was being created with --mi-system-assigned, which the stable extension rejects as an unrecognized argument, so provisioning would have failed on exactly the version the unpinned install now selects. The DPS is created plain again and its system-assigned identity is turned on through ARM, alongside the rest of the ADR wiring, and only when certificate management is enabled. The IoT Hub keeps --mi-system-assigned, which is a core CLI flag rather than an extension one. An extension that is already installed is also updated rather than left as it is, so a long-lived agent cannot keep provisioning with a stale one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
There is a confirmed null-dereference bug in the new ARM provisioning-state wait loop, and the default ADR/DPS API versions are dated in the future and may break provisioning without overrides.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
scripts/Azure.Iot.Sdk.Test.psm1:501
- Wait-AzProvisioningState calls
(Invoke-AzRest -AllowFailure).properties.provisioningStatedirectly. When Invoke-AzRest returns $null (any transient ARM/CLI failure), this will throw a null-reference and abort the wait loop, defeating the purpose of -AllowFailure.
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
Wait-AzProvisioningState read the provisioning state straight off the result of a call it had explicitly allowed to fail. These resources are polled for minutes, and a transient ARM or CLI failure along the way says nothing about the provisioning itself, so the failed read is now treated as an unknown state for that attempt and the wait continues against its deadline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the latest review in 80b9b3f. Null dereference in Future-dated api-versions. These cannot be lowered: the certificate-authority resource tree exists only at Local verification: module parses and imports, 43 assertions pass across the unit and flow suites. Live provisioning against a subscription remains the outstanding validation, as noted in the description. |
There was a problem hiding this comment.
🔵 Needs a closer look
The defaults include future-dated api-version strings and there is at least one reliability issue around masking az extension update failures that should be addressed before merging.
Review details
Suppressed comments (1)
scripts/Azure.Iot.Sdk.Test.psm1:167
Install-AzureIotCliExtensionunconditionally suppressesaz extension updatestderr and then forces$LASTEXITCODE = 0. This hides real update failures (e.g., transient network issues, extension install corruption), which can later surface as confusing provisioning errors while the log claims the extension was updated. It’s safer to ignore only the known "already up to date" failure mode and fail fast for other errors.
# Already up to date is reported as a failure, so the exit code is reset rather than checked.
Write-Host "Azure IoT extension $($Extension.version) found; updating."
az extension update --name azure-iot --only-show-errors 2>$null | Out-Null
$global:LASTEXITCODE = 0
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
The update suppressed stderr and reset the exit code, on the belief that being already up to date is reported as a failure. It is not: with no newer version available the command warns and returns successfully. The suppression therefore bought nothing and hid genuine failures, which would have surfaced later as confusing provisioning errors against a log claiming the extension was updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed in 8fac417. Masked Future-dated api-versions. As above, these cannot be lowered without reverting to the retired model, and all three are overridable from the environment. |
There was a problem hiding this comment.
🔵 Needs a closer look
There are confirmed null-handling bugs in tags-only PATCH flows (which can break reconciliation/dataplane sync) and a polling robustness issue that can fail provisioning on transient ARM reads.
Review details
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
scripts/Azure.Iot.Sdk.Test.psm1:1704
Connect-AdrNamespacepolls for endpointlinkingStatefor up to 15 minutes, but a single transient failure to read the namespace viaInvoke-AzRestwill currently stop provisioning immediately. This is inconsistent withWait-AzProvisioningState, which treats transient ARM/CLI read failures as non-fatal during polling.
scripts/Azure.Iot.Sdk.Test.psm1:1732- In the namespace reconciliation path,
$Namespace.tagscan be$nullwhen the resource has no tags; calling.PSObject.Propertieson$nullwill throw and prevent recovery fromprovisioningState=Failed. Guard the tag enumeration so the reconcile PATCH always succeeds.
This issue also appears on line 1834 of the same file.
scripts/Azure.Iot.Sdk.Test.psm1:1837
Sync-DpsAdrConfigurationbuilds$Tagsby enumerating(Invoke-AzRest ...).tags.PSObject.Properties, but.tagscan be$nullwhen no tags are set, causing the best-effort sync to always fail early (and skip the PATCH that triggers the dataplane push). Guard the tag enumeration so the PATCH is attempted even with no existing tags.
try {
$Tags = @{}
(Invoke-AzRest -Url $Url).tags.PSObject.Properties | %{ $Tags[$_.Name] = $_.Value }
$Tags["AdrDataplaneSyncUtc"] = (Get-Date).ToUniversalTime().ToString("o")
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
Both tags-only updates copied the resource's current tags by piping the tags property into ForEach-Object. Piping a null still runs the body once, with a null key, so on a resource carrying no tags the copy threw. Neither the ADR namespace nor the DPS is created with tags, so this was the normal case, not an edge one: the namespace reconcile that recovers a link left Failed would throw instead of recovering, and the DPS data-plane push would fail before it was attempted, leaving every enrollment write to fail with errorCode 400004. The namespace link poll also gave up the whole run on a single failed read, over a window of up to fifteen minutes. It now treats a failed read the same way the provisioning-state wait does: an attempt spent, not a failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The DPS authenticates to the ADR namespace as itself, so it needs a system-assigned identity. Turning that on after creation does not work: DPS rejects a managed-identity PATCH with IH400158, "Patch for managed identity information is not supported". The identity flags on 'az iot dps create' are not an option either, since they exist only in preview builds of the azure-iot extension. The DPS is therefore created through ARM with the identity in the request, the way the IoT Hub already is, and the separate enable step is gone. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Linking failed with LinkableResourceNotReady on every attempt: the saga acts as the namespace identity against the hub and DPS, and as those resources' identities against the namespace, so Contributor has to be granted BOTH ways. Only one direction was, and the namespace identity also lacked Contributor on the DPS and on the namespace itself, which is what ADR's device-create runs as. Also added the two grants the flow needs once linked: the DPS identity needs IoT Hub Data Contributor to register devices, and issuing a certificate for a CSR needs an ADR data action that no built-in role carries, so a custom role is created for it and granted to the DPS identity. Linking now gets a head start after the grants are created and retries for longer, with the wait growing to a cap. Being readable is not the same as being enforced, and the previous budget of five attempts thirty seconds apart expired while replication was still in progress. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The subscription is at its custom role definition limit, so creating one failed the run outright. Matching only on the expected name meant a role granting the same action under a different name was ignored. Roles assignable at the subscription are now matched by name first and by the data action second, and a new definition is only created when neither exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Creating a custom role for the ADR certificate-issuance data action failed with RoleDefinitionLimitExceeded: the subscription is already at its limit, and a role definition outlives the resource group, so every run added to a total that cannot grow. None was needed. The built-in Azure Device Registry Contributor already carries microsoft.deviceregistry/namespaces/credentials/policies/issueCertificate/action as a data action, and the DPS identity is granted that role on the namespace anyway for enrollment writes. The custom role and its lookup are gone. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Linking reported LinkableResourceNotReady on every attempt for eleven minutes. It was not replication: an S1 hub cannot be linked to an ADR namespace at all. The previous model created a GEN2 hub for this path and that part was right, so the generation is restored. Hubs provisioned without certificate management stay on S1, which is what they were already getting by default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'az iot hub create --sku' rejects GEN2 outside preview builds of the azure-iot extension, which the removed version pin used to guarantee. The hub is now created through ARM like the DPS, which also drops the last dependency on a preview extension build. Hubs provisioned without certificate management stay on S1, as before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01-preview is not registered for IotHubs in the regions this provisions into; 2026-05-01-preview is, and is the version the preview extension used for the same resource. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A GEN2 hub carries the ADR namespace, and the identity it reaches that namespace with, in its own properties: creating one without them fails IH400913, "Invalid DeviceRegistry properties for Gen2 SKU". So the namespace and the identity have to exist before the hub, not after it, and the identity has to be user-assigned because the hub names it by resource id. Creating the hub also validates that the identity can reach the namespace, so the grant is made and waited for first. This is the part of the previous model that was right, and dropping it is what left the hub unlinkable. What has genuinely changed is the DPS, which is created plain and attached to the namespace afterwards. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The GEN2 hub passed validation and then failed asynchronously, ending at provisioningState=Failed after four minutes. A hub registers itself with the namespace as it provisions, which is a write, and its identity only held Azure Device Registry Contributor, which reads. Azure Device Registry Onboarding carries namespaces/write and is granted alongside it, as the previous model did. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
provisioningState alone does not carry a reason, so an asynchronous failure reported nothing but the word 'Failed'. The resource is now included in the error, which is where the reason is recorded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The hub reached state ActivationFailed, asynchronously and without a reason. It was being created with both a system-assigned and a user-assigned identity and with a minimum TLS version, none of which the previous model set for this path: it created a GEN2 hub carrying the user-assigned identity alone. That shape is restored. The hub therefore has no identity of its own, so the grant that named it is gone. In its place the namespace identity receives the two roles ADR documents as needed against a hub it is attached to, Contributor and IoT Hub Registry Contributor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Activating a GEN2 hub is work the IoT Hub service does on its own behalf, not as any identity we create, so its application needs access to the group holding the namespace. That grant was dropped as part of the previous model; without it the hub is accepted, activates for several minutes and then settles at state=ActivationFailed without saying why. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A GEN2 hub wired to an ADR namespace cannot be built from a plain ARM request: the request is accepted and the hub then settles at state=ActivationFailed, asynchronously and without a reason, whatever combination of identity, TLS setting and Device Registry properties it is given. The CLI call the previous model used does produce a working hub, so it is kept as it was. That call needs --sku GEN2, --ns-resource-id and --ns-identity-id, which ship only in preview builds of the azure-iot extension, so the version pin comes back. It is now pinned for the hub, not for the retired 'az iot adr' command group: the ADR resources are still created through ARM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The hub could never activate. ADR reported why once the CLI surfaced the underlying error: "Linking a messaging endpoint of type 'Microsoft.Devices/IotHubs' requires the namespace to have at least one provisioning endpoint of type 'Microsoft.Devices/provisioningServices'". A hub is a messaging endpoint and adds itself to the namespace while it activates, so the namespace has to have a DPS attached first. The DPS is therefore created and linked before the hub, and the link submits only the provisioning endpoint: submitting a messaging endpoint here would duplicate what the hub does for itself. Every endpoint also has to carry an inbound caller identity, which ADR rejects the link without. The role grants follow the same split, since each one needs its resource to exist: the namespace and DPS grants before the link, the hub grants after the hub. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The wait insisted every endpoint reach a terminal state before looking at any of them. A namespace can hold an endpoint that never carries a linkingState at all, so a failed link burned the full fifteen minutes and then reported a timeout instead of the failure, and the retry that exists for exactly that case never ran. A failure now ends the attempt at once, and the endpoint's error is reported in full rather than by code alone. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The link reported that the DPS 'could not be read and may not be ready', for eleven minutes. It reads the DPS as the namespace identity, and only the other direction was being waited for, so the attempt could start against a grant that was not yet effective. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Live-validated against the C SDK CSR e2e pipeline ( Baseline. That pipeline was already failing on every run against Fixed and confirmed by the service, in order of discovery:
Remaining blocker. Attaching the DPS as a provisioning endpoint fails 12 times over 12 minutes with:
The DPS is Open question for whoever owns the service: how is a DPS meant to be attached to an ADR namespace in this build — as a namespace provisioning endpoint as here, or still at DPS creation time? If at creation, The provisioning changes up to that point are validated. The branch is not ready to merge until the DPS attach works. |
Chasing each failure in isolation drifted this away from the harness that already provisions the same topology, and the drift is what kept the link failing. Three differences mattered: - The namespace was created without the tag the harness puts on it. ADR reads it as a feature flag; without it the linking saga takes a different path and reports the resource it is linking as unreadable, which is the LinkableResourceNotReady the link could not get past. - The hub was GEN2, carrying the namespace and a user-assigned identity in its own properties. That is the retired model. The harness creates a plain S1 hub with a system-assigned identity and attaches it like anything else. - The hub and the DPS were attached in two steps, because a GEN2 hub attaches itself. Both endpoints go in one link. The extension version pin goes with them: every ADR resource, and both resources that take part in the link, are created through ARM, so nothing needs the preview line. The grant to the IoT Hub first-party application goes too; it existed only for GEN2 activation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The hub api-version constant was removed when the hub briefly went back to being created through the CLI, and creating it through ARM again left the request with a bare 'api-version=', which ARM rejects. A URL without one is now refused before it is sent, since an empty value otherwise looks like a working call right up to the point ARM answers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two more things the reference harness does that this did not: - DPS control-plane calls go to the regional host in the canary locations. The DPS manifest carrying the ADR linking surface is registered regionally there, so a DPS created through the global host does not come up with it. - The namespace, its grants and the link are retried as one unit. The link runs as the namespace identity, and an identity that never becomes usable to it cannot be waited out; recreating the namespace mints a fresh one. The previous code exhausted its in-place retries and gave up, which is exactly where the reference recreates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Routing DPS to the regional host produced 'the Authorization header is missing'. The CLI only infers a token audience for the hosts it knows, so a regional ARM host got no header at all. Every ARM call now states it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Converged the provisioning onto the reference E2E harness for this feature rather than continuing to patch it. That was the right call and fixed several things, but the remaining blocker turns out to be environmental, not scripted. Now matched to the reference (each had been a divergence of mine):
Blocker.
So the IoT Hub resource provider in this subscription has no ADR-linking surface, which is also why the original Needed: the ADR-linking preview feature registered on Local gates green throughout. Not ready to merge until the link succeeds end to end. |
There was a problem hiding this comment.
🟡 Changes recommended
There is at least one confirmed runtime-breaking bug (missing Location parameter usage) plus a security posture regression risk (min TLS not set) that should be addressed before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 4
- Review effort level: Lite
| param( | ||
| [string]$DpsId | ||
| ) | ||
|
|
||
| $Url = "$(Get-DpsArmHost -Location $Location)$($DpsId)?api-version=$($script:DpsControlPlaneApiVersion)" |
There was a problem hiding this comment.
Correct, and a real one. $Location was never bound, so Get-DpsArmHost received an empty value and returned the global host — the data-plane push went to the wrong host in exactly the canary locations the regional host exists for, silently rather than throwing. Parameter declared in c9937bd; the call site was already passing it.
| location = $AzureLocation | ||
| sku = @{ name = "S1"; capacity = 1 } | ||
| identity = @{ type = "SystemAssigned" } | ||
| properties = @{ disableLocalAuth = $false } | ||
| } | Out-Null |
There was a problem hiding this comment.
Agreed, fixed in c9937bd. The certificate-management hub now sets minTlsVersion = "1.2" like every other hub here instead of relying on the default. It was omitted while the hub was briefly GEN2 and being minimised to isolate an activation failure; the hub is plain S1 again, so there is no reason to leave it off.
| # Azure Device Registry Onboarding: namespaces/write, namespaces/credentials/*. A hub registers | ||
| # itself with the namespace as it provisions, so its identity needs to be able to write. | ||
| $script:AdrOnboardingRoleId = "547f7f0a-69c0-4807-bd9e-0321dfb66a84" |
There was a problem hiding this comment.
Right, removed in c9937bd. It became unused when the hub stopped reaching the namespace as its own identity; nothing is granted that role any more.
| Only the DPS is attached here. A hub is a messaging endpoint and adds itself while it activates, | ||
| which is also why this has to run BEFORE the hub is created: ADR refuses a messaging endpoint | ||
| unless the namespace already has a provisioning one. |
There was a problem hiding this comment.
Correct, the description was stale, fixed in c9937bd. It described an earlier arrangement that attached the DPS first and let the hub attach itself. Both endpoints now go in one write, because ADR refuses a messaging endpoint unless the namespace also has a provisioning one, and the description says so.
…vers The push chose its ARM host from a $Location the function never declared, so the value was always empty and the call went to the global host even in the canary locations the regional host exists for. The parameter is now declared; the call site was already passing it. Also: the certificate-management hub sets the same minimum TLS version as every other hub in this module rather than relying on the default; the Device Registry Onboarding role id is gone, having become unused when the hub stopped reaching the namespace as its own identity; and Connect-AdrNamespace's description says what it now does, which is to attach the hub and the DPS in one write. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Answering the question directly: nothing in the module differs from the reference any more. The difference is the subscription. Measured, not inferred:
The owners' pipeline hardcodes that subscription, and their harness documents it as the default and as a prerequisite (Contributor + User Access Administrator on it). The pipeline identity can see exactly one subscription — the build subscription — and
So the IoT Hub resource provider there has no ADR-linking surface, which is why Ask, either one:
Test branch |
There was a problem hiding this comment.
🔵 Needs a closer look
It makes substantial changes to live Azure provisioning/linking behavior (including new ARM/DPS REST flows) and the PR notes live subscription validation is still outstanding.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
scripts/Azure.Iot.Sdk.Test.psm1:1814
- The reconcile path comment says the namespace is created without tags, but
New-AdrNamespacealways setstags = $script:AdrNamespaceTags. The guard itself is fine, but the rationale is now misleading; please update it to reflect that tags may be absent on some namespaces (e.g., pre-existing ones) rather than on those created by this module.
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
Certificate management provisioning no longer works: the ADR object model it targeted was public preview and has been replaced.
A certificate policy used to hang off a namespace credential and be referenced from an enrollment by a single name. It now hangs off an issuing CA, and an enrollment references it by three names that must travel together. The hub and DPS used to be pointed at a namespace and a shared user-assigned identity when they were created; the relationship is now expressed on the namespace, as endpoints linked to it afterwards, with each resource authenticating as its own system-assigned identity.
Changes
PolicyRequiresIssuingCa.Failedby an otherwise successful link.--credential-policy, and theaz iot adrgroup ships only in preview builds and still models the retired shape.errorCode 400004.IH409313).TestEnvironmentInfo.AzureAdrPolicyNameis replaced byAdrPolicy, and the generated test configuration exports the namespace and certificate authority names alongsideADR_CERT_MGMT_POLICY_NAME. No other file in the repository referenced the old property.Verification
Run locally against PowerShell 7.4.6; the module parses and imports cleanly, and 37 assertions pass across two suites:
TestEnvironmentInfoJSON round-trip, and the DPS SAS token checked against an independent implementation of the same HMAC-SHA256 construction.credentialPolicyName, and no retired command or flag is used.Live provisioning against a subscription has not been run and is the remaining validation: create an environment with
-EnableCertificateManagement, confirm the chain and endpoints reachSucceeded, and confirm the first enrollment write is not rejected.