Global Administrator was active, yet one application-filter plan still failed with error 2075. The missing capability was reading custom security attribute definitions. That failure became the most useful part of the migration: finding the roles and Graph permissions the work actually required.

I moved 19 standalone policies into seven production plans with 18 enabled rules. Equivalent classic policies and plan rules needed the same tested roles and Graph scopes. All 45 scope comparisons matched without Global Reader. Ordinary writes worked with Conditional Access Administrator alone; the custom security attribute filter also needed Attribute Definition Reader.

The migration exposed differences elsewhere: a saved client default broadened a rule, shared conditions changed how targeting had to be compared, and consolidation needed to preserve a concurrent lab exception.

My earlier article covers the model and Maester's visibility into it. This field report covers the scripted Graph migration, its permission tests and the checks around cutover. Policy plans were in preview during the tests.

The starting point and the final shape

The source configuration contained 18 enabled policies and one in report-only. It covered internal users, external users, privileged B2B administrators, geographic restrictions, legacy clients, authentication contexts and application-specific authentication requirements. A separate lab plan already existed with two disabled rules.

This live identity tenant does not operate a managed device estate. Device-compliance controls are configured, and I checked their configuration and applicability. I did not validate compliance delivery across an Intune-managed fleet.

I exported the policies, plans, rules, named locations and authentication strengths. A mapping journal connected each original policy to its replacement rule and recorded the original state. That made each later comparison traceable to a specific source.

Migration and consolidation were separate changes:

  • Before migration: 19 standalone policies; 18 enabled and one report-only.
  • Initial cutover: Eight plans with 19 rules; 18 enabled and one report-only.
  • After consolidation: Seven plans with 18 enabled rules.

The 19 original policies remain present and disabled for rollback. Lab objects are outside the production totals.

One change was intentional from the start. The report-only sign-in risk policy required MFA and a compliant device. I removed device compliance from its replacement and retained MFA. The separate user risk policy already required MFA and password change without device compliance. Recording that decision prevented the comparison from treating an approved change as a migration defect.

The attribute filter that Global Administrator could not copy

The Graph client had delegated Policy.ReadWrite.ConditionalAccess consent, but the user had only Global Reader active. Reads worked; plan creation returned HTTP 403. Activating Global Administrator and renewing Graph authentication allowed ordinary plan creation. Microsoft documents Conditional Access Administrator as the least privileged built-in role for this operation. Create a plan.

The application-filter plan failed with a different error:

2075: Insufficient roles to read custom security attributes.

That policy selected applications through a custom security attribute. Global Administrator was already active. Activating Attribute Assignment Administrator and renewing authentication still produced the error. With Attribute Definition Administrator also active and a fresh Graph session, creation succeeded.

The distinction matters when preparing the change window. Microsoft's application-filter guidance specifies Conditional Access Administrator plus Attribute Definition Reader for creating a filtered policy. Global Administrator has no custom security attribute permissions by default. Reading attribute definitions and assigning attribute values are different tasks. Application filters and roles.

Disabled test objects let me isolate the narrower role combination: Conditional Access Administrator and Attribute Definition Reader.

The permission delta from standalone policies

I compared equivalent classic policies and plan rules under the same roles and isolated API scopes. Five isolated delegated clients supplied different scope combinations. Each performed a direct GET, PATCH and POST against three equivalent configurations: All resources, one valid application ID, and the existing custom security attribute application filter. The PATCH requests resubmitted the same conditions and MFA controls; all test objects were disabled.

Five scope profiles across three resource selections and three operations produced 90 requests: 45 paired comparisons. Only Conditional Access Administrator and Attribute Definition Reader were active. Global Reader was PIM-eligible and inactive. Setup roles were deactivated before the clients authenticated. Repeating the matrix without Global Reader returned the same 45 status pairs as the baseline with it.

  • Roles: CA Administrator for ordinary writes; Attribute Definition Reader for attribute filters. The same combination worked for policies and plans, without Global Reader.
  • Graph permissions: The same requirements in all 45 scope comparisons. Creating a concrete app-ID selection also needed application-read permission.
  • Errors: A missing definition-reader role returned 403 for classic policies and 400/2075 for plans. Both denied the write.
  • Targeting: Classic policies define their own targeting and exclusions. Plans share conditions across independently evaluated rules.

All 45 scope comparisons returned the same status. The write scope alone failed in both models. Adding application-read permission without Policy.Read.All also failed. Policy.Read.All alone allowed the tested reads and denied writes. With the two policy scopes, reapplying an unchanged application-ID selection by PATCH succeeded in both models, even though creating that selection required Application.Read.All. I did not test changing the application ID on an existing object, so that PATCH result is not a general update exemption.

A separate counter-test used only Conditional Access Administrator, fresh authentication and all three API scopes. Both models still returned 200 for reads, 204 for updates and 201 for creation with All resources or a concrete application ID. Reading the filtered objects also returned 200. Creating or updating them was denied in both models. Standalone policies returned 403 AccessDenied and explicitly said Attribute Definition Reader was required. Plans returned 400 BadRequest with 2075: Insufficient roles to read custom security attributes. Repeating the classic-policy calls through /beta still returned 403 for the writes, ruling out a simple v1.0-versus-beta explanation for this observed difference.

The measured permission delta was therefore no additional role or scope requirement in these cases. The diagnostic delta was real: automation needs to recognize a plan's 400/2075 as an authorization failure, even though the equivalent classic call reports 403. This does not establish identical behavior for every CA feature, custom role or app-only client.

Effective role membership was recorded before each scope profile and the single-role counter-test. Global Reader was not needed for these CA calls.

The role result agrees with Microsoft's guidance for standalone policy creation and application filters. Global Administrator was not the least privileged built-in role for the old policy workflow either.

The working role and scope profile

Both authorization layers matter: an active Entra role authorizes the user, while delegated Graph permissions authorize the client. A role does not replace a missing API scope, and consenting to a scope does not activate an Entra role. For this workflow, the two layers break down as follows:

  • All resources: Conditional Access Administrator with Policy.Read.All and Policy.ReadWrite.ConditionalAccess.
  • Concrete application-ID creation: Conditional Access Administrator with the two policy scopes plus Application.Read.All.
  • Existing attribute filter: Conditional Access Administrator and Attribute Definition Reader, with the two policy scopes.
  • Sign-in logs with CA results: A supported log-reader role such as Reports Reader, plus Conditional Access Administrator for CA data; AuditLog.Read.All and Policy.Read.ConditionalAccess.

The filter requires access to existing definitions. Attribute Assignment Administrator serves the separate task of assigning attribute values to objects, while Attribute Definition Administrator manages the definitions themselves. Neither administrator role was required for the tested migration writes. Microsoft documents these distinctions in its application-filter role guidance.

A separate test copied all seven production plans and 18 rules into disabled objects. Saved conditions and controls matched; updating and deleting the copies succeeded. Global Reader was active for that full-copy test. The paired matrix verified the three resource-selection patterns without it.

The tested CA clients needed no Directory.Read.All or attribute-write scopes. The sign-in reader used AuditLog.Read.All and Policy.Read.ConditionalAccess, with Global Reader active. Microsoft's sign-in API documentation lists Reports Reader among the supported log-reader roles. The role-isolation tests covered CA configuration calls, not tenant-wide sign-in analysis.

Keeping setup permissions out of the test

The delegated clients were isolated public-client applications without secrets. Each had a principal-specific consent for exactly its scope profile. That avoided the existing PowerShell app's accumulated consent making a narrow request appear to work. Microsoft also documents an additional permission requirement for Conditional Access writes.

A separate setup session used Privileged Role Administrator for role assignments and Application Developer for app registration. Both roles were inactive before the authorization tests. The additional SDK permission for managing active role schedules was withdrawn during cleanup.

Cleanup removed all 20 disabled test objects—ten classic policies and ten plans—and all five test applications with their service principals. Every existing plan, rule and standalone policy matched the pre-test snapshot. Temporary role activations ended; Global Reader retained permanent PIM eligibility. The separate baseline and full-copy tests have their own cleanup records, including concurrent lab changes.

Reading the failures correctly

These errors came from different parts of the workflow. I would check the response body and the active role set before changing a request or adding permissions.

  • 400 with inner error 2075 on a plan write: Check Attribute Definition Reader activation and renew the Graph session. The filtered write could not read attribute definitions.
  • 403 when creating a valid application-ID selection with the two policy scopes: Check the isolated client's granted Application.Read.All scope. Both APIs needed it for that creation.
  • 400 with error 2018: Review the user assignment. All users and the explicit guest/external selector could not be combined; this migration preserved them on separate rules.

A saved default changed the legacy client scope

The source legacy authentication policy selected exchangeActiveSync and other. Both values went into the plan's shared conditions. I initially omitted client application types from the child rule, expecting the shared selection to carry the scope.

The saved child rule returned:

{
  "clientAppTypes": ["all"]
}

Plans combine shared and rule conditions additively. In this configuration, the effective client application selection included all. That was a broader target than the original legacy-only block. Shared conditions and rules.

I explicitly wrote exchangeActiveSync and other onto the rule as well. Reading it back and recomputing its effective conditions then matched the source. Subsequent create payloads explicitly included the intended rule-level client application types.

This was the most consequential configuration finding in the run. A successful create response had saved an object that needed correction before use. The disabled staging state gave the comparison a chance to catch it. I did not exercise the portal migration wizard, so this observation should stay attached to the scripted create requests.

The same comparison also accepted a harmless normalization. After removing device compliance from the sign-in risk rule, only MFA remained. Graph saved the grant operator as OR, although the request contained AND. With one control, both expressions require MFA. Comparing meaning allowed that change while rejecting the expanded client scope.

The CAE question depended on the existing location type

The preview documentation prompted an obvious question before cutover: would moving the geographic policies lose immediate location enforcement?

Microsoft states that plans targeting users with a location condition receive one-hour access tokens without an instant IP enforcement check during preview. The four geographic policies in this tenant used country-based named locations. Plan runtime enforcement.

Country and region conditions already fall outside CAE's immediate location enforcement for standalone policies. For these particular rules, the preview note therefore did not establish an additional loss from the migration. An IP-based named location would require a different assessment. CAE supported location policies.

That is a narrower finding than saying plans have no CAE. CAE also handles critical events such as account disablement and explicit session revocation in supported scenarios. I assessed the location limitation against the location conditions actually configured in this tenant.

Three checks before retiring the originals

I needed answers to three different questions. Did the saved rule have the intended configuration? Did it apply to the same scenarios? Did real sign-in records reflect the cutover?

The first check used the condition-merging and rule-projection functions from the tested Maester contribution. It compared effective targeting, exclusions, grant controls, session controls and states. Authentication strengths were compared by their IDs. This was where the legacy client mismatch surfaced.

The second check used What If. The initial matrix covered 56 scenarios across all 19 policy-to-rule pairs: 1,064 applicability comparisons. It included member and emergency access identities, Windows and Android, different clients, applications and countries, independent user and sign-in risk levels, authentication contexts and simulated external-user types. Each pair had to agree on applicability and analysis reasons. All 1,064 comparisons passed before cutover and again afterward against the saved expectations.

The third check used actual sign-in events. After the respective cutovers, successful events contained all 19 replacement rule IDs across 259 rule observations. The originals were recorded as notEnabled; replacements showed their intended state and a successful or non-applicable result. The internal MFA replacement recorded success while its original was notEnabled.

These checks answer different questions. What If tests applicability. Saved controls establish what an applicable rule requires. A non-applicable sign-in entry does not demonstrate that someone satisfied a device or authentication-strength requirement. The live results provided a sign-in smoke test alongside the broader scenario comparisons.

The initial sequence was disabled rules, configuration checks, report-only, then enforcement for the rules intended to be enabled. I enabled and validated replacements before disabling their sources. The originally report-only rule retained that state at this stage. Microsoft's migration guidance describes the same staged transition.

Sign-in evidence lagged behind configuration changes. Early records sometimes still showed notEnabled after the rule's report-only update had been saved. I waited for newer events reflecting the requested state before advancing. That delay belongs in the change window.

Consolidation preserved the assignment boundaries

Once the initial migration was validated, I combined the internal and guest baselines into one Identity Protection plan. Their common group exclusions moved into shared conditions. MFA and risk assignments kept their original selectors on the individual rules.

I also tried to reduce the two MFA assignments to one rule. Combining All users with the existing explicit guest/external selector returned HTTP 400:

2018: Users assignment value 'None', 'All' or 'GuestsOrExternalUsers' cannot be combined with guestOrExternalUsers.

This was a schema constraint. Microsoft's All users definition includes B2B guests; the error does not show otherwise. I kept two MFA rules in the common plan to preserve the existing external-user categories and tenant selection without assuming a different assignment was equivalent. User assignments.

The report-only sign-in risk rule could be retired. Once its device-compliance control was removed, its remaining MFA requirement was already covered by the enabled internal MFA baseline for the same users and resources. Removing that diagnostic rule reduced the production total from 19 to 18 rules.

Other distinctions remained useful. The geographic rules had different exceptions. The guest-risk and travel-risk blocks required both elevated user risk and elevated sign-in risk. Their new names explicitly preserve that AND, so an administrator does not mistake either rule for a block triggered by either risk signal alone.

A concurrent lab exception became another test case

During consolidation, the comparison detected the same user exclusion newly added to both the source and replacement internal MFA rules by a parallel lab workflow.

Reapplying the earlier export would have overwritten that change. Moving it into shared conditions would have excluded the lab identity from the risk rules too. I preserved the exclusion on the internal MFA rule, carried it into the rollback payload and added a dedicated scenario for that identity.

The consolidation matrix therefore grew to 57 scenarios across 18 rules. That produced 1,026 applicability comparisons per phase, plus 57 comparisons of the complete applicable grant and session control sets. Both sets passed before and after switching away from the superseded baselines.

The final runtime check observed all four new Identity Protection rule IDs across 124 rule observations. The newer successful events reflected the cutover with the superseded baseline rules disabled. After validation and a configuration backup, I removed the two superseded baseline plans. The 19 original standalone policies remain disabled and available.

Seven plans with names that explain the controls

The final production structure uses numbered security objectives at plan level. Rules add an identifier, their scope or condition, and the required control.

  • CA | 10 Identity Protection: 4 enabled rules.
  • CA | 20 Privileged Access: 1 enabled rule.
  • CA | 30 Geographic Access: 4 enabled rules.
  • CA | 40 Legacy Authentication: 1 enabled rule.
  • CA | 50 Authentication Contexts: 4 enabled rules.
  • CA | 60 Demo Application Security: 2 enabled rules.
  • CA | 70 Application Authentication: 2 enabled rules.

For example, ID-01 | Internal users | Require MFA explains the baseline immediately. GEO-01 | Sanctioned countries | Block access explains the location control without requiring someone to remember an old policy number.

The two Maester checks used in this run also passed after consolidation: MT.1001 for the presence of device-compliance requirements in the configuration and MT.1275 for recorded plan owners. MT.1001 passing does not show that this tenant manages devices or that an endpoint completed a compliance assessment. The owner check confirms metadata presence; assigning a plan owner does not grant administrative permissions. Plan details.

What I would carry into the next migration

I would repeat the disabled staging, saved-object comparison and scenario checks. They caught the expanded client scope before enforcement. The role profile is equally clear for the tested writes: Conditional Access Administrator, with Attribute Definition Reader for the attribute filter.

For consolidation, I would start with common targeting and exclusions, then justify each rule removal through its assignments and controls. This tenant went from 19 standalone policies to seven production plans, but it still has 18 distinct enforcement rules. Shared administration is the larger gain: related controls now sit together, and common exclusions have one place to be maintained.

I kept two rollback routes. Each restores the earlier controls before disabling their replacements; they restore different stages of the change.

  • Return to standalone policies: Restore the 19 retained policies in their recorded enabled or report-only states, reconciling later approved exceptions first. Then disable the current replacement rules using the post-consolidation mapping.
  • Restore pre-consolidation baselines: Restore the saved plans and rule states, including the preserved lab exclusion. Recreate deleted plans and record their new IDs before disabling the consolidated replacement baseline.

Re-enabling the originals is a rollback to their recorded controls, including the old report-only sign-in risk policy. Recreating a deleted intermediate plan is a separate operation and produces new IDs. After consolidation, rollback must use the current rule mapping and account for recreated IDs. These routes were prepared from the saved configuration; a full rollback drill was not part of this run.

The next change to this tenant starts from named security objectives, explicit rule scope and a scenario matrix that already includes the lab exception. That is the result I wanted from the consolidation: less repeated configuration, with enough evidence to explain why each remaining rule is there.

← All articles