Jekyll2022-06-25T15:16:28+01:00https://www.wesleytrust.com/feed.xmlUndercover hero | A tech blog by Wesley TrustA tech blog by Wesley Trust, Cloud Platform Architect, Focused on Infrastructure as Code, DevOps, Scripting and Automation.Wesley TrustAutomating Azure AD group-based licensing in a CI/CD Pipeline2021-04-26T00:00:00+01:002021-04-26T00:00:00+01:00https://www.wesleytrust.com/blog/graph-api-licences-pipeline<p>Now that we have the PowerShell to <a href="/blog/graph-api-group-licences/#getting-subscriptions-in-an-azure-ad-tenant">get subscriptions</a>, <a href="/blog/graph-api-group-licences/#evaluating-service-plan-dependencies-for-subscriptions">calculate dependencies</a> and <a href="/blog/graph-api-groups-relationship/#create-azure-ad-group-relationships">assign licences to groups</a>, as well as <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Groups/New-WTAADSubscriptionGroup.ps1">create groups</a>, we can bring this all together to automate in a CI/CD pipeline. This is in part based upon the <a href="/blog/graph-api-groups-pipeline-validate/">Azure AD groups pipeline</a>.</p>
<p>I’m using <a href="https://dev.azure.com/wesleytrust/GraphAPI">Azure DevOps</a> to execute my Pipeline, but with some tweaks the YAML could run in GitHub Actions, making it relatively easy to use on either platform.</p>
<p><em>Both Azure Pipelines and GitHub Actions have free tiers for public projects, and free execution minutes for private projects.</em></p>
<p>This post covers the YAML and PowerShell executed in the pipeline, the PowerShell can also be called directly or executed in a Windows Server docker container, making this quite portable and versatile.</p>
<table>
<thead>
<tr>
<th align="center">Current Import & Validate Status</th>
<th align="center">Current Plan & Evaluate Status</th>
<th align="center">Current Apply & Deploy Status</th>
<th align="center">Overall CI/CD Pipeline Status</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><a href="https://dev.azure.com/wesleytrust/GraphAPI/_build/latest?definitionId=23&branchName=main"><img src="https://dev.azure.com/wesleytrust/GraphAPI/_apis/build/status/Azure%20AD/Subscriptions/SVC-CS%3BENV-P%3B%20Subscriptions?branchName=main&stageName=Validate&jobName=Import" alt="Build Status" /></a></td>
<td align="center"><a href="https://dev.azure.com/wesleytrust/GraphAPI/_build/latest?definitionId=23&branchName=main"><img src="https://dev.azure.com/wesleytrust/GraphAPI/_apis/build/status/Azure%20AD/Subscriptions/SVC-CS%3BENV-P%3B%20Subscriptions?branchName=main&stageName=Plan&jobName=Evaluate" alt="Build Status" /></a></td>
<td align="center"><a href="https://dev.azure.com/wesleytrust/GraphAPI/_build/latest?definitionId=23&branchName=main"><img src="https://dev.azure.com/wesleytrust/GraphAPI/_apis/build/status/Azure%20AD/Subscriptions/SVC-CS%3BENV-P%3B%20Subscriptions?branchName=main&stageName=Apply&jobName=Deploy" alt="Build Status" /></a></td>
<td align="center"><a href="https://dev.azure.com/wesleytrust/GraphAPI/_build/latest?definitionId=23&branchName=main"><img src="https://dev.azure.com/wesleytrust/GraphAPI/_apis/build/status/Azure%20AD/Subscriptions/SVC-CS%3BENV-P%3B%20Subscriptions?branchName=main" alt="Build Status" /></a></td>
</tr>
</tbody>
</table>
<p><em>The apply stage is skipped when there are no changes to deploy, and so may show as “cancelled”</em></p>
<h3 id="pipeline-stages">Pipeline Stages</h3>
<ul>
<li><a href="#trigger-pipeline">Trigger Pipeline</a></li>
<li><a href="#shared-pipeline">Shared Pipeline</a>
<ul>
<li><a href="#import--validate">Import & Validate</a></li>
<li><a href="#plan--evaluate">Plan & Evaluate</a></li>
<li><a href="#apply--deploy">Apply & Deploy</a></li>
</ul>
</li>
</ul>
<h2 id="trigger-pipeline">Trigger Pipeline</h2>
<p>You can access the <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/Pipeline/AzureAD/Subscriptions/ENV-P/azure-pipelines.yml">trigger pipeline on my GitHub here</a>. This trigger contains an extend, so that each stage of the rest of the pipeline is included.</p>
<h4 id="pipeline-yaml-example-below----omit-in-toc---">Pipeline YAML example below: <!-- omit in toc --></h4>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">trigger</span><span class="pi">:</span>
<span class="na">batch</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">branches</span><span class="pi">:</span>
<span class="na">include</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">main</span>
<span class="na">paths</span><span class="pi">:</span>
<span class="na">include</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">AzureAD/Subscriptions/</span>
<span class="na">schedules</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0</span><span class="nv"> </span><span class="s">*/1</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*"</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Run hourly every day</span>
<span class="na">branches</span><span class="pi">:</span>
<span class="na">include</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">main</span>
<span class="na">always</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">pr</span><span class="pi">:</span> <span class="s">none</span>
<span class="na">extends</span><span class="pi">:</span>
<span class="na">template</span><span class="pi">:</span> <span class="s">../Shared/azure-pipelines.yml</span>
</code></pre></div></div>
</details>
<h4 id="what-does-this-do----omit-in-toc---">What does this do? <!-- omit in toc --></h4>
<ul>
<li>This triggers on a change to the main branch, for a specific path within the <a href="https://github.com/wesley-trust/GraphAPIConfig/tree/main/AzureAD/Subscriptions">Graph API Config repo</a>
<ul>
<li>This prevents runs on changes to other config files within the repo</li>
</ul>
</li>
<li>Changes are batched, so the pipeline will only execute once concurrently
<ul>
<li>This is necessary as if the pipeline ran more than once concurrently, subsequent runs could use stale data to create the “plan” of actions to “apply”</li>
</ul>
</li>
<li>This is also executed on an hourly schedule for the main branch, and will always run, even if there are no changes to the branch
<ul>
<li>This is so that new subscription licences are picked up from Azure AD and the pipeline executed</li>
</ul>
</li>
<li>The pipeline is not triggered on pull requests, as this pipeline runs in my sandbox tenant already
<ul>
<li>If triggered on a production repo, this PR could be used to execute the pipeline against a dev environment</li>
</ul>
</li>
<li>An extend is included, leading to the shared template that contains all the stages of the pipeline</li>
</ul>
<h2 id="shared-pipeline">Shared Pipeline</h2>
<p>You can access the <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/Pipeline/AzureAD/Subscriptions/Shared/azure-pipelines.yml">shared pipeline on my GitHub here</a>.</p>
<h4 id="pipeline-yaml-example-below----omit-in-toc----1">Pipeline YAML example below: <!-- omit in toc --></h4>
<p><em>Azure Pipelines automatically clones the config repo for the first stage, and any artifacts created in subsequent stages</em></p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">variables</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s1">'</span><span class="s">GitHubAuth'</span>
<span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s1">'</span><span class="s">ServicePrincipal'</span>
<span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s1">'</span><span class="s">SubscriptionMemberGroups'</span>
<span class="na">stages</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">Validate</span>
<span class="na">pool</span><span class="pi">:</span>
<span class="na">vmImage</span><span class="pi">:</span> <span class="s1">'</span><span class="s">windows-latest'</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">job</span><span class="pi">:</span> <span class="s">Import</span>
<span class="na">continueOnError</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CloneGraphAPI</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Clone Graph API repo</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">git</span><span class="nv"> </span><span class="s">clone</span><span class="nv"> </span><span class="s">--branch</span><span class="nv"> </span><span class="s">$(Branch)</span><span class="nv"> </span><span class="s">--single-branch</span><span class="nv"> </span><span class="s">https://github.com/wesley-trust/GraphAPI.git'</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">InvokeWTValidateSubscription</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Invoke-WTValidateSubscription</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">inline'</span>
<span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s"># Dot source function</span>
<span class="s">. $(System.ArtifactsDirectory)\GraphAPI\Public\AzureAD\Subscriptions\Pipeline\Invoke-WTValidateSubscription.ps1</span>
<span class="s"># Test if directory exist and execute function as appropriate</span>
<span class="s">$TestPath = Test-Path $(Build.Repository.LocalPath)\AzureAD\Subscriptions\Definitions -PathType Container</span>
<span class="s">if ($TestPath){</span>
<span class="s">$ValidateDefinedSubscriptions = Invoke-WTValidateSubscription `</span>
<span class="s">-Path $(Build.Repository.LocalPath)\AzureAD\Subscriptions\Definitions</span>
<span class="s">}</span>
<span class="s"># Create directory for artifact, if it does not exist</span>
<span class="s">$TestPath = Test-Path $(Pipeline.Workspace)\Output -PathType Container</span>
<span class="s">if (!$TestPath){</span>
<span class="s">New-Item -Path $(Pipeline.Workspace)\Output -ItemType Directory | Out-Null</span>
<span class="s">}</span>
<span class="s"># If there are Subscriptions (as if there are no Subscriptions to import, existing Subscriptions are not removed)</span>
<span class="s">if ($ValidateDefinedSubscriptions){</span>
<span class="no"> </span>
<span class="s"># Convert to JSON and export</span>
<span class="s">$ValidateDefinedSubscriptions | ConvertTo-Json -Depth 10 | Out-File -Force -FilePath $(Pipeline.Workspace)\Output\Validate.json</span>
<span class="s">}</span>
<span class="na">pwsh</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PublishPipelineArtifact@1</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetPath</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(Pipeline.Workspace)\Output'</span>
<span class="na">artifact</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Import'</span>
<span class="na">publishLocation</span><span class="pi">:</span> <span class="s1">'</span><span class="s">pipeline'</span>
<span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">Plan</span>
<span class="na">pool</span><span class="pi">:</span>
<span class="na">vmImage</span><span class="pi">:</span> <span class="s1">'</span><span class="s">windows-latest'</span>
<span class="na">dependsOn</span><span class="pi">:</span> <span class="s">Validate</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s">succeeded()</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">job</span><span class="pi">:</span> <span class="s">Evaluate</span>
<span class="na">continueOnError</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">DownloadPipelineArtifact@2</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">buildType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">current'</span>
<span class="na">targetPath</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(Pipeline.Workspace)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CloneGraphAPI</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Clone Graph API repo</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">git</span><span class="nv"> </span><span class="s">clone</span><span class="nv"> </span><span class="s">--branch</span><span class="nv"> </span><span class="s">$(Branch)</span><span class="nv"> </span><span class="s">--single-branch</span><span class="nv"> </span><span class="s">https://github.com/wesley-trust/GraphAPI.git'</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CloneToolKit</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Clone Toolkit repo</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">git</span><span class="nv"> </span><span class="s">clone</span><span class="nv"> </span><span class="s">--branch</span><span class="nv"> </span><span class="s">$(Branch)</span><span class="nv"> </span><span class="s">--single-branch</span><span class="nv"> </span><span class="s">https://github.com/wesley-trust/ToolKit.git'</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">InvokeWTPlanSubscription</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Invoke-WTPlanSubscription</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">inline'</span>
<span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s"># Import and convert Subscriptions from JSON, should they exist</span>
<span class="s">$TestPath = Test-Path $(Pipeline.Workspace)\Import\Validate.json -PathType Leaf</span>
<span class="s">if ($TestPath){</span>
<span class="s">$ValidateDefinedSubscriptions = Get-Content -Raw -Path $(Pipeline.Workspace)\Import\Validate.json | ConvertFrom-Json -Depth 10</span>
<span class="s">}</span>
<span class="s"># Dot source and execute function</span>
<span class="s">. $(System.ArtifactsDirectory)\GraphAPI\Public\AzureAD\Subscriptions\Pipeline\Invoke-WTPlanSubscription.ps1</span>
<span class="s">$PlanDefinedSubscriptions = Invoke-WTPlanSubscription `</span>
<span class="s">-TenantDomain $(TenantDomain) `</span>
<span class="s">-ClientID ${env:CLIENTID} `</span>
<span class="s">-ClientSecret ${env:CLIENTSECRET} `</span>
<span class="s">-DefinedSubscriptions $ValidateDefinedSubscriptions `</span>
<span class="s">-RemoveDefinedSubscriptions `</span>
<span class="s">-Force</span>
<span class="s"># Create directory for artifact, if it does not exist</span>
<span class="s">$TestPath = Test-Path $(Pipeline.Workspace)\Output -PathType Container</span>
<span class="s">if (!$TestPath){</span>
<span class="s">New-Item -Path $(Pipeline.Workspace)\Output -ItemType Directory | Out-Null</span>
<span class="s">}</span>
<span class="s"># If there are Subscriptions</span>
<span class="s">if ($PlanDefinedSubscriptions.RemoveSubscriptions -or $PlanDefinedSubscriptions.CreateSubscriptions){</span>
<span class="s"># Set ShouldRun variable to true, for apply stage</span>
<span class="s">echo "##vso[task.setvariable variable=ShouldRun;isOutput=true]true"</span>
<span class="s"># Convert to JSON and export</span>
<span class="s">$PlanDefinedSubscriptions | ConvertTo-Json -Depth 10 | Out-File -Force -FilePath $(Pipeline.Workspace)\Output\Plan.json</span>
<span class="s">}</span>
<span class="na">pwsh</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="na">env</span><span class="pi">:</span>
<span class="na">CLIENTID</span><span class="pi">:</span> <span class="s">$(ClientID)</span>
<span class="na">CLIENTSECRET</span><span class="pi">:</span> <span class="s">$(ClientSecret)</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PublishPipelineArtifact@1</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetPath</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(Pipeline.Workspace)\Output'</span>
<span class="na">artifact</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Evaluate'</span>
<span class="na">publishLocation</span><span class="pi">:</span> <span class="s1">'</span><span class="s">pipeline'</span>
<span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">Apply</span>
<span class="na">pool</span><span class="pi">:</span>
<span class="na">vmImage</span><span class="pi">:</span> <span class="s1">'</span><span class="s">windows-latest'</span>
<span class="na">dependsOn</span><span class="pi">:</span> <span class="s">Plan</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s">and(succeeded(), eq(dependencies.Plan.outputs['Evaluate.InvokeWTPlanSubscription.ShouldRun'], 'true'))</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">deployment</span><span class="pi">:</span> <span class="s">Deploy</span>
<span class="na">continueOnError</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">environment</span><span class="pi">:</span> <span class="s">$(Environment)</span>
<span class="na">strategy</span><span class="pi">:</span>
<span class="na">runOnce</span><span class="pi">:</span>
<span class="na">deploy</span><span class="pi">:</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">checkout</span><span class="pi">:</span> <span class="s">self</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CloneGraphAPI</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Clone Graph API repo</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">git</span><span class="nv"> </span><span class="s">clone</span><span class="nv"> </span><span class="s">--branch</span><span class="nv"> </span><span class="s">$(Branch)</span><span class="nv"> </span><span class="s">--single-branch</span><span class="nv"> </span><span class="s">https://github.com/wesley-trust/GraphAPI.git'</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CloneToolKit</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Clone Toolkit repo</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">git</span><span class="nv"> </span><span class="s">clone</span><span class="nv"> </span><span class="s">--branch</span><span class="nv"> </span><span class="s">$(Branch)</span><span class="nv"> </span><span class="s">--single-branch</span><span class="nv"> </span><span class="s">https://github.com/wesley-trust/ToolKit.git'</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">InvokeWTApplySubscription</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Invoke-WTApplySubscription</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">inline'</span>
<span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s"># Import and convert Subscriptions from JSON, should they exist</span>
<span class="s">$TestPath = Test-Path $(Pipeline.Workspace)\Evaluate\Plan.json -PathType Leaf</span>
<span class="s">if ($TestPath){</span>
<span class="s">$PlanDefinedSubscriptions = Get-Content -Raw -Path $(Pipeline.Workspace)\Evaluate\Plan.json | ConvertFrom-Json -Depth 10</span>
<span class="s">}</span>
<span class="s"># Import service plan dependencies if they exist and convert from JSON</span>
<span class="s">$DependentServicePlansPath = "$(Build.Repository.LocalPath)\AzureAD\Subscriptions\Dependencies"</span>
<span class="s">$PathExists = Test-Path -Path $DependentServicePlansPath</span>
<span class="s">if ($PathExists) {</span>
<span class="s">$DependentServicePlansFilePath = (Get-ChildItem -Path $DependentServicePlansPath -Filter "*.json").FullName</span>
<span class="s">}</span>
<span class="s">if ($DependentServicePlansFilePath) {</span>
<span class="s">$DependentServicePlansImport = foreach ($DependentServicePlanFile in $DependentServicePlansFilePath) {</span>
<span class="s">Get-Content -Raw -Path $DependentServicePlanFile</span>
<span class="s">}</span>
<span class="s">}</span>
<span class="s">if ($DependentServicePlansImport) {</span>
<span class="s">$DependentServicePlans = $DependentServicePlansImport | ConvertFrom-Json -Depth 10</span>
<span class="s">}</span>
<span class="s"># Dot source and execute function</span>
<span class="s">. $(System.ArtifactsDirectory)\GraphAPI\Public\AzureAD\Subscriptions\Pipeline\Invoke-WTApplySubscription.ps1</span>
<span class="s">Invoke-WTApplySubscription `</span>
<span class="s">-TenantDomain $(TenantDomain) `</span>
<span class="s">-ClientID ${env:CLIENTID} `</span>
<span class="s">-ClientSecret ${env:CLIENTSECRET} `</span>
<span class="s">-DefinedSubscriptions $PlanDefinedSubscriptions `</span>
<span class="s">-DependentServicePlans $DependentServicePlans `</span>
<span class="s">-RemoveDefinedSubscriptions `</span>
<span class="s">-Path $(Build.SourcesDirectory)\AzureAD\Subscriptions\Definitions `</span>
<span class="s">-Pipeline</span>
<span class="na">pwsh</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="na">env</span><span class="pi">:</span>
<span class="na">CLIENTID</span><span class="pi">:</span> <span class="s">$(ClientID)</span>
<span class="na">CLIENTSECRET</span><span class="pi">:</span> <span class="s">$(ClientSecret)</span>
<span class="na">GITHUBPAT</span><span class="pi">:</span> <span class="s">$(GitHubPAT)</span>
<span class="na">REPOHOME</span><span class="pi">:</span> <span class="s">$(Build.Repository.LocalPath)</span>
<span class="na">BRANCH</span><span class="pi">:</span> <span class="s">$(Branch)</span>
<span class="na">USERGROUPID</span><span class="pi">:</span> <span class="s">$(UserGroupID)</span>
<span class="na">GITHUBCONFIGREPO</span><span class="pi">:</span> <span class="s">$(GitHubConfigRepo)</span>
</code></pre></div></div>
</details>
<h4 id="what-does-this-do----omit-in-toc----1">What does this do? <!-- omit in toc --></h4>
<ul>
<li>Variable groups are defined and included within the pipeline
<ul>
<li>Including the service principal to authenticate with the Graph API and the GitHub PAT for pushing changes
<ul>
<li>These are set as environmental variables for tasks as appropriate
<ul>
<li>This is because they contain secrets that would be exposed in the pipeline if not</li>
</ul>
</li>
</ul>
</li>
<li>Subscription group members are defined, for adding members to the groups that are created
<ul>
<li>This automatically licences those members</li>
</ul>
</li>
</ul>
</li>
<li>The container image for the Azure DevOps agent is defined</li>
<li>Steps are defined for tasks to clone required repos</li>
<li>Each stage in the pipeline is defined
<ul>
<li>With a PowerShell task to load the PowerShell pipeline function into memory and execute</li>
<li>With artifacts and variables set for subsequent stages as appropriate
<ul>
<li>With conditions set for stages so they only trigger when there is something to do</li>
</ul>
</li>
</ul>
</li>
<li>In addition, pipeline variables are set to define the branch and environment the pipeline is executing
<ul>
<li>This allows for approvals to be put on the environment, so changes only apply when approved</li>
<li>As well as pushing changes to the correct branch in GitHub</li>
</ul>
</li>
</ul>
<h4 id="powershell-example-below----omit-in-toc---">PowerShell example below: <!-- omit in toc --></h4>
<p>This function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Pipeline/Invoke-WTSubscriptionImport.ps1">Invoke-WTSubscriptionImport</a>, which you can access from my GitHub. This mimics the pipeline stages.</p>
<p>I created this to make it easier to test locally as well as run in a Windows Server docker container using PowerShell 7.</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-WTAzureADSubscriptionImport</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with the correct Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with the correct Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The file path to the JSON file(s) that will be imported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$FilePath</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The directory path(s) of which all JSON file(s) will be imported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Path</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The directory path to the location where the ServicePlan dependencies will be imported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$DependentServicePlansPath</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether defined subscriptions deployed in the tenant will be removed, if not present in the import"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="w">
</span><span class="nv">$RemoveDefinedSubscriptions</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether the groups used for subscriptions, should not be removed, if the subscription is removed"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="w">
</span><span class="nv">$ExcludeGroupRemoval</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"If there are no subscriptions, whether to forcibly remove any defined subscriptions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$Force</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify until what stage the import should invoke. All preceding stages will execute as dependencies"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s2">"Validate"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Plan"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Apply"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Stage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Apply"</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether the function is operating within a pipeline"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$Pipeline</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Pipeline\Invoke-WTValidateSubscription.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Pipeline\Invoke-WTPlanSubscription.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Pipeline\Invoke-WTApplySubscription.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Stage</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"Validate"</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="nv">$Stage</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"Plan"</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="nv">$Stage</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"Apply"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$ValidateParameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ValidateParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$FilePath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ValidateParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"FilePath"</span><span class="p">,</span><span class="w"> </span><span class="nv">$FilePath</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$Path</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ValidateParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"Path"</span><span class="p">,</span><span class="w"> </span><span class="nv">$Path</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Import and validate subscriptions</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Stage 1: Validate"</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$FilePath</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="nv">$Path</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$TestPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="w"> </span><span class="nt">-PathType</span><span class="w"> </span><span class="nx">Container</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$TestPath</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="nv">$FilePath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Invoke-WTValidateSubscription</span><span class="w"> </span><span class="err">@</span><span class="nx">ValidateParameters</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Tee-Object</span><span class="w"> </span><span class="nt">-Variable</span><span class="w"> </span><span class="nx">ValidateSubscriptions</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Stage</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"Plan"</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="nv">$Stage</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"Apply"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$PlanParameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ValidateSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"DefinedSubscriptions"</span><span class="p">,</span><span class="w"> </span><span class="nv">$ValidateSubscriptions</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RemoveDefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"RemoveDefinedSubscriptions"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Force</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"Force"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Create plan evaluating whether to create, update or remove subscriptions</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Stage 2: Plan"</span><span class="w">
</span><span class="n">Invoke-WTPlanSubscription</span><span class="w"> </span><span class="err">@</span><span class="nx">PlanParameters</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Tee-Object</span><span class="w"> </span><span class="nt">-Variable</span><span class="w"> </span><span class="nx">PlanSubscriptions</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Stage</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"Apply"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PlanSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Import service plan dependencies if they exist and convert from JSON</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependentServicePlansPath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PathExists</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$DependentServicePlansPath</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PathExists</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$DependentServicePlansFilePath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$DependentServicePlansPath</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="s2">"*.json"</span><span class="p">)</span><span class="o">.</span><span class="nf">FullName</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependentServicePlansFilePath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$DependentServicePlansImport</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependentServicePlanFile</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$DependentServicePlansFilePath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Get-Content</span><span class="w"> </span><span class="nt">-Raw</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$DependentServicePlanFile</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependentServicePlansImport</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$DependentServicePlans</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DependentServicePlansImport</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$ApplyParameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="nx">DefinedSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$PlanSubscriptions</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ApplyParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RemoveDefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ApplyParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"RemoveDefinedSubscriptions"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludeGroupRemoval</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ApplyParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludeGroupRemoval"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$FilePath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ApplyParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"FilePath"</span><span class="p">,</span><span class="w"> </span><span class="nv">$FilePath</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$Path</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ApplyParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"Path"</span><span class="p">,</span><span class="w"> </span><span class="nv">$Path</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Pipeline</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ApplyParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"Pipeline"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependentServicePlans</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ApplyParameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"DependentServicePlans"</span><span class="p">,</span><span class="w"> </span><span class="nv">$DependentServicePlans</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Apply plan to Azure AD</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Stage 3: Apply"</span><span class="w">
</span><span class="n">Invoke-WTApplySubscription</span><span class="w"> </span><span class="err">@</span><span class="nx">ApplyParameters</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No subscriptions will be created, updated or removed, as none exist that are different to the import"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h3 id="import-amp-validate">Import & Validate</h3>
<p>This function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Pipeline/Invoke-WTValidateSubscription.ps1">Invoke-WTValidateSubscription</a>, which you can access from my GitHub.</p>
<p>This imports JSON definitions of subscriptions, or imports subscription objects via a parameter, and validates these against a set of criteria.</p>
<p>Outputting a JSON validate file (as appropriate) as a pipeline artifact for the next stage in the pipeline.</p>
<h4 id="what-does-this-do----omit-in-toc----2">What does this do? <!-- omit in toc --></h4>
<ul>
<li>This sets specific variables, including the required properties that must be present in the input</li>
<li>To import, a file path to specific files or a directory path from which all files will be imported is required
<ul>
<li>Alternatively, a subscription or collection of subscriptions can also be passed in a parameter to validate</li>
</ul>
</li>
<li>This then checks for the properties each subscription has
<ul>
<li>Each required property that is missing is added to a variable</li>
</ul>
</li>
<li>A check is then performed as to whether the properties contain a value
<ul>
<li>This is again added to a variable if null</li>
</ul>
</li>
<li>A validate object is then built for each subscription with failed checks</li>
<li>Information is then returned about whether the subscription passed validation, and if not, why each subscription failed</li>
<li>If successful, the validated subscription objects are returned</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-WTValidateSubscription</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The file path to the JSON file(s) that will be imported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$FilePath</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The directory path(s) of which all JSON file(s) will be imported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Path</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Azure AD Subscriptions to be validated if not imported from a JSON file"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s1">'Subscription'</span><span class="p">,</span><span class="w"> </span><span class="s1">'SubscriptionDefinition'</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$DefinedSubscriptions</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether files should be imported only, and not validated"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ImportOnly</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Variables</span><span class="w">
</span><span class="nv">$RequiredProperties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"skuPartNumber"</span><span class="p">,</span><span class="s2">"skuId"</span><span class="p">,</span><span class="s2">"servicePlans"</span><span class="p">,</span><span class="s2">"capabilityStatus"</span><span class="p">,</span><span class="s2">"appliesTo"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># For each directory, get the file path of all JSON files within the directory, if the directory exists</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Path</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PathExists</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PathExists</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$FilePath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="s2">"*.json"</span><span class="p">)</span><span class="o">.</span><span class="nf">FullName</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The provided path does not exist </span><span class="nv">$Path</span><span class="s2">, please check the path is correct"</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Import Subscriptions from JSON file, if the files exist</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$FilePath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$SubscriptionImport</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$File</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$FilePath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$FilePathExists</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$File</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$FilePathExists</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Get-Content</span><span class="w"> </span><span class="nt">-Raw</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$File</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The provided filepath </span><span class="nv">$File</span><span class="s2"> does not exist, please check the path is correct"</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If import was successful, convert from JSON</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$SubscriptionImport</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$DefinedSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$SubscriptionImport</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No JSON files could be imported, please check the filepath is correct"</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there are subscriptions imported, run validation checks</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Importing Defined Subscriptions"</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Subscriptions: </span><span class="si">$(</span><span class="nv">$DefinedSubscriptions</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$DefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Import: Subscription Name: </span><span class="si">$(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Import: Subscription Id: </span><span class="si">$(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Import: Subscription Invalid"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If import only is set, return subscriptions without validating</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ImportOnly</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$DefinedSubscriptions</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Validating Defined Subscriptions"</span><span class="w">
</span><span class="c"># For each policy, run validation checks</span><span class="w">
</span><span class="nv">$InvalidSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$DefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$SubscriptionValidate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="c"># Check for missing properties</span><span class="w">
</span><span class="nv">$SubscriptionProperties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="nv">$SubscriptionProperties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-Member</span><span class="w"> </span><span class="nt">-MemberType</span><span class="w"> </span><span class="nx">NoteProperty</span><span class="p">)</span><span class="o">.</span><span class="nf">name</span><span class="w">
</span><span class="nv">$PropertyCheck</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="c"># Check whether each required property, exists in the list of properties for the object</span><span class="w">
</span><span class="nv">$PropertyCheck</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Property</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$RequiredProperties</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Property</span><span class="w"> </span><span class="nt">-notin</span><span class="w"> </span><span class="nv">$SubscriptionProperties</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Property</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Check whether each required property has a value, if not, return property</span><span class="w">
</span><span class="nv">$PropertyValueCheck</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="nv">$PropertyValueCheck</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Property</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$RequiredProperties</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$null</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$Subscription</span><span class="o">.</span><span class="nv">$Property</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Property</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Build and return object</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PropertyCheck</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="nv">$PropertyValueCheck</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$SubscriptionValidate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">ordered</span><span class="p">]@{}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$SubscriptionValidate</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"skuPartNumber"</span><span class="p">,</span><span class="w"> </span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$SubscriptionValidate</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"Id"</span><span class="p">,</span><span class="w"> </span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">id</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PropertyCheck</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$SubscriptionValidate</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"MissingProperties"</span><span class="p">,</span><span class="w"> </span><span class="nv">$PropertyCheck</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PropertyValueCheck</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$SubscriptionValidate</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"MissingPropertyValues"</span><span class="p">,</span><span class="w"> </span><span class="nv">$PropertyValueCheck</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$SubscriptionValidate</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$SubscriptionValidate</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Return validation result for each policy</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$InvalidSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Invalid subscriptions: </span><span class="si">$(</span><span class="nv">$InvalidSubscriptions</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2"> out of </span><span class="si">$(</span><span class="nv">$DefinedSubscriptions</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2"> imported"</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$InvalidSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"INVALID: Subscription Name: </span><span class="si">$(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"INVALID: Subscription Id: </span><span class="si">$(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"INVALID: No skuPartNumber or Id for policy"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">MissingProperties</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Required properties not present (</span><span class="si">$(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">MissingProperties</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">): </span><span class="si">$(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">MissingProperties</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">MissingPropertyValues</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Required property values not present (</span><span class="si">$(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">MissingPropertyValues</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">): </span><span class="si">$(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">MissingPropertyValues</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Abort import</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Validation of subscriptions was not successful, review configuration files and any warnings generated"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Return validated subscriptions</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"All subscriptions have passed validation for required properties and values"</span><span class="w">
</span><span class="nv">$ValidSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DefinedSubscriptions</span><span class="w">
</span><span class="nv">$ValidSubscriptions</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No Subscriptions to be imported, import may have failed or none may exist"</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h3 id="plan-amp-evaluate">Plan & Evaluate</h3>
<p>This function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Pipeline/Invoke-WTPlanSubscription.ps1">Invoke-WTPlanSubscription</a>, which you can access from my GitHub.</p>
<p>Within the pipeline, this imports the validated JSON artifact of subscriptions (should they exist), which is passed to the function via a parameter. This then creates a plan of what should be created, updated or removed (as appropriate).</p>
<p>Outputting a JSON plan file (as appropriate) as a pipeline artifact for the next stage in the pipeline.</p>
<h4 id="what-does-this-do----omit-in-toc----3">What does this do? <!-- omit in toc --></h4>
<ul>
<li>Specific variables are set and any dependent functions are imported into memory</li>
<li>An <a href="/blog/obtain-access-token/">access token is obtained</a>, if one is not provided, this allows the same token to be shared within the pipeline</li>
<li>Checks are performed about whether to evaluate subscriptions for removal</li>
<li>Existing subscriptions in Azure AD are obtained from the <a href="/blog/graph-api-group-licences/#getting-subscriptions-in-an-azure-ad-tenant">get subscriptions function</a>, in order to compare against the validated import</li>
<li>An object comparison is performed on the skuPartNumber, determining:
<ul>
<li>What defined subscriptions could be removed (as they don’t exist in Azure AD, but were in the import)
<ul>
<li>So should have their groups removed and the definitions removed in the config repo</li>
</ul>
</li>
<li>What existing subscriptions need their definitions creating (as they exist in Azure AD, but were not defined in the import)
<ul>
<li>So should have groups created, subscriptions assigned and definitions created in the config repo</li>
</ul>
</li>
</ul>
</li>
<li>A safety check is performed if no subscriptions exist but were defined in the import, so removing all defined subscriptions requires a “Force” switch</li>
<li>If subscriptions should not be removed, the variable for removing subscriptions is cleared</li>
<li>If no subscriptions exist in the import, any existing subscriptions must all be created, so the variable is updated</li>
<li>An object is then built containing the subscriptions to be removed or created (as appropriate)</li>
<li>This object is then returned as a plan of action, which is output as a pipeline artifact for the next stage</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-WTPlanSubscription</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with Subscription Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with Subscription Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Subscription object"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"Subscription"</span><span class="p">,</span><span class="w"> </span><span class="s2">"SubscriptionDefinition"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Subscriptions"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$DefinedSubscriptions</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether current Subscription deployed in the tenant will be removed, if not present in the import"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="w">
</span><span class="nv">$RemoveDefinedSubscriptions</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"If there are no Subscription to import, whether to forcibly remove any current Subscription"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$Force</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"Toolkit\Public\Invoke-WTPropertyTagging.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Get-WTAzureADSubscription.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Evaluating Subscriptions"</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Get user subscriptions that have not been deleted</span><span class="w">
</span><span class="nv">$CurrentSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTAzureADSubscription</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w">
</span><span class="nv">$AssignableSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$CurrentSubscriptions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$_</span><span class="o">.</span><span class="nf">capabilityStatus</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s2">"Deleted"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">appliesTo</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"User"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AssignableSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Compare object on id and pass thru all objects, including those that exist and are to be imported</span><span class="w">
</span><span class="nv">$SubscriptionComparison</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Compare-Object</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ReferenceObject</span><span class="w"> </span><span class="nv">$AssignableSubscriptions</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-DifferenceObject</span><span class="w"> </span><span class="nv">$DefinedSubscriptions</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Property</span><span class="w"> </span><span class="nx">skuPartNumber</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-PassThru</span><span class="w">
</span><span class="c"># Filter for defined Subscription that should be removed, as they exist only in the import</span><span class="w">
</span><span class="nv">$RemoveSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$SubscriptionComparison</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">sideindicator</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"=>"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="c"># Filter for defined Subscription that should be created, as they exist only in Azure AD</span><span class="w">
</span><span class="nv">$CreateSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$SubscriptionComparison</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">sideindicator</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"<="</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If force is enabled, then if removal of Subscription is specified, all current will be removed</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Force</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$RemoveSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DefinedSubscriptions</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$RemoveDefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If Subscription are not to be removed, disregard any Subscription for removal</span><span class="w">
</span><span class="nv">$RemoveSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If no defined subscription exist, any enabled subscriptions should be defined</span><span class="w">
</span><span class="nv">$CreateSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AssignableSubscriptions</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Build object to return</span><span class="w">
</span><span class="nv">$PlanSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">ordered</span><span class="p">]@{}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RemoveSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanSubscriptions</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"RemoveSubscriptions"</span><span class="p">,</span><span class="w"> </span><span class="nv">$RemoveSubscriptions</span><span class="p">)</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Defined Subscription to remove: </span><span class="si">$(</span><span class="nv">$RemoveSubscriptions</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$RemoveSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Remove: Subscription ID: </span><span class="si">$(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2"> (Subscription Groups will be removed as appropriate)"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">DarkRed</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"No Subscription will be removed, as none exist that are different to the import"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$CreateSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanSubscriptions</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"CreateSubscriptions"</span><span class="p">,</span><span class="w"> </span><span class="nv">$CreateSubscriptions</span><span class="p">)</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Defined Subscription to create: </span><span class="si">$(</span><span class="nv">$CreateSubscriptions</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2"> (Subscription Groups will be created as appropriate)"</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$CreateSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Create: Subscription Name: </span><span class="si">$(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">DarkGreen</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"No Subscription will be created, as none exist that are different to the import"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there are Subscription, return PS object</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PlanSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$PlanSubscriptions</span><span class="w">
</span><span class="nv">$PlanSubscriptions</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h3 id="apply-amp-deploy">Apply & Deploy</h3>
<p>This function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Pipeline/Invoke-WTApplySubscription.ps1">Invoke-WTApplySubscription</a>, which you can access from my GitHub.</p>
<p>Within the pipeline, this imports the plan JSON artifact of subscriptions, which is passed to the function via a parameter. This contains the subscriptions that should have groups created or removed (as appropriate), as well as licences assigned and definitions created or removed (as appropriate).</p>
<h4 id="what-does-this-do----omit-in-toc----4">What does this do? <!-- omit in toc --></h4>
<ul>
<li>Specific variables are set and any dependent functions are imported into memory</li>
<li>An <a href="/blog/obtain-access-token/">access token is obtained</a>, if one is not provided, this allows the same token to be shared within the pipeline</li>
<li>If subscriptions should be removed,
<ul>
<li>Subscription groups are obtained with the <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Groups/Get-WTAADSubscriptionGroup.ps1">get subscription group function</a> and are tagged with the <a href="/blog/tagging-object-properties/#invoke-property-tagging">property tagging function</a></li>
<li>Then each of the skuPartNumbers have their config removed</li>
<li>The group for the subscription is identified, and this is provided to the <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Groups/Remove-WTAADSubscriptionGroup.ps1">remove subscription group function</a></li>
<li>The group config is then removed</li>
</ul>
</li>
<li>If there are subscription definitions to be created,
<ul>
<li>If there are service plan dependencies, these are evaluated with the <a href="/blog/graph-api-group-licences/#evaluating-service-plan-dependencies-for-subscriptions">get subscription dependency function</a></li>
<li>Display names for the subscription groups are then created and provided to the <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Groups/New-WTAADSubscriptionGroup.ps1">new subscription group function</a></li>
<li>The groups are then tagged with the <a href="/blog/tagging-object-properties/#invoke-property-tagging">property tagging function</a></li>
<li>For each subscription, the subscription group is identified</li>
<li>If the subscription has a dependency, each dependency is assigned, then the subscription itself, using the <a href="/blog/graph-api-groups-relationship/#create-azure-ad-group-relationships">new group relationship function</a></li>
<li>Then, to get around the lack of nested group support for licence assignment,
<ul>
<li>I get the members of a group defined in the pipeline with the <a href="/blog/graph-api-groups-relationship/#get-azure-ad-group-relationships">get group relationship function</a></li>
<li>And add these with the <a href="/blog/graph-api-groups-relationship/#create-azure-ad-group-relationships">new group relationship function</a> (using different parameter values)</li>
</ul>
</li>
<li>The new subscription definitions are then exported using the <a href="/blog/graph-api-group-licences/#exporting-subscriptions">export subscription function</a>
<ul>
<li>This acts as a system state, storing subscriptions that have been processed</li>
</ul>
</li>
<li>The new group config is also exported using the <a href="/blog/graph-api-groups/#export-an-azure-ad-group">export group function</a></li>
<li>Within the pipeline, the files are added, committed and pushed to the <a href="https://github.com/wesley-trust/GraphAPIConfig/tree/main/AzureAD/Subscriptions">config repo</a></li>
</ul>
</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-WTApplySubscription</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with Subscription Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with Subscription Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Subscription object"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"Subscription"</span><span class="p">,</span><span class="w"> </span><span class="s2">"SubscriptionDefinition"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Subscriptions"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$DefinedSubscriptions</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The dependent service plan objects"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"ServicePlan"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ServicePlans"</span><span class="p">,</span><span class="w"> </span><span class="s2">"DependentServicePlan"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$DependentServicePlans</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether existing subscriptions deployed in the tenant will be removed, if not present in the import"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="w">
</span><span class="nv">$RemoveDefinedSubscriptions</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to the groups used for subscriptions, should not be removed, if the subscription is removed"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="w">
</span><span class="nv">$ExcludeGroupRemoval</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The file path to the JSON file(s) that will be exported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$FilePath</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The directory path(s) of which all JSON file(s) will be exported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Path</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether the function is operating within a pipeline"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$Pipeline</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"Toolkit\Public\Invoke-WTPropertyTagging.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Groups\Get-WTAADSubscriptionGroup.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Groups\New-WTAADSubscriptionGroup.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Groups\Remove-WTAADSubscriptionGroup.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Get-WTAzureADSubscriptionDependency.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Export-WTAzureADSubscription.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Groups\Export-WTAzureADGroup.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Groups\Relationship\Get-WTAzureADGroupRelationship.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Groups\Relationship\New-WTAzureADGroupRelationship.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Variables</span><span class="w">
</span><span class="nv">$Tag</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"SKU"</span><span class="w">
</span><span class="nv">$PropertyToTag</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"displayName"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Deploying Subscriptions"</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RemoveDefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If subscriptions require removing, pass the ids to the remove function</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DefinedSubscriptions</span><span class="o">.</span><span class="nf">RemoveSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Get and tag group for the subscriptions</span><span class="w">
</span><span class="nv">$SubscriptionGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTAADSubscriptionGroup</span><span class="w">
</span><span class="nv">$TaggedSubscriptionGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-WTPropertyTagging</span><span class="w"> </span><span class="nt">-Tags</span><span class="w"> </span><span class="nv">$Tag</span><span class="w"> </span><span class="nt">-QueryResponse</span><span class="w"> </span><span class="nv">$SubscriptionGroups</span><span class="w"> </span><span class="nt">-PropertyToTag</span><span class="w"> </span><span class="nv">$PropertyToTag</span><span class="w">
</span><span class="c"># Path to group config</span><span class="w">
</span><span class="nv">$GroupsPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Path</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"\..\Groups"</span><span class="w">
</span><span class="c"># Remove subscription definition and groups</span><span class="w">
</span><span class="nv">$SubscriptionSkuPartNumbers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DefinedSubscriptions</span><span class="o">.</span><span class="nf">RemoveSubscriptions</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$SubscriptionSkuPartNumber</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$SubscriptionSkuPartNumbers</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Remove-Item</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Path</span><span class="s2">\</span><span class="nv">$SubscriptionSkuPartNumber</span><span class="s2">.json"</span><span class="w">
</span><span class="c"># If the switch to not remove groups is not set, remove the groups for each Subscription also</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$ExcludeGroupRemoval</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Identify the group for the subscription</span><span class="w">
</span><span class="nv">$SubscriptionGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="nv">$SubscriptionGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$TaggedSubscriptionGroups</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$_</span><span class="o">.</span><span class="nv">$Tag</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$SubscriptionSkuPartNumber</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there is a group, pass the id which will perform a check and remove only subscription groups</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$SubscriptionGroup</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Remove group (licences should no longer be assigned to deleted subscriptions)</span><span class="w">
</span><span class="n">Remove-WTAADSubscriptionGroup</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="nt">-IDs</span><span class="w"> </span><span class="nv">$SubscriptionGroup</span><span class="o">.</span><span class="nf">id</span><span class="w">
</span><span class="c"># Remove group config</span><span class="w">
</span><span class="n">Remove-Item</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$GroupsPath</span><span class="s2">\</span><span class="si">$(</span><span class="nv">$SubscriptionGroup</span><span class="o">.</span><span class="nf">displayName</span><span class="si">)</span><span class="s2">.json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No subscriptions will be removed, as none exist that are different to the import"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there are new subscriptions create the groups</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DefinedSubscriptions</span><span class="o">.</span><span class="nf">CreateSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$CreateSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DefinedSubscriptions</span><span class="o">.</span><span class="nf">CreateSubscriptions</span><span class="w">
</span><span class="c"># Find subscriptions with service plan dependencies</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependentServicePlans</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$DependentSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTAzureADSubscriptionDependency</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Subscriptions</span><span class="w"> </span><span class="nv">$CreateSubscriptions</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ServicePlans</span><span class="w"> </span><span class="nv">$DependentServicePlans</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-DependencyType</span><span class="w"> </span><span class="nx">SkuId</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Calculate the display names to be used for the Subscription groups</span><span class="w">
</span><span class="nv">$SubscriptionGroupDisplayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$CreateSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"</span><span class="nv">$Tag</span><span class="s2">"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">";"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Create groups</span><span class="w">
</span><span class="nv">$SubscriptionGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-WTAADSubscriptionGroup</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="nt">-DisplayName</span><span class="w"> </span><span class="nv">$SubscriptionGroupDisplayName</span><span class="w">
</span><span class="c"># Tag groups</span><span class="w">
</span><span class="nv">$TaggedSubscriptionGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-WTPropertyTagging</span><span class="w"> </span><span class="nt">-Tags</span><span class="w"> </span><span class="nv">$Tag</span><span class="w"> </span><span class="nt">-QueryResponse</span><span class="w"> </span><span class="nv">$SubscriptionGroups</span><span class="w"> </span><span class="nt">-PropertyToTag</span><span class="w"> </span><span class="nv">$PropertyToTag</span><span class="w">
</span><span class="c"># For each subscription, perform subscription specific changes</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$CreateSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Find the matching group</span><span class="w">
</span><span class="nv">$SubscriptionGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="nv">$SubscriptionGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$TaggedSubscriptionGroups</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$_</span><span class="o">.</span><span class="nv">$Tag</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there is a group for this subscription (as subscriptions may not always have groups)</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$SubscriptionGroup</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If this subscription is in the list of dependent subscriptions</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuId</span><span class="w"> </span><span class="nt">-in</span><span class="w"> </span><span class="nv">$DependentSubscriptions</span><span class="o">.</span><span class="nf">skuId</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Filter to the specific subscription dependency</span><span class="w">
</span><span class="nv">$DependentSubscription</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="nv">$DependentSubscription</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DependentSubscriptions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$_</span><span class="o">.</span><span class="nf">skuId</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuId</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Assign each required sku for the dependent subscription</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$SkuId</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$DependentSubscription</span><span class="o">.</span><span class="nf">RequiredSkuId</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">New-WTAzureADGroupRelationship</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Id</span><span class="w"> </span><span class="nv">$SubscriptionGroup</span><span class="o">.</span><span class="nf">id</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Relationship</span><span class="w"> </span><span class="s2">"assignLicense"</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-RelationshipIDs</span><span class="w"> </span><span class="nv">$SkuId</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-Null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Assign licence to group</span><span class="w">
</span><span class="n">New-WTAzureADGroupRelationship</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Id</span><span class="w"> </span><span class="nv">$SubscriptionGroup</span><span class="o">.</span><span class="nf">id</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Relationship</span><span class="w"> </span><span class="s2">"assignLicense"</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-RelationshipIDs</span><span class="w"> </span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuId</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-Null</span><span class="w">
</span><span class="c"># Workaround lack of nested group support, by getting users that should be licenced</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">${ENV:UserGroupID}</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Members</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTAzureADGroupRelationship</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Id</span><span class="w"> </span><span class="nv">${ENV:UserGroupID}</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Relationship</span><span class="w"> </span><span class="s2">"members"</span><span class="w">
</span><span class="c"># Then adding the users that should be licenced directly to the group</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Members</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">New-WTAzureADGroupRelationship</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Id</span><span class="w"> </span><span class="nv">$SubscriptionGroup</span><span class="o">.</span><span class="nf">id</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Relationship</span><span class="w"> </span><span class="s2">"members"</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-RelationshipIDs</span><span class="w"> </span><span class="nv">$Members</span><span class="o">.</span><span class="nf">id</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Export subscriptions</span><span class="w">
</span><span class="n">Export-WTAzureADSubscription</span><span class="w"> </span><span class="nt">-DefinedSubscriptions</span><span class="w"> </span><span class="nv">$CreateSubscriptions</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ExcludeExportCleanup</span><span class="w">
</span><span class="c"># Path to group config</span><span class="w">
</span><span class="nv">$GroupsPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Path</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"\..\Groups"</span><span class="w">
</span><span class="c"># Export groups</span><span class="w">
</span><span class="n">Export-WTAzureADGroup</span><span class="w"> </span><span class="nt">-AzureADGroups</span><span class="w"> </span><span class="nv">$SubscriptionGroups</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$GroupsPath</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ExcludeExportCleanup</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ExcludeTagEvaluation</span><span class="w">
</span><span class="c"># If executing in a pipeline, stage, commit and push the changes back to the repo</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Pipeline</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Commit configuration changes post pipeline deployment"</span><span class="w">
</span><span class="n">Set-Location</span><span class="w"> </span><span class="nv">${ENV:REPOHOME}</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">user.email</span><span class="w"> </span><span class="nx">AzurePipeline</span><span class="err">@</span><span class="nx">wesleytrust.com</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">user.name</span><span class="w"> </span><span class="nx">AzurePipeline</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nt">-A</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">commit</span><span class="w"> </span><span class="nt">-a</span><span class="w"> </span><span class="nt">-m</span><span class="w"> </span><span class="s2">"Commit configuration changes post deployment [skip ci]"</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">push</span><span class="w"> </span><span class="nx">https://</span><span class="nv">${ENV:GITHUBPAT}</span><span class="err">@</span><span class="nx">github.com/wesley-trust/</span><span class="nv">${ENV:GITHUBCONFIGREPO}</span><span class="o">.</span><span class="nf">git</span><span class="w"> </span><span class="nx">HEAD:</span><span class="nv">${ENV:BRANCH}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No subscriptions will be created, as none exist that are different to the import"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>Wesley TrustThis post covers the CI/CD pipeline which will be used to automate creating and assigning licences to Azure AD groups...Manage subscription licences in Azure AD with group assignment2021-04-23T00:00:00+01:002021-04-23T00:00:00+01:00https://www.wesleytrust.com/blog/graph-api-group-licences<p>For the next post I had intended on continuing the documentation leading to the Conditional Access pipeline, but over the last week or so, I’ve been writing code for managing Azure AD subscriptions (licences) and directory roles. All based upon group membership to make management far more efficient. So in short, I got distracted.</p>
<p>Group based licensing can be done manually, but automated this in a CI/CD pipeline is way more fun, and far more efficient. It also wasn’t the easiest, so I had to get my thinking cap on a bit and do a fair bit of research and testing. As ever, Microsoft’s API documentation has been somewhat lacking.</p>
<p>One of the big obstacles was the dependencies that subscriptions can have, as when assigning licences to groups, all dependent conditions must be met. Let’s break this down.</p>
<h2 id="subscriptions-and-service-plans----omit-in-toc---">Subscriptions and Service Plans <!-- omit in toc --></h2>
<p>In Azure AD, a subscriptions is defined as a “subscribedSku” (stock-keeping unit). A subscription such as “Microsoft 365 E3” has a skuPartNumber of “SPE_E3”. Within that subscription there are service plans, these are the individual licences that when bundled together make up the subscription.</p>
<p>For example, “Microsoft 365 E3” contains the service plan “EXCHANGE_S_ENTERPRISE”, which corresponds to “Exchange Online (Plan 2)”. That service plan is also available as a standalone subscription with a skuPartNumber of “EXCHANGEENTERPRISE”.</p>
<p>However, there are some service plans that are only available as addons to an existing subscription, and are not available as a standalone subscription in their own right. This means that when you attempt to assign an addon subscription, it will fail unless a dependent subscription is already assigned.</p>
<p>For my Microsoft 365 Dev sandbox tenant, this wasn’t a problem, as you get E5 licences for development and testing. However, my production tenant, is licenced with the Microsoft Action Pack from being a Microsoft Partner.</p>
<p>This comes with “Office 365 E3” and “EMS E3”, which is great, however it’s limiting as I can’t develop and rollout the full feature set, so I’ve purchased the “E5 Security” addon, to make use of feature like Privileged Identity Management, Microsoft Defender for Endpoint and Microsoft Defender for Identity.</p>
<p>This is where the addon complexity came into play, as “E5 Security” is an addon that relies on service plans contained within “Office 365 E3” and “EMS E3”, so all three must be assigned together, with the dependencies assigned first.</p>
<h3 id="managing-subscriptions">Managing Subscriptions</h3>
<ul>
<li><a href="#getting-subscriptions-in-an-azure-ad-tenant">Getting subscriptions in an Azure AD tenant</a></li>
<li><a href="#exporting-subscriptions">Exporting subscriptions</a></li>
<li><a href="#evaluating-service-plan-dependencies-for-subscriptions">Evaluating service plan dependencies for subscriptions</a></li>
<li><a href="#assigning-subscriptions-to-azure-ad-groups">Assigning subscriptions to Azure AD groups</a></li>
</ul>
<h2 id="getting-subscriptions-in-an-azure-ad-tenant">Getting subscriptions in an Azure AD tenant</h2>
<p>The first function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Get-WTAzureADSubscription.ps1">Get-WTAzureADSubscription</a>, which you can access from my GitHub.</p>
<p>This gets the commercial (IE Microsoft 365) subscriptions deployed in an Azure AD tenant. I’ll be using this in the CI/CD pipeline to create a group per subscription and then assign the subscription to the group. Allowing members to then be added to the groups to be assigned those licences.</p>
<p>This requires the following Graph API permissions: Organization.Read.All, Directory.Read.All, Organization.ReadWrite.All, Directory.ReadWrite.All.</p>
<p><a href="https://docs.microsoft.com/en-us/graph/api/subscribedsku-list?view=graph-rest-1.0&tabs=http">More information can be found in the Graph APi documentation here</a></p>
<h3 id="what-does-this-do----omit-in-toc---">What does this do? <!-- omit in toc --></h3>
<ul>
<li>This sets specific variables, including the activity and the Graph Uri</li>
<li>An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline</li>
<li>The private function is then called, with the query altered as appropriate depending on the parameters</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Get-WTAzureADSubscription</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with Azure AD subscription Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with Azure AD subscription Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Azure AD subscriptions to get, this must contain valid id(s)"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"id"</span><span class="p">,</span><span class="w"> </span><span class="s2">"SubscriptionID"</span><span class="p">,</span><span class="w"> </span><span class="s2">"SubscriptionIDs"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$IDs</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Private\Invoke-WTGraphGet.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Variables</span><span class="w">
</span><span class="nv">$Activity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Getting Azure AD Commercial Subscriptions"</span><span class="w">
</span><span class="nv">$Uri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"subscribedSkus"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="nx">Activity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Activity</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$IDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"IDs"</span><span class="p">,</span><span class="w"> </span><span class="nv">$IDs</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Get Azure AD subscriptions with default properties</span><span class="w">
</span><span class="nv">$QueryResponse</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-WTGraphGet</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="nv">$Uri</span><span class="w">
</span><span class="c"># Return response if one is returned</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$QueryResponse</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$QueryResponse</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No Azure AD subscriptions exist in Azure AD, or with parameters specified"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="exporting-subscriptions">Exporting subscriptions</h2>
<p>The next function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Export-WTAzureADSubscription.ps1">Export-WTAzureADSubscription</a>, which you can access from my GitHub.</p>
<p>This exports the subscription config information from Azure AD to a JSON file. Within the pipeline this allows new subscriptions to have the config committed back to the repo for version control. This also acts as the “state” of knowing what subscriptions will have had groups created and licences assigned.</p>
<h3 id="what-does-this-do----omit-in-toc----1">What does this do? <!-- omit in toc --></h3>
<ul>
<li>This sets specific variables, including optional properties to cleanup from the config prior to export
<ul>
<li>As well as the RegEx for unsupported characters for Windows, which are replaced with underscores</li>
</ul>
</li>
<li>If no subscriptions are specified, all subscriptions are obtained unless there are specific ids provided
<ul>
<li>An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline</li>
</ul>
</li>
<li>A JSON file is then created per subscription as required</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Export-WTAzureADSubscription</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with the correct Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with the correct Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The path where the JSON file(s) will be created"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Path</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The file path where the JSON file will be created"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$FilePath</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude the cleanup operations of the policies to be exported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludeExportCleanup</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Subscriptions to get, this must contain valid id(s), when not specified, all policies are returned"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"DefinedSubscription"</span><span class="p">,</span><span class="s2">"Subscription"</span><span class="p">,</span><span class="s2">"Subscriptions"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$DefinedSubscriptions</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Subscriptions to get, this must contain valid id(s), when not specified, all policies are returned"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"id"</span><span class="p">,</span><span class="w"> </span><span class="s2">"SubscriptionID"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$SubscriptionIDs</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Get-WTAzureADSubscription.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Variables</span><span class="w">
</span><span class="nv">$CleanUpProperties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="s2">"id"</span><span class="p">,</span><span class="w">
</span><span class="s2">"createdDateTime"</span><span class="p">,</span><span class="w">
</span><span class="s2">"modifiedDateTime"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="nv">$UnsupportedCharactersRegEx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'[\\\/:*?"<>|]'</span><span class="w">
</span><span class="nv">$Counter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there are no policies to export, get policies based on specified parameters</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$DefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$SubscriptionIDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"SubscriptionIDs"</span><span class="p">,</span><span class="w"> </span><span class="nv">$IDs</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Get all Subscriptions</span><span class="w">
</span><span class="nv">$DefinedSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTAzureADSubscription</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$DefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Microsoft Graph did not return a valid response"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there are policies</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Sort and filter (if applicable) policies</span><span class="w">
</span><span class="nv">$DefinedSubscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DefinedSubscriptions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Sort-Object</span><span class="w"> </span><span class="nx">skuPartNumber</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$ExcludeExportCleanup</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$DefinedSubscriptions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Foreach-object</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Cleanup properties for export</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Property</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$CleanUpProperties</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$_</span><span class="o">.</span><span class="nf">PSObject</span><span class="o">.</span><span class="nf">Properties</span><span class="o">.</span><span class="nf">Remove</span><span class="p">(</span><span class="s2">"</span><span class="nv">$Property</span><span class="s2">"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Export to JSON</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Exporting Subscriptions (Count: </span><span class="si">$(</span><span class="nv">$DefinedSubscriptions</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">)"</span><span class="w">
</span><span class="c"># If a file path is specified, output all policies in one JSON formatted file</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$FilePath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$DefinedSubscriptions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-File</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-FilePath</span><span class="w"> </span><span class="nv">$FilePath</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$DefinedSubscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Remove characters not supported in Windows file names</span><span class="w">
</span><span class="nv">$SubscriptionDisplayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">skuPartNumber</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="nv">$UnsupportedCharactersRegEx</span><span class="p">,</span><span class="w"> </span><span class="s2">"_"</span><span class="w">
</span><span class="c"># If directory path does not exist for export, create it</span><span class="w">
</span><span class="nv">$TestPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="w"> </span><span class="nt">-PathType</span><span class="w"> </span><span class="nx">Container</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$TestPath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">New-Item</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="w"> </span><span class="nt">-ItemType</span><span class="w"> </span><span class="nx">Directory</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-Null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Output current status</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Processing Subscription </span><span class="nv">$Counter</span><span class="s2"> with file name: </span><span class="nv">$SubscriptionDisplayName</span><span class="s2">.json"</span><span class="w">
</span><span class="c"># Output individual policy JSON file</span><span class="w">
</span><span class="nv">$Subscription</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-File</span><span class="w"> </span><span class="nt">-Force</span><span class="p">:</span><span class="bp">$true</span><span class="w"> </span><span class="nt">-FilePath</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Path</span><span class="s2">\</span><span class="nv">$SubscriptionDisplayName</span><span class="s2">.json"</span><span class="w">
</span><span class="c"># Increment counter</span><span class="w">
</span><span class="nv">$Counter</span><span class="o">++</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"There are no Subscriptions to export"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="evaluating-service-plan-dependencies-for-subscriptions">Evaluating service plan dependencies for subscriptions</h2>
<p>The next function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Subscriptions/Get-WTAzureADSubscriptionDependency.ps1">Get-WTAzureADSubscriptionDependency</a>, which you can access from my GitHub.</p>
<p>There is no API that Microsoft provide for service plan dependencies for subscriptions (that I’ve found), so I had to define these myself for the subscriptions that I use.</p>
<p>I defined a <a href="https://github.com/wesley-trust/GraphAPIConfig/tree/main/AzureAD/Subscriptions/Dependencies">JSON dependency object</a>, which contains service plans that depend on other service plans. This PowerShell function then uses that definition to evaluate the subscriptions, to decide which subscriptions have dependent service plans, and so need to be grouped together with the subscriptions that contain those service plans.</p>
<h3 id="what-does-this-do----omit-in-toc----2">What does this do? <!-- omit in toc --></h3>
<ul>
<li>An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline</li>
<li>If no subscriptions are provided to the function, they’re obtained from Azure AD</li>
<li>A check is performed on each subscription, to see if any contain service plans defined as dependencies</li>
<li>If there are subscriptions with service plan dependencies, a check is performed on which subscription contains the dependent service plan</li>
<li>An object is built and returned for which subscriptions have dependent subscriptions (as they contain the dependent service plan, and so must be assigned together)
<ul>
<li>By default this returns the SkuId, which is required for assigning the licence</li>
<li>Alternatively the SkuPartNumber can be returned</li>
<li>Or just the subscriptions that contain dependent service plans</li>
</ul>
</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Get-WTAzureADSubscriptionDependency</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with Azure AD subscription Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with Azure AD subscription Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Azure AD subscriptions to check for dependencies"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"Subscription"</span><span class="p">,</span><span class="w"> </span><span class="s2">"subscribedSkus"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$Subscriptions</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Azure AD subscription service plan objects with dependencies"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"ServicePlan"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$ServicePlans</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to return the required ServicePlan or skuPartNumber instead of the default skuId of subscriptions with dependencies"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s2">"ServicePlan"</span><span class="p">,</span><span class="w"> </span><span class="s2">"SkuPartNumber"</span><span class="p">,</span><span class="w"> </span><span class="s2">"SkuId"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$DependencyType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"skuId"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Subscriptions\Get-WTAzureADSubscription.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Output current activity</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Getting Azure AD Commercial Subscription Dependencies"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there are no subscriptions, get all subscriptions</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$Subscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Get Azure AD subscriptions with default properties</span><span class="w">
</span><span class="nv">$Subscriptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTAzureADSubscription</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ServicePlans</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Output current activity</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Evaluating Service Plans for subscriptions with dependencies"</span><span class="w">
</span><span class="c"># Find subscriptions with dependencies</span><span class="w">
</span><span class="nv">$DependentSubscriptionServicePlans</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Subscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$RequiredServicePlans</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="nv">$RequiredServicePlans</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$ServicePlan</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$ServicePlans</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">servicePlans</span><span class="o">.</span><span class="nf">servicePlanName</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$ServicePlan</span><span class="o">.</span><span class="nf">ServicePlanName</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$ServicePlan</span><span class="o">.</span><span class="nf">dependency</span><span class="o">.</span><span class="nf">servicePlanName</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there are dependencies, build object to return</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RequiredServicePlans</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">skuId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Subscription</span><span class="err">.</span><span class="nx">skuId</span><span class="w">
</span><span class="nx">skuPartNumber</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Subscription</span><span class="err">.</span><span class="nx">skuPartNumber</span><span class="w">
</span><span class="nx">RequiredServicePlans</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$RequiredServicePlans</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Find the skuPartNumbers with the dependent Service Plans for each subscription with dependencies</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependencyType</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"SkuPartNumber"</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="nv">$DependencyType</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"SkuId"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Output current activity</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Evaluating SKUs containing Service Plans for subscriptions with dependencies"</span><span class="w">
</span><span class="c"># Find the skuPartNumbers with the dependent Service Plans for each subscription with dependencies</span><span class="w">
</span><span class="nv">$DependentSubscriptionSkus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependentSubscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$DependentSubscriptionServicePlans</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$RequiredSkus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Subscription</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Subscriptions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependentSubscriptionServicePlan</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$DependentSubscription</span><span class="o">.</span><span class="nf">RequiredServicePlans</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependentSubscriptionServicePlan</span><span class="w"> </span><span class="nt">-in</span><span class="w"> </span><span class="nv">$Subscription</span><span class="o">.</span><span class="nf">servicePlans</span><span class="o">.</span><span class="nf">servicePlanName</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Subscription</span><span class="o">.</span><span class="nv">$DependencyType</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RequiredSkus</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">skuId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DependentSubscription</span><span class="err">.</span><span class="nx">skuId</span><span class="w">
</span><span class="nx">skuPartNumber</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DependentSubscription</span><span class="err">.</span><span class="nx">skuPartNumber</span><span class="w">
</span><span class="s2">"Required</span><span class="nv">$DependencyType</span><span class="s2">"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$RequiredSkus</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"There are no SKUs containing Service Plans for subscriptions with dependencies"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Return dependent subscription with required skuPartNumbers</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependentSubscriptionSkus</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$DependentSubscriptionSkus</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$DependencyType</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"servicePlan"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Return dependent subscription with required servicePlans</span><span class="w">
</span><span class="nv">$DependentSubscriptionServicePlans</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No Azure AD subscription service plans to check for dependencies"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No Azure AD subscriptions exist in Azure AD, or with parameters specified"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="assigning-subscriptions-to-azure-ad-groups">Assigning subscriptions to Azure AD groups</h2>
<p>Assigning licences to groups is performed by creating an “assignLicense” group relationship.</p>
<p>This uses the <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Groups/Relationship/New-WTAzureADGroupRelationship.ps1">New-WTAzureADGroupRelationship</a> function, which you can access on my GitHub.</p>
<p><a href="/blog/graph-api-groups-relationship/#create-azure-ad-group-relationships">For more information, see the Azure AD group relationship post here</a>, which I have updated to support assigning licences.</p>Wesley TrustUsing the Graph API & PowerShell, it's possible to assign Azure AD subscription licences to groups for easy and efficient management...Azure AD groups in a CI/CD Pipeline, Stage 3: Apply & Deploy2021-04-14T00:00:00+01:002021-04-14T00:00:00+01:00https://www.wesleytrust.com/blog/graph-api-groups-pipeline-apply<p>The third stage in the Azure AD groups CI/CD pipeline applies the changes from the plan provided from the previous stage (should there be any).</p>
<p>This is the third stage, in the three stage pipeline for managing Azure AD groups:</p>
<ul>
<li><a href="/blog/graph-api-groups-pipeline-validate/">Import & Validate</a></li>
<li><a href="/blog/graph-api-groups-pipeline-plan/">Plan & Evaluate</a></li>
<li><strong>Apply & Deploy</strong></li>
</ul>
<p>This post covers the YAML and PowerShell involved in the third stage which executes the plan of actions (if any). The PowerShell can also be called directly.</p>
<table>
<thead>
<tr>
<th align="center">Current Import & Validate Status</th>
<th align="center">Current Plan & Evaluate Status</th>
<th align="center">Current Apply & Deploy Status</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><a href="https://dev.azure.com/wesleytrust/GraphAPI/_build/latest?definitionId=9&branchName=main"><img src="https://dev.azure.com/wesleytrust/GraphAPI/_apis/build/status/Azure%20AD/Groups/SVC-AD%3BENV-P%3B%20Groups?branchName=main&stageName=Validate&jobName=Import" alt="Build Status" /></a></td>
<td align="center"><a href="https://dev.azure.com/wesleytrust/GraphAPI/_build/latest?definitionId=9&branchName=main"><img src="https://dev.azure.com/wesleytrust/GraphAPI/_apis/build/status/Azure%20AD/Groups/SVC-AD%3BENV-P%3B%20Groups?branchName=main&stageName=Plan&jobName=Evaluate" alt="Build Status" /></a></td>
<td align="center"><a href="https://dev.azure.com/wesleytrust/GraphAPI/_build/latest?definitionId=9&branchName=main"><img src="https://dev.azure.com/wesleytrust/GraphAPI/_apis/build/status/Azure%20AD/Groups/SVC-AD%3BENV-P%3B%20Groups?branchName=main&stageName=Apply&jobName=Deploy" alt="Build Status" /></a></td>
</tr>
</tbody>
</table>
<p><em>The apply stage is skipped when there are no changes to deploy, and so may show as “cancelled”</em></p>
<h2 id="invoke-apply-azure-ad-group">Invoke Apply Azure AD group</h2>
<p>This function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Groups/Pipeline/Invoke-WTApplyAzureADGroup.ps1">Invoke-WTApplyAzureADGroup</a>, which you can access from my GitHub.</p>
<p>Within the pipeline, this imports the plan JSON artifact of groups, which is passed to the function via a parameter. This contains what groups that should be created, updated or removed (as appropriate).</p>
<h3 id="pipeline-yaml-example-below">Pipeline YAML example below:</h3>
<p><em>Triggered on a change to the Azure AD groups within the <a href="https://github.com/wesley-trust/GraphAPIConfig">GraphAPIConfig template repo in GitHub</a></em></p>
<p>As Azure AD groups can be created in multiple ways, and by multiple applications, having the config repo being the source of authority didn’t seem appropriate, so by default, groups are not removed if they exist in Azure AD and do not exist in the config repo. In the future I might consider a “state” file, similar to Terraform to keep track of this.</p>
<p><em>Azure Pipelines automatically downloads artifacts created in the previous stage</em></p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">Apply</span>
<span class="na">pool</span><span class="pi">:</span>
<span class="na">vmImage</span><span class="pi">:</span> <span class="s1">'</span><span class="s">windows-latest'</span>
<span class="na">dependsOn</span><span class="pi">:</span> <span class="s">Plan</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s">and(succeeded(), eq(dependencies.Plan.outputs['Evaluate.InvokeWTPlanAzureADGroup.ShouldRun'], 'true'))</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">deployment</span><span class="pi">:</span> <span class="s">Deploy</span>
<span class="na">continueOnError</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">environment</span><span class="pi">:</span> <span class="s">$(Environment)</span>
<span class="na">strategy</span><span class="pi">:</span>
<span class="na">runOnce</span><span class="pi">:</span>
<span class="na">deploy</span><span class="pi">:</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">checkout</span><span class="pi">:</span> <span class="s">self</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CloneGraphAPI</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Clone Graph API repo</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">git</span><span class="nv"> </span><span class="s">clone</span><span class="nv"> </span><span class="s">--branch</span><span class="nv"> </span><span class="s">$(Branch)</span><span class="nv"> </span><span class="s">--single-branch</span><span class="nv"> </span><span class="s">https://github.com/wesley-trust/GraphAPI.git'</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CloneToolKit</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Clone Toolkit repo</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">git</span><span class="nv"> </span><span class="s">clone</span><span class="nv"> </span><span class="s">--branch</span><span class="nv"> </span><span class="s">$(Branch)</span><span class="nv"> </span><span class="s">--single-branch</span><span class="nv"> </span><span class="s">https://github.com/wesley-trust/ToolKit.git'</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">InvokeWTApplyAzureADGroup</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Invoke-WTApplyAzureADGroup</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">inline'</span>
<span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s"># Import and convert Groups from JSON, should they exist</span>
<span class="s">$TestPath = Test-Path $(Pipeline.Workspace)\Evaluate\Plan.json -PathType Leaf</span>
<span class="s">if ($TestPath){</span>
<span class="s">$PlanAzureADGroups = Get-Content -Raw -Path $(Pipeline.Workspace)\Evaluate\Plan.json | ConvertFrom-Json -Depth 10</span>
<span class="s">}</span>
<span class="s"># Dot source and execute function</span>
<span class="s">. $(System.ArtifactsDirectory)\GraphAPI\Public\AzureAD\Groups\Pipeline\Invoke-WTApplyAzureADGroup.ps1</span>
<span class="s">Invoke-WTApplyAzureADGroup `</span>
<span class="s">-TenantDomain $(TenantDomain) `</span>
<span class="s">-ClientID ${env:CLIENTID} `</span>
<span class="s">-ClientSecret ${env:CLIENTSECRET} `</span>
<span class="s">-AzureADGroups $PlanAzureADGroups `</span>
<span class="s">-UpdateExistingGroups `</span>
<span class="s">-Path $(Build.SourcesDirectory)\AzureAD\Groups `</span>
<span class="s">-Pipeline</span>
<span class="na">pwsh</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="na">env</span><span class="pi">:</span>
<span class="na">CLIENTID</span><span class="pi">:</span> <span class="s">$(ClientID)</span>
<span class="na">CLIENTSECRET</span><span class="pi">:</span> <span class="s">$(ClientSecret)</span>
<span class="na">GITHUBPAT</span><span class="pi">:</span> <span class="s">$(GitHubPAT)</span>
<span class="na">REPOHOME</span><span class="pi">:</span> <span class="s">$(Build.Repository.LocalPath)</span>
<span class="na">BRANCH</span><span class="pi">:</span> <span class="s">$(Branch)</span>
<span class="na">GITHUBCONFIGREPO</span><span class="pi">:</span> <span class="s">$(GitHubConfigRepo)</span>
</code></pre></div></div>
</details>
<h3 id="powershell-example-below">PowerShell example below:</h3>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone repo that contains the Graph API and ToolKit functions</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nt">--branch</span><span class="w"> </span><span class="nx">main</span><span class="w"> </span><span class="nt">--single-branch</span><span class="w"> </span><span class="nx">https://github.com/wesley-trust/GraphAPI.git</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nt">--branch</span><span class="w"> </span><span class="nx">main</span><span class="w"> </span><span class="nt">--single-branch</span><span class="w"> </span><span class="nx">https://github.com/wesley-trust/ToolKit.git</span><span class="w">
</span><span class="c"># Dot source function into memory</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="o">.</span><span class="n">\GraphAPI\Public\AzureAD\Groups\Pipeline\Invoke-WTApplyAzureADGroup.ps1</span><span class="w">
</span><span class="c"># Define Variables</span><span class="w">
</span><span class="nv">$ClientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"sdg23497-sd82-983s-sdf23-dsf234kafs24"</span><span class="w">
</span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"khsdfhbdfg723498345_sdfkjbdf~-SDFFG1"</span><span class="w">
</span><span class="nv">$TenantDomain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"wesleytrustsandbox.onmicrosoft.com"</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"</span><span class="w">
</span><span class="c"># Example groups (mailNickName if missing, is auto-generated upon creation)</span><span class="w">
</span><span class="nv">$RemoveGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"41fd3497-52hq-983s-sdf23-dsf234kafs24"</span><span class="w">
</span><span class="nx">displayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"This group will be removed"</span><span class="w">
</span><span class="nx">mailEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="nx">securityEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$UpdateGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"52bf4497-f2g7-983s-sdf23-dsf234kafs24"</span><span class="w">
</span><span class="nx">displayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"This group will be updated"</span><span class="w">
</span><span class="nx">mailEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="nx">securityEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$CreateGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">displayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"This group will be created"</span><span class="w">
</span><span class="nx">mailEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="nx">securityEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Build plan object</span><span class="w">
</span><span class="nv">$PlanAzureADGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">RemoveGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$RemoveGroup</span><span class="w">
</span><span class="nx">UpdateGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$UpdateGroup</span><span class="w">
</span><span class="nx">CreateGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$CreateGroup</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Create hashtable</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">ClientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w">
</span><span class="nx">ClientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w">
</span><span class="nx">TenantDomain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="nx">UpdateExistingGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="nx">AzureADGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$PlanAzureADGroup</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Apply a plan, splatting the hashtable of parameters</span><span class="w">
</span><span class="n">Invoke-WTApplyAzureADGroup</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w">
</span><span class="c"># Or pipe specific object definitions to the apply function, with an access token previously obtained</span><span class="w">
</span><span class="nv">$PlanAzureADGroup</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Invoke-WTApplyAzureADGroup</span><span class="w"> </span><span class="nt">-AccessToken</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="c"># Or specify each parameter individually, with an access token previously obtained</span><span class="w">
</span><span class="n">Invoke-WTApplyAzureADGroup</span><span class="w"> </span><span class="nt">-AzureADGroup</span><span class="w"> </span><span class="nv">$PlanAzureADGroup</span><span class="w"> </span><span class="nt">-AccessToken</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w"> </span><span class="nt">-UpdateExistingGroups</span><span class="w">
</span></code></pre></div></div>
</details>
<h3 id="what-does-this-do----omit-in-toc---">What does this do? <!-- omit in toc --></h3>
<ul>
<li>An <a href="https://www.wesleytrust.com/blog/obtain-access-token/">access token is obtained</a>, if one is not provided, this allows the same token to be shared within the pipeline</li>
<li>If groups should be removed, and the objects exist, the group IDs are provided to the <a href="/blog/graph-api-groups/#remove-an-azure-ad-group">remove group function</a></li>
<li>If groups should be updated, and the objects exist, the group objects are provided to the <a href="/blog/graph-api-groups/#update-an-azure-ad-group">edit group function</a></li>
<li>If there are group objects to be created, the objects are provided to the <a href="/blog/graph-api-groups/#create-an-azure-ad-group">new group function</a>
<ul>
<li>The new group config information is then exported using the <a href="/blog/graph-api-groups/#export-an-azure-ad-group">export group function</a></li>
<li>This ensures the new group Ids are available in the config to manage in the future</li>
<li>Within the pipeline, the files are added, committed and pushed to the <a href="https://github.com/wesley-trust/GraphAPIConfig/tree/main/AzureAD/Groups">config repo</a></li>
</ul>
</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-WTApplyAzureADGroup</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">cmdletbinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with AzureAD Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with AzureAD Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The AzureAD group object"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s1">'AzureADGroup'</span><span class="p">,</span><span class="w"> </span><span class="s1">'GroupDefinition'</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$AzureADGroups</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to update existing groups deployed in the tenant, where the IDs match"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="w">
</span><span class="nv">$UpdateExistingGroups</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether existing groups deployed in the tenant will be removed, if not present in the import"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="w">
</span><span class="nv">$RemoveExistingGroups</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The file path to the JSON file(s) that will be exported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$FilePath</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The directory path(s) of which all JSON file(s) will be exported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Path</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether the function is operating within a pipeline"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$Pipeline</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Groups\Remove-WTAzureADGroup.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Groups\New-WTAzureADGroup.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Groups\Edit-WTAzureADGroup.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Groups\Export-WTAzureADGroup.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Deploying Azure AD Groups"</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RemoveExistingGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If groups require removing, pass the ids to the remove function</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AzureADGroups</span><span class="o">.</span><span class="nf">RemoveGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$GroupIDs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AzureADGroups</span><span class="o">.</span><span class="nf">RemoveGroups</span><span class="o">.</span><span class="nf">id</span><span class="w">
</span><span class="n">Remove-WTAzureADGroup</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="nt">-GroupIDs</span><span class="w"> </span><span class="nv">$GroupIDs</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No groups will be removed, as none exist that are different to the import"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$UpdateExistingGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If groups require updating, pass the ids</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AzureADGroups</span><span class="o">.</span><span class="nf">UpdateGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Edit-WTAzureADGroup</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="nt">-AzureADGroups</span><span class="w"> </span><span class="nv">$AzureADGroups</span><span class="o">.</span><span class="nf">UpdateGroups</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No groups will be updated, as none exist that are different to the import"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there are new groups to be created, create them, passing through the group state</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AzureADGroups</span><span class="o">.</span><span class="nf">CreateGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Create groups</span><span class="w">
</span><span class="nv">$CreatedGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-WTAzureADGroup</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-AzureADGroups</span><span class="w"> </span><span class="nv">$AzureADGroups</span><span class="o">.</span><span class="nf">CreateGroups</span><span class="w">
</span><span class="c"># Update configuration files</span><span class="w">
</span><span class="c"># Export groups</span><span class="w">
</span><span class="n">Export-WTAzureADGroup</span><span class="w"> </span><span class="nt">-AzureADGroups</span><span class="w"> </span><span class="nv">$CreatedGroups</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ExcludeExportCleanup</span><span class="w">
</span><span class="c"># If executing in a pipeline, stage, commit and push the changes back to the repo</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Pipeline</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Commit configuration changes post pipeline deployment"</span><span class="w">
</span><span class="n">Set-Location</span><span class="w"> </span><span class="nv">${ENV:REPOHOME}</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">user.email</span><span class="w"> </span><span class="nx">AzurePipeline</span><span class="err">@</span><span class="nx">wesleytrust.com</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">user.name</span><span class="w"> </span><span class="nx">AzurePipeline</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nt">-A</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">commit</span><span class="w"> </span><span class="nt">-a</span><span class="w"> </span><span class="nt">-m</span><span class="w"> </span><span class="s2">"Commit configuration changes post deployment [skip ci]"</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">push</span><span class="w"> </span><span class="nx">https://</span><span class="nv">${ENV:GITHUBPAT}</span><span class="err">@</span><span class="nx">github.com/wesley-trust/</span><span class="nv">${ENV:GITHUBCONFIGREPO}</span><span class="o">.</span><span class="nf">git</span><span class="w"> </span><span class="nx">HEAD:</span><span class="nv">${ENV:BRANCH}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No groups will be created, as none exist that are different to the import"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>Wesley TrustThis post covers the third stage in the pipeline which will be used to automate creating, updating and removing Azure AD groups...Azure AD groups in a CI/CD Pipeline, Stage 2: Plan & Evaluate2021-04-13T00:00:00+01:002021-04-13T00:00:00+01:00https://www.wesleytrust.com/blog/graph-api-groups-pipeline-plan<p>The PowerShell I’m writing, which in part mimics the stages that Terraform goes through when deploying Azure resources, adds some “smarts” to the Pipeline.</p>
<p>This is the second stage, in the three stage pipeline for managing Azure AD groups:</p>
<ul>
<li><a href="/blog/graph-api-groups-pipeline-validate/">Import & Validate</a></li>
<li><strong>Plan & Evaluate</strong></li>
<li><a href="/blog/graph-api-groups-pipeline-apply/">Apply & Deploy</a></li>
</ul>
<p>This post covers the YAML and PowerShell involved in the second stage which creates a plan of actions (if any), after evaluating the validated group input against Azure AD. The PowerShell can also be called directly.</p>
<table>
<thead>
<tr>
<th align="center">Current Import & Validate Status</th>
<th align="center">Current Plan & Evaluate Status</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><a href="https://dev.azure.com/wesleytrust/GraphAPI/_build/latest?definitionId=9&branchName=main"><img src="https://dev.azure.com/wesleytrust/GraphAPI/_apis/build/status/Azure%20AD/Groups/SVC-AD%3BENV-P%3B%20Groups?branchName=main&stageName=Validate&jobName=Import" alt="Build Status" /></a></td>
<td align="center"><a href="https://dev.azure.com/wesleytrust/GraphAPI/_build/latest?definitionId=9&branchName=main"><img src="https://dev.azure.com/wesleytrust/GraphAPI/_apis/build/status/Azure%20AD/Groups/SVC-AD%3BENV-P%3B%20Groups?branchName=main&stageName=Plan&jobName=Evaluate" alt="Build Status" /></a></td>
</tr>
</tbody>
</table>
<h2 id="invoke-plan-azure-ad-group">Invoke Plan Azure AD group</h2>
<p>This function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Groups/Pipeline/Invoke-WTPlanAzureADGroup.ps1">Invoke-WTPlanAzureADGroup</a>, which you can access from my GitHub.</p>
<p>Within the pipeline, this imports the validated JSON artifact of groups (should they exist), which is passed to the function via a parameter. This then creates a plan of what should be created, updated or removed (as appropriate).</p>
<p>Outputting a JSON plan file (as appropriate) as a pipeline artifact for the next stage in the pipeline.</p>
<h3 id="pipeline-yaml-example-below">Pipeline YAML example below:</h3>
<p><em>Triggered on a change to the Azure AD groups within the <a href="https://github.com/wesley-trust/GraphAPIConfig">GraphAPIConfig template repo in GitHub</a></em></p>
<p>As Azure AD groups can be created in multiple ways, and by multiple applications, having the config repo being the source of authority didn’t seem appropriate, so by default, groups are not removed if they exist in Azure AD and do not exist in the config repo. In the future I might consider a “state” file, similar to Terraform to keep track of this.</p>
<p><em>Azure Pipelines automatically downloads artifacts created in the previous stage</em></p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">Plan</span>
<span class="na">pool</span><span class="pi">:</span>
<span class="na">vmImage</span><span class="pi">:</span> <span class="s1">'</span><span class="s">windows-latest'</span>
<span class="na">dependsOn</span><span class="pi">:</span> <span class="s">Validate</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s">and(succeeded(), eq(dependencies.Validate.outputs['Import.InvokeWTValidateAzureADGroup.ShouldRun'], 'true'))</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">job</span><span class="pi">:</span> <span class="s">Evaluate</span>
<span class="na">continueOnError</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">DownloadPipelineArtifact@2</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">buildType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">current'</span>
<span class="na">targetPath</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(Pipeline.Workspace)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CloneGraphAPI</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Clone Graph API repo</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">git</span><span class="nv"> </span><span class="s">clone</span><span class="nv"> </span><span class="s">--branch</span><span class="nv"> </span><span class="s">$(Branch)</span><span class="nv"> </span><span class="s">--single-branch</span><span class="nv"> </span><span class="s">https://github.com/wesley-trust/GraphAPI.git'</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CloneToolKit</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Clone Toolkit repo</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">git</span><span class="nv"> </span><span class="s">clone</span><span class="nv"> </span><span class="s">--branch</span><span class="nv"> </span><span class="s">$(Branch)</span><span class="nv"> </span><span class="s">--single-branch</span><span class="nv"> </span><span class="s">https://github.com/wesley-trust/ToolKit.git'</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">InvokeWTPlanAzureADGroup</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Invoke-WTPlanAzureADGroup</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">inline'</span>
<span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s"># Import and convert Groups from JSON, should they exist</span>
<span class="s">$TestPath = Test-Path $(Pipeline.Workspace)\Import\Validate.json -PathType Leaf</span>
<span class="s">if ($TestPath){</span>
<span class="s">$ValidateAzureADGroups = Get-Content -Raw -Path $(Pipeline.Workspace)\Import\Validate.json | ConvertFrom-Json -Depth 10</span>
<span class="s">}</span>
<span class="s"># Dot source and execute function</span>
<span class="s">. $(System.ArtifactsDirectory)\GraphAPI\Public\AzureAD\Groups\Pipeline\Invoke-WTPlanAzureADGroup.ps1</span>
<span class="s">$PlanAzureADGroups = Invoke-WTPlanAzureADGroup `</span>
<span class="s">-TenantDomain $(TenantDomain) `</span>
<span class="s">-ClientID ${env:CLIENTID} `</span>
<span class="s">-ClientSecret ${env:CLIENTSECRET} `</span>
<span class="s">-AzureADGroups $ValidateAzureADGroups `</span>
<span class="s">-UpdateExistingGroups</span>
<span class="s"># Create directory for artifact, if it does not exist</span>
<span class="s">$TestPath = Test-Path $(Pipeline.Workspace)\Output -PathType Container</span>
<span class="s">if (!$TestPath){</span>
<span class="s">New-Item -Path $(Pipeline.Workspace)\Output -ItemType Directory | Out-Null</span>
<span class="s">}</span>
<span class="s"># If there are Groups</span>
<span class="s">if ($PlanAzureADGroups.RemoveGroups -or $PlanAzureADGroups.UpdateGroups -or $PlanAzureADGroups.CreateGroups){</span>
<span class="s"># Set ShouldRun variable to true, for apply stage</span>
<span class="s">echo "##vso[task.setvariable variable=ShouldRun;isOutput=true]true"</span>
<span class="s"># Convert to JSON and export</span>
<span class="s">$PlanAzureADGroups | ConvertTo-Json -Depth 10 | Out-File -Force -FilePath $(Pipeline.Workspace)\Output\Plan.json</span>
<span class="s">}</span>
<span class="na">pwsh</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="na">env</span><span class="pi">:</span>
<span class="na">CLIENTID</span><span class="pi">:</span> <span class="s">$(ClientID)</span>
<span class="na">CLIENTSECRET</span><span class="pi">:</span> <span class="s">$(ClientSecret)</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PublishPipelineArtifact@1</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetPath</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(Pipeline.Workspace)\Output'</span>
<span class="na">artifact</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Evaluate'</span>
<span class="na">publishLocation</span><span class="pi">:</span> <span class="s1">'</span><span class="s">pipeline'</span>
</code></pre></div></div>
</details>
<h3 id="powershell-example-below">PowerShell example below:</h3>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone repo that contains the Graph API and ToolKit functions</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nt">--branch</span><span class="w"> </span><span class="nx">main</span><span class="w"> </span><span class="nt">--single-branch</span><span class="w"> </span><span class="nx">https://github.com/wesley-trust/GraphAPI.git</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nt">--branch</span><span class="w"> </span><span class="nx">main</span><span class="w"> </span><span class="nt">--single-branch</span><span class="w"> </span><span class="nx">https://github.com/wesley-trust/ToolKit.git</span><span class="w">
</span><span class="c"># Dot source function into memory</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="o">.</span><span class="n">\GraphAPI\Public\AzureAD\Groups\Pipeline\Invoke-WTPlanAzureADGroup.ps1</span><span class="w">
</span><span class="c"># Define Variables</span><span class="w">
</span><span class="nv">$ClientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"sdg23497-sd82-983s-sdf23-dsf234kafs24"</span><span class="w">
</span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"khsdfhbdfg723498345_sdfkjbdf~-SDFFG1"</span><span class="w">
</span><span class="nv">$TenantDomain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"wesleytrustsandbox.onmicrosoft.com"</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"</span><span class="w">
</span><span class="c"># Example valid group (mailNickName if missing, is auto-generated upon creation)</span><span class="w">
</span><span class="nv">$ValidateAzureADGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">displayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"SVC-CA; Exclude from all Conditional Access Policies"</span><span class="w">
</span><span class="nx">mailEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="nx">securityEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Create hashtable</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">ClientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w">
</span><span class="nx">ClientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w">
</span><span class="nx">TenantDomain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="nx">UpdateExistingGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="nx">AzureADGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ValidateAzureADGroup</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Create a plan, splatting the hashtable of parameters</span><span class="w">
</span><span class="n">Invoke-WTPlanAzureADGroup</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w">
</span><span class="c"># Or pipe specific object definitions to the plan function, with an access token previously obtained</span><span class="w">
</span><span class="nv">$ValidateAzureADGroup</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Invoke-WTPlanAzureADGroup</span><span class="w"> </span><span class="nt">-AccessToken</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="c"># Or specify each parameter individually, with an access token previously obtained</span><span class="w">
</span><span class="n">Invoke-WTPlanAzureADGroup</span><span class="w"> </span><span class="nt">-AzureADGroup</span><span class="w"> </span><span class="nv">$ValidateAzureADGroup</span><span class="w"> </span><span class="nt">-AccessToken</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w"> </span><span class="nt">-UpdateExistingGroups</span><span class="w">
</span></code></pre></div></div>
</details>
<h3 id="what-does-this-do----omit-in-toc---">What does this do? <!-- omit in toc --></h3>
<ul>
<li>An <a href="https://www.wesleytrust.com/blog/obtain-access-token/">access token is obtained</a>, if one is not provided, this allows the same token to be shared within the pipeline</li>
<li>Checks are performed about whether to evaluate groups for updating or removal</li>
<li>Existing groups in Azure AD are obtained (as appropriate) from the <a href="/blog/graph-api-groups/#get-an-azure-ad-group">Get function</a>, in order to compare against the validated import</li>
<li>An object comparison is performed on the group IDs, determining:
<ul>
<li>What groups could be removed (as they exist, but don’t have an ID in the import)</li>
<li>What groups could be created (as an ID might not exist, or might not match an existing ID in Azure AD)</li>
</ul>
</li>
<li>A safety check is performed, if no groups are provided, the removal of all existing groups requires a “Force” switch</li>
<li>If groups should not be removed, the variable for removing groups is cleared</li>
<li>If groups should be updated, and there are existing groups in Azure AD, only groups with valid IDs are included</li>
<li>An object comparison is then performed on specific object properties, to check for specific differences (only)
<ul>
<li>If there are differences, they’re added to a variable</li>
</ul>
</li>
<li>If no groups exist, any imported groups must all be created, so the variable is updated</li>
<li>An object is then built containing the groups to be removed, updated or created (as appropriate)</li>
<li>This object is then returned as a plan of action, which is output as a pipeline artifact for the next stage</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-WTPlanAzureADGroup</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">cmdletbinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with Azure AD Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with Azure AD Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Azure AD group object"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s1">'AzureADGroup'</span><span class="p">,</span><span class="w"> </span><span class="s1">'GroupDefinition'</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$AzureADGroups</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to update existing groups deployed in the tenant, where the IDs match"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="w">
</span><span class="nv">$UpdateExistingGroups</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether existing groups deployed in the tenant will be removed, if not present in the import"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="w">
</span><span class="nv">$RemoveExistingGroups</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"If there are no groups to import, whether to forcibly remove any existing groups"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$Force</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"Toolkit\Public\Invoke-WTPropertyTagging.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Public\AzureAD\Groups\Get-WTAzureADGroup.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Evaluating Azure AD Groups"</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Evaluate groups if parameters exist</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RemoveExistingGroups</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="nv">$UpdateExistingGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Get existing groups for comparison</span><span class="w">
</span><span class="nv">$ExistingGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTAzureADGroup</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExistingGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AzureADGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Compare object on id and pass thru all objects, including those that exist and are to be imported</span><span class="w">
</span><span class="nv">$GroupComparison</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Compare-Object</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ReferenceObject</span><span class="w"> </span><span class="nv">$ExistingGroups</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-DifferenceObject</span><span class="w"> </span><span class="nv">$AzureADGroups</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Property</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-PassThru</span><span class="w">
</span><span class="c"># Filter for groups that should be removed, as they do not exist in the import</span><span class="w">
</span><span class="nv">$RemoveGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GroupComparison</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">sideindicator</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"<="</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="c"># Filter for groups that did not contain an id, and so are groups that should be created</span><span class="w">
</span><span class="nv">$CreateGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GroupComparison</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">sideindicator</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"=>"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If force is enabled, then if removal of groups is specified, all existing will be removed</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Force</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$RemoveGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ExistingGroups</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$RemoveExistingGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If groups are not to be removed, disregard any groups for removal</span><span class="w">
</span><span class="nv">$RemoveGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$UpdateExistingGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AzureADGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Check whether the groups that could be updated have valid ids (so can be updated, ignore the rest)</span><span class="w">
</span><span class="nv">$UpdateGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$AzureADGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">id</span><span class="w"> </span><span class="nt">-in</span><span class="w"> </span><span class="nv">$ExistingGroups</span><span class="o">.</span><span class="nf">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Group</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If groups exist, with ids that matched the import</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$UpdateGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Compare again, with all mandatory property elements for differences</span><span class="w">
</span><span class="nv">$GroupPropertyComparison</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Compare-Object</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ReferenceObject</span><span class="w"> </span><span class="nv">$ExistingGroups</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-DifferenceObject</span><span class="w"> </span><span class="nv">$UpdateGroups</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Property</span><span class="w"> </span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">displayName</span><span class="p">,</span><span class="w"> </span><span class="nx">description</span><span class="p">,</span><span class="w"> </span><span class="nx">membershipRule</span><span class="w">
</span><span class="nv">$UpdateGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GroupPropertyComparison</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">sideindicator</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"=>"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If no groups exist, any imported must be created</span><span class="w">
</span><span class="nv">$CreateGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AzureADGroups</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If no groups are to be removed or updated, any imported must be created</span><span class="w">
</span><span class="nv">$CreateGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AzureADGroups</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Build object to return</span><span class="w">
</span><span class="nv">$PlanAzureADGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">ordered</span><span class="p">]@{}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RemoveGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanAzureADGroups</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"RemoveGroups"</span><span class="p">,</span><span class="w"> </span><span class="nv">$RemoveGroups</span><span class="p">)</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Groups to remove: </span><span class="si">$(</span><span class="nv">$RemoveGroups</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$RemoveGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Remove: Group ID: </span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">DarkRed</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"No groups will be removed, as none exist that are different to the import"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$UpdateGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanAzureADGroups</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"UpdateGroups"</span><span class="p">,</span><span class="w"> </span><span class="nv">$UpdateGroups</span><span class="p">)</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Groups to update: </span><span class="si">$(</span><span class="nv">$UpdateGroups</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$UpdateGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Update: Group ID: </span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">DarkYellow</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"No groups will be updated, as none exist that are different to the import"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$CreateGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanAzureADGroups</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"CreateGroups"</span><span class="p">,</span><span class="w"> </span><span class="nv">$CreateGroups</span><span class="p">)</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Groups to create: </span><span class="si">$(</span><span class="nv">$CreateGroups</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$CreateGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Create: Group Name: </span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">displayName</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">DarkGreen</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"No groups will be created, as none exist that are different to the import"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there are groups, return PS object</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PlanAzureADGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PlanAzureADGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$PlanAzureADGroups</span><span class="w">
</span><span class="nv">$PlanAzureADGroups</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>Wesley TrustThis post covers the second stage in the pipeline which will be used to automate creating, updating and removing Azure AD groups...Azure AD groups in a CI/CD Pipeline, Stage 1: Import & Validate2021-04-12T00:00:00+01:002021-04-12T00:00:00+01:00https://www.wesleytrust.com/blog/graph-api-groups-pipeline-validate<p>Pipelines are awesome for automating Infrastructure as Code. I’m making use of <a href="https://dev.azure.com/wesleytrust/GraphAPI">Azure DevOps</a> to execute my Pipeline, but with some tweaks the YAML could run in GitHub Actions, making it relatively easy to use on either platform.</p>
<p>Managing Azure AD groups is a dependency for the Azure AD Conditional Access pipeline, as I’ll be adding groups created with this pipeline, as nested groups for (almost) every Conditional Access policy.</p>
<p><em>Both Azure Pipelines and GitHub Actions have free tiers for public projects, and free execution minutes for private projects.</em></p>
<p>For managing Azure AD groups in a pipeline, I’m taking a three stage approach consisting of:</p>
<ul>
<li><strong>Import & Validate</strong></li>
<li><a href="/blog/graph-api-groups-pipeline-plan/">Plan & Evaluate</a></li>
<li><a href="/blog/graph-api-groups-pipeline-apply/">Apply & Deploy</a></li>
</ul>
<p>This post covers the YAML and PowerShell involved in the first stage of importing and validating the input. The PowerShell can also be called directly.</p>
<table>
<thead>
<tr>
<th align="center">Current Import & Validate Status</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><a href="https://dev.azure.com/wesleytrust/GraphAPI/_build/latest?definitionId=9&branchName=main"><img src="https://dev.azure.com/wesleytrust/GraphAPI/_apis/build/status/Azure%20AD/Groups/SVC-AD%3BENV-P%3B%20Groups?branchName=main&stageName=Validate&jobName=Import" alt="Build Status" /></a></td>
</tr>
</tbody>
</table>
<h2 id="invoke-validate-azure-ad-group">Invoke Validate Azure AD group</h2>
<p>This function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Groups/Pipeline/Invoke-WTValidateAzureADGroup.ps1">Invoke-WTValidateAzureADGroup</a>, which you can access from my GitHub.</p>
<p>This imports JSON definitions of groups, or imports group objects via a parameter, and validates these against a set of criteria.</p>
<p>Outputting a JSON validate file (as appropriate) as a pipeline artifact for the next stage in the pipeline.</p>
<h3 id="pipeline-yaml-example-below">Pipeline YAML example below:</h3>
<p><em>Triggered on a change to the Azure AD groups within the <a href="https://github.com/wesley-trust/GraphAPIConfig">GraphAPIConfig template repo in GitHub</a></em></p>
<p><em>Azure Pipelines automatically clones this repo</em></p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">Validate</span>
<span class="na">pool</span><span class="pi">:</span>
<span class="na">vmImage</span><span class="pi">:</span> <span class="s1">'</span><span class="s">windows-latest'</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">job</span><span class="pi">:</span> <span class="s">Import</span>
<span class="na">pool</span><span class="pi">:</span>
<span class="na">vmImage</span><span class="pi">:</span> <span class="s1">'</span><span class="s">windows-latest'</span>
<span class="na">continueOnError</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CloneGraphAPI</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Clone Graph API repo</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">git</span><span class="nv"> </span><span class="s">clone</span><span class="nv"> </span><span class="s">--branch</span><span class="nv"> </span><span class="s">$(Branch)</span><span class="nv"> </span><span class="s">--single-branch</span><span class="nv"> </span><span class="s">https://github.com/wesley-trust/GraphAPI.git'</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">InvokeWTValidateAzureADGroup</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s">Invoke-WTValidateAzureADGroup</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">inline'</span>
<span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s"># Dot source function</span>
<span class="s">. $(System.ArtifactsDirectory)\GraphAPI\Public\AzureAD\Groups\Pipeline\Invoke-WTValidateAzureADGroup.ps1</span>
<span class="s"># Test if directory exist and execute function as appropriate</span>
<span class="s">$TestPath = Test-Path $(Build.Repository.LocalPath)\AzureAD\Groups -PathType Container</span>
<span class="s">if ($TestPath){</span>
<span class="s">$ValidateAzureADGroups = Invoke-WTValidateAzureADGroup `</span>
<span class="s">-Path $(Build.Repository.LocalPath)\AzureAD\Groups</span>
<span class="s">}</span>
<span class="s"># Create directory for artifact, if it does not exist</span>
<span class="s">$TestPath = Test-Path $(Pipeline.Workspace)\Output -PathType Container</span>
<span class="s">if (!$TestPath){</span>
<span class="s">New-Item -Path $(Pipeline.Workspace)\Output -ItemType Directory | Out-Null</span>
<span class="s">}</span>
<span class="s"># If there are Groups (as if there are no groups to import, existing groups are not removed)</span>
<span class="s">if ($ValidateAzureADGroups){</span>
<span class="no"> </span>
<span class="s"># Set ShouldRun variable to true, for plan stage</span>
<span class="s">echo "##vso[task.setvariable variable=ShouldRun;isOutput=true]true"</span>
<span class="no"> </span>
<span class="s"># Convert to JSON and export</span>
<span class="s">$ValidateAzureADGroups | ConvertTo-Json -Depth 10 | Out-File -Force -FilePath $(Pipeline.Workspace)\Output\Validate.json</span>
<span class="s">}</span>
<span class="na">pwsh</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">workingDirectory</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PublishPipelineArtifact@1</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetPath</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(Pipeline.Workspace)\Output'</span>
<span class="na">artifact</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Import'</span>
<span class="na">publishLocation</span><span class="pi">:</span> <span class="s1">'</span><span class="s">pipeline'</span>
</code></pre></div></div>
</details>
<h3 id="powershell-example-below">PowerShell example below:</h3>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone repo that contains the Graph API functions and config definitions</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nt">--branch</span><span class="w"> </span><span class="nx">main</span><span class="w"> </span><span class="nt">--single-branch</span><span class="w"> </span><span class="nx">https://github.com/wesley-trust/GraphAPI.git</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nt">--branch</span><span class="w"> </span><span class="nx">main</span><span class="w"> </span><span class="nt">--single-branch</span><span class="w"> </span><span class="nx">https://github.com/wesley-trust/GraphAPIConfig.git</span><span class="w">
</span><span class="c"># Dot source function into memory</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="o">.</span><span class="n">\GraphAPI\Public\AzureAD\Groups\Pipeline\Invoke-WTValidateAzureADGroup.ps1</span><span class="w">
</span><span class="c"># Define Variables</span><span class="w">
</span><span class="nv">$Path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">".\GraphAPIConfig\AzureAD\Groups"</span><span class="w">
</span><span class="nv">$FilePath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">".\GraphAPIConfig\AzureAD\Groups\SVC-CA\SVC-CA; Exclude from all Conditional Access Policies.json"</span><span class="w">
</span><span class="c"># Example valid group (mailNickName if missing, is auto-generated upon creation)</span><span class="w">
</span><span class="nv">$AzureADGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">displayName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"SVC-CA; Exclude from all Conditional Access Policies"</span><span class="w">
</span><span class="nx">mailEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="nx">securityEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Example invalid group (mailNickName if missing, is auto-generated upon creation)</span><span class="w">
</span><span class="c"># Missing property (displayName), as well as null property value (securityEnabled)</span><span class="w">
</span><span class="nv">$AzureADGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">mailEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="nx">securityEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Import and validate all JSON files from the path specified</span><span class="w">
</span><span class="n">Invoke-WTValidateAzureADGroup</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="w">
</span><span class="c"># Or import and validate a specific JSON file from the filepath specified</span><span class="w">
</span><span class="n">Invoke-WTValidateAzureADGroup</span><span class="w"> </span><span class="nt">-FilePath</span><span class="w"> </span><span class="nv">$FilePath</span><span class="w">
</span><span class="c"># Or pipe specific object definitions to the validate function</span><span class="w">
</span><span class="nv">$AzureADGroup</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Invoke-WTValidateAzureADGroup</span><span class="w">
</span></code></pre></div></div>
</details>
<h3 id="what-does-this-do----omit-in-toc---">What does this do? <!-- omit in toc --></h3>
<ul>
<li>This sets specific variables, including the required properties that must be present in the input</li>
<li>To import, a file path to specific files or a directory path from which all files will be imported is required
<ul>
<li>Alternatively, a group or collection of groups can also be passed in a parameter to validate</li>
</ul>
</li>
<li>This then checks for the properties each group has
<ul>
<li>Each required property that is missing is added to a variable</li>
</ul>
</li>
<li>A check is then performed as to whether the properties contain a value
<ul>
<li>This is again added to a variable if null</li>
</ul>
</li>
<li>A validate object is then built for each group with failed checks</li>
<li>Information is then returned about whether the group passed validation, and if not, why each group failed</li>
<li>If successful, the validated group objects are returned</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-WTValidateAzureADGroup</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">cmdletbinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The file path to the JSON file(s) that will be imported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$FilePath</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The directory path(s) of which all JSON file(s) will be imported"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Path</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Azure AD Groups to be validated if not imported from a JSON file"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s1">'AzureADGroup'</span><span class="p">,</span><span class="w"> </span><span class="s1">'GroupDefinition'</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$AzureADGroups</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether files should be imported only, and not validated"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ImportOnly</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Variables</span><span class="w">
</span><span class="nv">$RequiredProperties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"displayName"</span><span class="p">,</span><span class="w"> </span><span class="s2">"mailEnabled"</span><span class="p">,</span><span class="w"> </span><span class="s2">"securityEnabled"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># For each directory, get the file path of all JSON files within the directory, if the directory exists</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Path</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$PathExists</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PathExists</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$FilePath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="s2">"*.json"</span><span class="w"> </span><span class="nt">-Recurse</span><span class="p">)</span><span class="o">.</span><span class="nf">FullName</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The provided path does not exist </span><span class="nv">$Path</span><span class="s2">, please check the path is correct"</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Import groups from JSON file, if the files exist</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$FilePath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AzureADGroupImport</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$File</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$FilePath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$FilePathExists</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Test-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$File</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$FilePathExists</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Get-Content</span><span class="w"> </span><span class="nt">-Raw</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$File</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The provided filepath </span><span class="nv">$File</span><span class="s2"> does not exist, please check the path is correct"</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If import was successful, convert from JSON</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AzureADGroupImport</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AzureADGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AzureADGroupImport</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No JSON files could be imported, please check the filepath is correct"</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If a file has been imported, or objects provided in the parameter</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AzureADGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Importing Azure AD Groups"</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Groups: </span><span class="si">$(</span><span class="nv">$AzureADGroups</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$AzureADGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">displayName</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Import: Group Name: </span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">displayName</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Import: Group Id: </span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Import: Group Invalid"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If import only is set, return groups without validating</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ImportOnly</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AzureADGroups</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Output current action</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Validating Azure AD Groups"</span><span class="w">
</span><span class="c"># For each group, run validation checks</span><span class="w">
</span><span class="nv">$InvalidGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$AzureADGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$GroupValidate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="c"># Check for missing properties</span><span class="w">
</span><span class="nv">$GroupProperties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="nv">$GroupProperties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-Member</span><span class="w"> </span><span class="nt">-MemberType</span><span class="w"> </span><span class="nx">NoteProperty</span><span class="p">)</span><span class="o">.</span><span class="nf">name</span><span class="w">
</span><span class="nv">$PropertyCheck</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="c"># Check whether each required property, exists in the list of properties for the object</span><span class="w">
</span><span class="nv">$PropertyCheck</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Property</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$RequiredProperties</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Property</span><span class="w"> </span><span class="nt">-notin</span><span class="w"> </span><span class="nv">$GroupProperties</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Property</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Check whether each required property has a value, if not, return property</span><span class="w">
</span><span class="nv">$PropertyValueCheck</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$null</span><span class="w">
</span><span class="nv">$PropertyValueCheck</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Property</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$RequiredProperties</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$null</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$Group</span><span class="o">.</span><span class="nv">$Property</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Property</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Build and return object</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PropertyCheck</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="nv">$PropertyValueCheck</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$GroupValidate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">ordered</span><span class="p">]@{}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">displayName</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$GroupValidate</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"DisplayName"</span><span class="p">,</span><span class="w"> </span><span class="nv">$Group</span><span class="o">.</span><span class="nf">displayName</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$GroupValidate</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"Id"</span><span class="p">,</span><span class="w"> </span><span class="nv">$Group</span><span class="o">.</span><span class="nf">id</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PropertyCheck</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$GroupValidate</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"MissingProperties"</span><span class="p">,</span><span class="w"> </span><span class="nv">$PropertyCheck</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$PropertyValueCheck</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$GroupValidate</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"MissingPropertyValues"</span><span class="p">,</span><span class="w"> </span><span class="nv">$PropertyValueCheck</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$GroupValidate</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]</span><span class="nv">$GroupValidate</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Return validation result for each group</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$InvalidGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Invalid Groups: </span><span class="si">$(</span><span class="nv">$InvalidGroups</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2"> out of </span><span class="si">$(</span><span class="nv">$AzureADGroups</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2"> imported"</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$InvalidGroups</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">displayName</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"INVALID: Group Name: </span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">displayName</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"INVALID: Group Id: </span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">id</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"INVALID: No displayName or Id for group"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">MissingProperties</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Required properties not present (</span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">MissingProperties</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">): </span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">MissingProperties</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">MissingPropertyValues</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Required property values not present (</span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">MissingPropertyValues</span><span class="o">.</span><span class="nf">count</span><span class="si">)</span><span class="s2">): </span><span class="si">$(</span><span class="nv">$Group</span><span class="o">.</span><span class="nf">MissingPropertyValues</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Abort import</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Validation of groups was not successful, review configuration files and any warnings generated"</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Return validated groups</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"All groups have passed validation for required properties and values"</span><span class="w">
</span><span class="nv">$ValidGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AzureADGroups</span><span class="w">
</span><span class="nv">$ValidGroups</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No Azure AD groups to be imported, import may have failed or none may exist"</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>Wesley TrustThis post covers the first stage in the pipeline which will be used to automate creating, updating and removing Azure AD groups...A series of baseline definitions for Azure AD groups - GraphAPIConfig2021-04-09T00:00:00+01:002021-04-09T00:00:00+01:00https://www.wesleytrust.com/blog/graph-api-groups-config<p>This post continues the coverage of the <a href="https://github.com/wesley-trust/GraphAPIConfig">GraphAPIConfig</a> repo, which contains a set of baseline recommended configurations for the Graph API. <em>This is set up as a template, so you can duplicate this and modify as appropriate. Please always grab the latest versions from GitHub.</em></p>
<p>There are six Azure AD groups that will be used within the Conditional Access and Endpoint Manager (Intune) pipelines, as nested groups that are included in the inclusion/exclusion groups that will be created for the Conditional Access and Endpoint Manager policies.</p>
<p>The current defined groups are:</p>
<ul>
<li><a href="#all-users">All Users</a></li>
<li><a href="#all-guests">All Guests</a></li>
<li><a href="#all-devices">All Devices</a></li>
<li><a href="#all-windows-devices">All Windows Devices</a></li>
<li><a href="#svc-ca-exclude-from-all-conditional-access-policies">SVC-CA; Exclude from all Conditional Access policies</a></li>
<li><a href="#svc-em-exclude-from-all-endpoint-manager-device-policies">SVC-EM; Exclude from all Endpoint Manager device policies</a></li>
<li><a href="#svc-em-exclude-from-all-endpoint-manager-user-policies">SVC-EM; Exclude from all Endpoint Manager user policies</a></li>
</ul>
<p>The definitions of these groups are available in the <a href="https://github.com/wesley-trust/GraphAPIConfig">GraphAPIConfig</a> template repo in GitHub. By defining these groups, rather than using the inbuilt “All Users” options within Conditional Access, allows for greater customisation of each policy.</p>
<p><em>These are created with the PowerShell <a href="https://www.wesleytrust.com/blog/graph-api-groups/">Group</a> and <a href="https://www.wesleytrust.com/blog/graph-api-groups-relationship/">Group relationship</a> functions I wrote that use the Graph API, deployed in an <a href="https://www.wesleytrust.com/blog/graph-api-groups-pipeline-validate/">Azure DevOps pipeline</a>.</em></p>
<p><em>For each group, a mailNickName is required, when this does not exist, when executed in the pipeline it will be generated.</em></p>
<h2 id="all-users">All Users</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/Groups/All%20Users.json">All Users</a>, which you can access from my GitHub.</p>
<p>This has a dynamic query that includes all users (including members and external users) within the Azure AD tenant.</p>
<p>Used to target Azure AD Conditional Access and Endpoint Manager App Protection policies.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Dynamic query that includes all users (including guests and external users) within the directory"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"All Users"</span><span class="p">,</span><span class="w">
</span><span class="nl">"groupTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"DynamicMembership"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"mailEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"membershipRule"</span><span class="p">:</span><span class="w"> </span><span class="s2">"(user.objectId -ne null)"</span><span class="p">,</span><span class="w">
</span><span class="nl">"membershipRuleProcessingState"</span><span class="p">:</span><span class="w"> </span><span class="s2">"On"</span><span class="p">,</span><span class="w">
</span><span class="nl">"securityEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="all-guests">All Guests</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/Groups/All%20Guests.json">All Guests</a>, which you can access from my GitHub.</p>
<p>This has a dynamic query that includes all guests (which is all external users excluding members) within the Azure AD tenant.</p>
<p>Used to target Azure AD Conditional Access policies.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Dynamic query that includes all quests (including external users) within the directory"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"All Guests"</span><span class="p">,</span><span class="w">
</span><span class="nl">"groupTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"DynamicMembership"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"mailEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"membershipRule"</span><span class="p">:</span><span class="w"> </span><span class="s2">"(user.userType -ne </span><span class="se">\"</span><span class="s2">member</span><span class="se">\"</span><span class="s2">)"</span><span class="p">,</span><span class="w">
</span><span class="nl">"membershipRuleProcessingState"</span><span class="p">:</span><span class="w"> </span><span class="s2">"On"</span><span class="p">,</span><span class="w">
</span><span class="nl">"securityEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="all-devices">All Devices</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/Groups/All%20Devices.json">All Devices</a>, which you can access from my GitHub.</p>
<p>This has a dynamic query that includes all devices within the Azure AD tenant.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Dynamic query that includes all devices within the directory"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"All Devices"</span><span class="p">,</span><span class="w">
</span><span class="nl">"groupTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"DynamicMembership"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"mailEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"membershipRule"</span><span class="p">:</span><span class="w"> </span><span class="s2">"(device.deviceId -ne null)"</span><span class="p">,</span><span class="w">
</span><span class="nl">"membershipRuleProcessingState"</span><span class="p">:</span><span class="w"> </span><span class="s2">"On"</span><span class="p">,</span><span class="w">
</span><span class="nl">"securityEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="all-windows-devices">All Windows Devices</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/Groups/All%20Windows%20Devices.json">All Windows Devices</a>, which you can access from my GitHub.</p>
<p>This has a dynamic query that includes all Windows devices within the Azure AD tenant.</p>
<p>Used to target Endpoint Manager Device Compliance for Windows policy.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Dynamic query that includes all Windows devices within the directory"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"All Windows Devices"</span><span class="p">,</span><span class="w">
</span><span class="nl">"groupTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"DynamicMembership"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"mailEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"membershipRule"</span><span class="p">:</span><span class="w"> </span><span class="s2">"(device.deviceId -ne null) and (device.deviceOSType -eq </span><span class="se">\"</span><span class="s2">Windows</span><span class="se">\"</span><span class="s2">)"</span><span class="p">,</span><span class="w">
</span><span class="nl">"membershipRuleProcessingState"</span><span class="p">:</span><span class="w"> </span><span class="s2">"On"</span><span class="p">,</span><span class="w">
</span><span class="nl">"securityEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="svc-ca-exclude-from-all-conditional-access-policies">SVC-CA; Exclude from all Conditional Access policies</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/Groups/SVC-CA/SVC-CA%3B%20Exclude%20from%20all%20Conditional%20Access%20Policies.json">SVC-CA; Exclude from all Conditional Access policies</a>, which you can access from my GitHub.</p>
<p>This allows accounts to be added, such as break-glass accounts or others that should be excluded from all policies.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Contains the Break Glass accounts and any other account that should all be excluded from Conditional Access"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SVC-CA; Exclude from all Conditional Access Policies"</span><span class="p">,</span><span class="w">
</span><span class="nl">"mailEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"securityEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="svc-em-exclude-from-all-endpoint-manager-device-policies">SVC-EM; Exclude from all Endpoint Manager device policies</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/Groups/SVC-EM/SVC-EM%3B%20Exclude%20from%20all%20Endpoint%20Manager%20Device%20Policies.json">SVC-EM; Exclude from all Endpoint Manager device policies</a>, which you can access from my GitHub.</p>
<p>This allows accounts to be added, such as break-glass accounts or others that should be excluded from all policies.</p>
<p><em>It’s important to remember that for Endpoint Manager, you cannot mix users and devices in the same group.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Contains the Break Glass accounts and any other account that should all be excluded from Endpoint Manager"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SVC-EM; Exclude from all Endpoint Manager Device Policies"</span><span class="p">,</span><span class="w">
</span><span class="nl">"mailEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"securityEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="svc-em-exclude-from-all-endpoint-manager-user-policies">SVC-EM; Exclude from all Endpoint Manager user policies</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/Groups/SVC-EM/SVC-EM%3B%20Exclude%20from%20all%20Endpoint%20Manager%20User%20Policies.json">SVC-EM; Exclude from all Endpoint Manager user policies</a>, which you can access from my GitHub.</p>
<p>This allows accounts to be added, such as break-glass accounts or others that should be excluded from all policies.</p>
<p><em>It’s important to remember that for Endpoint Manager, you cannot mix users and devices in the same group.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Contains the Break Glass accounts and any other account that should all be excluded from Endpoint Manager"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SVC-EM; Exclude from all Endpoint Manager User Policies"</span><span class="p">,</span><span class="w">
</span><span class="nl">"mailEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"securityEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>Wesley TrustFor the Azure AD Conditional Access and Endpoint Manager (Intune) policies, I'll be using a series of dependent groups to be used in the inclusion/exclusion groups...Endpoint Manager Compliance & App Protection - GraphAPIConfig2021-04-08T00:00:00+01:002021-04-08T00:00:00+01:00https://www.wesleytrust.com/blog/graph-api-em-config<p>This post continues the coverage of the <a href="https://github.com/wesley-trust/GraphAPIConfig">GraphAPIConfig</a> repo, which contains a set of baseline recommended configurations for the Graph API. <em>This is set up as a template, so you can duplicate this and modify as appropriate. Please always grab the latest versions from GitHub.</em></p>
<p>For Endpoint Manager, I’ll be defining App Protection policies for Android and iOS (including iPadOS) and Device Compliance policies for Windows 10. This is because for Android and iOS, these are mostly used for personal devices, and for Windows, it’s more likely that they’ll be corporate devices.</p>
<p>I apply these policies to my personal Azure AD tenant, so they apply to my iPad, Android phone and Windows laptop. I don’t have a Mac…so I haven’t created a policy for that…when I have some free time I’ll flesh these out more (including device compliance for Android and iOS).</p>
<ul>
<li><a href="#app-protection-for-android">App Protection for Android</a></li>
<li><a href="#app-protection-for-ios-including-ipados">App Protection for iOS (including iPadOS)</a></li>
<li><a href="#device-compliance-for-windows-10">Device Compliance for Windows (10)</a></li>
</ul>
<p>These definitions are available in the <a href="https://github.com/wesley-trust/GraphAPIConfig">GraphAPIConfig</a> template repo in GitHub.</p>
<p><em>The policy definitions include the policy type, such as ‘androidManagedAppProtection’. This is not required to create a policy, however, when the policy type is not identified in the config, it must be defined within the Uri for the API call.</em></p>
<h2 id="app-protection-for-android">App Protection for Android</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/EndpointManager/AppManagement/Policies/ENV-P/REF-01%3BENV-P%3BVER-02%3B%20App%20Protection%20for%20Android.json">REF-01</a>, which you can access from my GitHub.</p>
<p>This sets some recommendations such as requiring app encryption, PIN (with minimum length) or biometric protection for apps, blocks rooted devices as well as access to data when offline for 12 hours.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.androidManagedAppProtection"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#deviceAppManagement/managedAppPolicies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"01"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowedAndroidDeviceManufacturers"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowedAndroidDeviceModels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"allowedDataIngestionLocations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"oneDriveForBusiness"</span><span class="p">,</span><span class="w">
</span><span class="s2">"sharePoint"</span><span class="p">,</span><span class="w">
</span><span class="s2">"camera"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"allowedDataStorageLocations"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"allowedInboundDataTransferSources"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allApps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowedOutboundClipboardSharingExceptionLength"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowedOutboundClipboardSharingLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allApps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowedOutboundDataTransferDestinations"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allApps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfAndroidDeviceManufacturerNotAllowed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfAndroidDeviceModelNotAllowed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfAndroidSafetyNetAppsVerificationFailed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfAndroidSafetyNetDeviceAttestationFailed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfDeviceComplianceRequired"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfDeviceLockNotSet"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfMaximumPinRetriesExceeded"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfUnableToAuthenticateUser"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"approvedKeyboards"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"biometricAuthenticationBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"blockAfterCompanyPortalUpdateDeferralInDays"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"blockDataIngestionIntoOrganizationDocuments"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"contactSyncBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-08T14:17:18.1393133Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"customBrowserDisplayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"customBrowserPackageId"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"customDialerAppDisplayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"customDialerAppPackageId"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"dataBackupBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"deployedAppCount"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceComplianceRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceLockRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"dialerRestrictionLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allApps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"disableAppEncryptionIfDeviceEncryptionIsEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"disableAppPinIfDevicePinIsSet"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-01;ENV-P;VER-02; App Protection for Android"</span><span class="p">,</span><span class="w">
</span><span class="nl">"encryptAppData"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"exemptedAppPackages"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"fingerprintBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"T_992343b3-1e73-4359-b80e-dc8f30559d3b"</span><span class="p">,</span><span class="w">
</span><span class="nl">"isAssigned"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"keyboardsRestricted"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"lastModifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-08T14:17:18.1393133Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"managedBrowser"</span><span class="p">:</span><span class="w"> </span><span class="s2">"notConfigured"</span><span class="p">,</span><span class="w">
</span><span class="nl">"managedBrowserToOpenLinksRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"maximumAllowedDeviceThreatLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"notConfigured"</span><span class="p">,</span><span class="w">
</span><span class="nl">"maximumPinRetries"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="nl">"maximumRequiredOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"maximumWarningOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"maximumWipeOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumPinLength"</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumRequiredAppVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumRequiredCompanyPortalVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumRequiredOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumRequiredPatchVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0000-00-00"</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWarningAppVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWarningCompanyPortalVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWarningOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWarningPatchVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0000-00-00"</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWipeAppVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWipeCompanyPortalVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWipeOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWipePatchVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0000-00-00"</span><span class="p">,</span><span class="w">
</span><span class="nl">"mobileThreatDefenseRemediationAction"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"notificationRestriction"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allow"</span><span class="p">,</span><span class="w">
</span><span class="nl">"organizationalCredentialsRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"periodBeforePinReset"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PT0S"</span><span class="p">,</span><span class="w">
</span><span class="nl">"periodOfflineBeforeAccessCheck"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PT12H"</span><span class="p">,</span><span class="w">
</span><span class="nl">"periodOfflineBeforeWipeIsEnforced"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P90D"</span><span class="p">,</span><span class="w">
</span><span class="nl">"periodOnlineBeforeAccessCheck"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PT30M"</span><span class="p">,</span><span class="w">
</span><span class="nl">"pinCharacterSet"</span><span class="p">:</span><span class="w"> </span><span class="s2">"numeric"</span><span class="p">,</span><span class="w">
</span><span class="nl">"pinRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"pinRequiredInsteadOfBiometricTimeout"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"previousPinBlockCount"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"printBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"requiredAndroidSafetyNetAppsVerificationType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"none"</span><span class="p">,</span><span class="w">
</span><span class="nl">"requiredAndroidSafetyNetDeviceAttestationType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"none"</span><span class="p">,</span><span class="w">
</span><span class="nl">"requiredAndroidSafetyNetEvaluationType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"basic"</span><span class="p">,</span><span class="w">
</span><span class="nl">"roleScopeTagIds"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"0"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"saveAsBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"screenCaptureBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"simplePinBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"targetedAppManagementLevels"</span><span class="p">:</span><span class="w"> </span><span class="s2">"unspecified"</span><span class="p">,</span><span class="w">
</span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"</span><span class="se">\"</span><span class="s2">c38a2c92-686a-407c-8b08-b8300ea42607</span><span class="se">\"</span><span class="s2">"</span><span class="p">,</span><span class="w">
</span><span class="nl">"warnAfterCompanyPortalUpdateDeferralInDays"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"wipeAfterCompanyPortalUpdateDeferralInDays"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="app-protection-for-ios-including-ipados">App Protection for iOS (including iPadOS)</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/EndpointManager/AppManagement/Policies/ENV-P/REF-02%3BENV-P%3BVER-02%3B%20App%20Protection%20for%20iOS.json">REF-02</a>, which you can access from my GitHub.</p>
<p>This sets some recommendations such as requiring app encryption, PIN (with minimum length) or biometric protection for apps, blocks jailbroken devices as well as access to data when offline for 12 hours.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"02"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#deviceAppManagement/managedAppPolicies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.iosManagedAppProtection"</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowedDataIngestionLocations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"oneDriveForBusiness"</span><span class="p">,</span><span class="w">
</span><span class="s2">"sharePoint"</span><span class="p">,</span><span class="w">
</span><span class="s2">"camera"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"allowedDataStorageLocations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"allowedInboundDataTransferSources"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allApps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowedIosDeviceModels"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowedOutboundClipboardSharingExceptionLength"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowedOutboundClipboardSharingLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allApps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowedOutboundDataTransferDestinations"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allApps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfDeviceComplianceRequired"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfIosDeviceModelNotAllowed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfMaximumPinRetriesExceeded"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"appActionIfUnableToAuthenticateUser"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"appDataEncryptionType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"whenDeviceLocked"</span><span class="p">,</span><span class="w">
</span><span class="nl">"blockDataIngestionIntoOrganizationDocuments"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"contactSyncBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-08T17:01:06.5512908Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"customBrowserProtocol"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"customDialerAppProtocol"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"dataBackupBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"deployedAppCount"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceComplianceRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"dialerRestrictionLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allApps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"disableAppPinIfDevicePinIsSet"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"disableProtectionOfManagedOutboundOpenInData"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-02;ENV-P;VER-02; App Protection for iOS"</span><span class="p">,</span><span class="w">
</span><span class="nl">"exemptedAppProtocols"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Default"</span><span class="p">,</span><span class="w">
</span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"skype;app-settings;calshow;itms;itmss;itms-apps;itms-appss;itms-services;"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"faceIdBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"filterOpenInToOnlyManagedApps"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"fingerprintBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"T_69e55462-5715-4b41-9128-4b67a76d4c64"</span><span class="p">,</span><span class="w">
</span><span class="nl">"isAssigned"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"lastModifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-08T17:01:06Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"managedBrowser"</span><span class="p">:</span><span class="w"> </span><span class="s2">"notConfigured"</span><span class="p">,</span><span class="w">
</span><span class="nl">"managedBrowserToOpenLinksRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"maximumAllowedDeviceThreatLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"notConfigured"</span><span class="p">,</span><span class="w">
</span><span class="nl">"maximumPinRetries"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="nl">"maximumRequiredOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"maximumWarningOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"maximumWipeOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumPinLength"</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumRequiredAppVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumRequiredOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumRequiredSdkVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWarningAppVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWarningOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWipeAppVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWipeOsVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"minimumWipeSdkVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"mobileThreatDefenseRemediationAction"</span><span class="p">:</span><span class="w"> </span><span class="s2">"block"</span><span class="p">,</span><span class="w">
</span><span class="nl">"notificationRestriction"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allow"</span><span class="p">,</span><span class="w">
</span><span class="nl">"organizationalCredentialsRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"periodBeforePinReset"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PT0S"</span><span class="p">,</span><span class="w">
</span><span class="nl">"periodOfflineBeforeAccessCheck"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PT12H"</span><span class="p">,</span><span class="w">
</span><span class="nl">"periodOfflineBeforeWipeIsEnforced"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P90D"</span><span class="p">,</span><span class="w">
</span><span class="nl">"periodOnlineBeforeAccessCheck"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PT30M"</span><span class="p">,</span><span class="w">
</span><span class="nl">"pinCharacterSet"</span><span class="p">:</span><span class="w"> </span><span class="s2">"numeric"</span><span class="p">,</span><span class="w">
</span><span class="nl">"pinRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"pinRequiredInsteadOfBiometricTimeout"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"previousPinBlockCount"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"printBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"protectInboundDataFromUnknownSources"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"roleScopeTagIds"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"0"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"saveAsBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"simplePinBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"targetedAppManagementLevels"</span><span class="p">:</span><span class="w"> </span><span class="s2">"unspecified"</span><span class="p">,</span><span class="w">
</span><span class="nl">"thirdPartyKeyboardsBlocked"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"</span><span class="se">\"</span><span class="s2">ca39e994-db9a-482f-a4de-7a6d3f069de2</span><span class="se">\"</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="device-compliance-for-windows-10">Device Compliance for Windows (10)</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/EndpointManager/DeviceManagement/Policies/ENV-P/REF-03%3BENV-P%3BVER-02%3B%20Device%20Compliance%20for%20Windows.json">REF-03</a>, which you can access from my GitHub.</p>
<p>This sets some recommendations such as requiring device encryption and secure boot, requiring antivirus and the firewall to be enabled.</p>
<p><em>The scheduledActionsForRule is required to create a policy, where one does not exist in the config, a default rule of notifying users and then blocking devices after 24 hours is created in the pipeline.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"03"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.windows10CompliancePolicy"</span><span class="p">,</span><span class="w">
</span><span class="nl">"roleScopeTagIds"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"0"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"e8c8b379-af43-4c5c-8df0-a72b7276148d"</span><span class="p">,</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2020-07-07T15:08:20.0938467Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"lastModifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2020-11-01T13:56:55.5928833Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-03;ENV-P;VER-02; Device Compliance for Windows"</span><span class="p">,</span><span class="w">
</span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span><span class="w">
</span><span class="nl">"passwordRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"passwordBlockSimple"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"passwordRequiredToUnlockFromIdle"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"passwordMinutesOfInactivityBeforeLock"</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="p">,</span><span class="w">
</span><span class="nl">"passwordExpirationDays"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"passwordMinimumLength"</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span><span class="w">
</span><span class="nl">"passwordMinimumCharacterSetCount"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"passwordRequiredType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"deviceDefault"</span><span class="p">,</span><span class="w">
</span><span class="nl">"passwordPreviousPasswordBlockCount"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"requireHealthyDeviceReport"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"osMinimumVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"osMaximumVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"mobileOsMinimumVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"mobileOsMaximumVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"earlyLaunchAntiMalwareDriverEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"bitLockerEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"secureBootEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"codeIntegrityEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"storageRequireEncryption"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"activeFirewallRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"defenderEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"defenderVersion"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"signatureOutOfDate"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"rtpEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"antivirusRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"antiSpywareRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceThreatProtectionEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceThreatProtectionRequiredSecurityLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"unavailable"</span><span class="p">,</span><span class="w">
</span><span class="nl">"configurationManagerComplianceRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"tpmRequired"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceCompliancePolicyScript"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"validOperatingSystemBuildRanges"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>Wesley TrustDesign of Compliance and App Protection policies for Endpoint Manager (Intune), that Conditional Access policies can then enforce...Location, location, Azure AD Named Location - GraphAPIConfig2021-04-07T00:00:00+01:002021-04-07T00:00:00+01:00https://www.wesleytrust.com/blog/graph-api-locations-config<p>This post continues the coverage of the <a href="https://github.com/wesley-trust/GraphAPIConfig">GraphAPIConfig</a> repo, which contains a set of baseline recommended configurations for the Graph API. <em>This is set up as a template, so you can duplicate this and modify as appropriate. Please always grab the latest versions from GitHub.</em></p>
<p>For the Azure AD Conditional Access policies, I’ll be defining named locations, that can be targeted within the policies. So for example, Azure AD sign-ins could be restricted to regions that typically users would be expected to sign-in from, lowering the chance of unexpected sign-in events (that could be malicious).</p>
<p>It’s important to remember, that a location policy, such as this, could be expected to be quite broad, as users could be legitimately signing-in from multiple locations, or have their location not be identified (such as if via IPv6).</p>
<p>So it’s important to combine policies such as MFA protection for administrators, for administrative actions, or sign-in or user risk to also reduce malicious sign-in events.</p>
<p><em>Within Azure AD, the countries and regions are defined with their two-letter format specified by ISO 3166-2.</em></p>
<p>I’ve created the below definitions which have been useful for me, with common trade, travel areas and countries:</p>
<ul>
<li><a href="#british-isles-common-travel-area">British Isles Common Travel Area</a></li>
<li><a href="#european-schengen-area-gibraltar">European Schengen Area, Gibraltar</a></li>
<li><a href="#european-economic-area">European Economic Area</a></li>
<li><a href="#european-free-trade-area">European Free Trade Area</a></li>
<li><a href="#european-union">European Union</a></li>
<li><a href="#trans-tasman-travel-arrangement">Trans-Tasman Travel Arrangement</a></li>
<li><a href="#united-kingdom">United Kingdom</a></li>
<li><a href="#united-states">United States</a></li>
</ul>
<p>These definitions (and more country definitions) are available in the <a href="https://github.com/wesley-trust/GraphAPIConfig">GraphAPIConfig</a> template repo in GitHub.</p>
<h2 id="british-isles-common-travel-area">British Isles Common Travel Area</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/NamedLocations/REF-01%3B%20British%20Isles%20Common%20Travel%20Area%2C%20IPv6%20and%20Unknown.json">REF-01</a>, which you can access from my GitHub.</p>
<p>This contains the UK, Ireland, Isle of Man, Jersey and Guernsey who all share a Common Travel Area (CTA).</p>
<p><em>Often combined with the <a href="#european-schengen-area-gibraltar">SA, Gibraltar</a>, <a href="#european-economic-area">EEA</a> and <a href="#european-free-trade-area">EFTA</a> to target the majority of Europe.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"01"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/namedLocations/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.countryNamedLocation"</span><span class="p">,</span><span class="w">
</span><span class="nl">"countriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"GB"</span><span class="p">,</span><span class="w">
</span><span class="s2">"IE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"IM"</span><span class="p">,</span><span class="w">
</span><span class="s2">"JE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GG"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T16:34:42.012456Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-01; British Isles Common Travel Area, IPv6 and Unknown"</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"102afe99-db6a-49d1-bdb6-45f973812aaf"</span><span class="p">,</span><span class="w">
</span><span class="nl">"includeUnknownCountriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T16:34:42.012456Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="european-schengen-area-gibraltar">European Schengen Area, Gibraltar</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/NamedLocations/REF-02%3B%20European%20Schengen%20Area%2C%20Gibraltar%2C%20IPv6%20and%20unknown.json">REF-02</a>, which you can access from my GitHub.</p>
<p>This contains all members of the Schengen Area (SA), as well as de facto members: Monaco, San Marino and the Vatican City as well as Gibraltar.</p>
<p><em>Often combined with the <a href="#british-isles-common-travel-area">CTA</a>, <a href="#european-economic-area">EEA</a> and <a href="#european-free-trade-area">EFTA</a> to target the majority of Europe.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"02"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/namedLocations/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.countryNamedLocation"</span><span class="p">,</span><span class="w">
</span><span class="nl">"countriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"BG"</span><span class="p">,</span><span class="w">
</span><span class="s2">"CZ"</span><span class="p">,</span><span class="w">
</span><span class="s2">"DK"</span><span class="p">,</span><span class="w">
</span><span class="s2">"DE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"EE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GR"</span><span class="p">,</span><span class="w">
</span><span class="s2">"ES"</span><span class="p">,</span><span class="w">
</span><span class="s2">"FR"</span><span class="p">,</span><span class="w">
</span><span class="s2">"IT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LV"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LU"</span><span class="p">,</span><span class="w">
</span><span class="s2">"HU"</span><span class="p">,</span><span class="w">
</span><span class="s2">"MT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"NL"</span><span class="p">,</span><span class="w">
</span><span class="s2">"AT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"PL"</span><span class="p">,</span><span class="w">
</span><span class="s2">"PT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"SI"</span><span class="p">,</span><span class="w">
</span><span class="s2">"SK"</span><span class="p">,</span><span class="w">
</span><span class="s2">"FI"</span><span class="p">,</span><span class="w">
</span><span class="s2">"SE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"IS"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LI"</span><span class="p">,</span><span class="w">
</span><span class="s2">"NO"</span><span class="p">,</span><span class="w">
</span><span class="s2">"CH"</span><span class="p">,</span><span class="w">
</span><span class="s2">"SM"</span><span class="p">,</span><span class="w">
</span><span class="s2">"MC"</span><span class="p">,</span><span class="w">
</span><span class="s2">"VA"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GI"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T17:00:29.0646195Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-02; European Schengen Area, Gibraltar, IPv6 and unknown"</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1a464618-4117-4814-acd2-49430ea52ae1"</span><span class="p">,</span><span class="w">
</span><span class="nl">"includeUnknownCountriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T17:00:29.0646195Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="european-economic-area">European Economic Area</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/NamedLocations/REF-03%3B%20European%20Economic%20Area%2C%20IPv6%20and%20unknown.json">REF-03</a>, which you can access from my GitHub.</p>
<p>This contains all members of the European Union as well as Iceland, Liechtenstein and Norway, forming the EEA.</p>
<p><em>Often used when targeting EU GDPR data protection residency, that applies to countries within the EEA, which are outside the <a href="#european-union">EU</a> but not <a href="#european-free-trade-area">EFTA</a>. This also is typically combined with <a href="#united-kingdom">UK</a> for UK GDPR data protection residency.</em></p>
<p><em>Often combined with the <a href="#british-isles-common-travel-area">CTA</a>, <a href="#european-schengen-area-gibraltar">SA, Gibraltar</a> and <a href="#european-free-trade-area">EFTA</a> to target the majority of Europe.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"03"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/namedLocations/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.countryNamedLocation"</span><span class="p">,</span><span class="w">
</span><span class="nl">"countriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"BE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"BG"</span><span class="p">,</span><span class="w">
</span><span class="s2">"CZ"</span><span class="p">,</span><span class="w">
</span><span class="s2">"DK"</span><span class="p">,</span><span class="w">
</span><span class="s2">"DE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"EE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"IE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GR"</span><span class="p">,</span><span class="w">
</span><span class="s2">"ES"</span><span class="p">,</span><span class="w">
</span><span class="s2">"FR"</span><span class="p">,</span><span class="w">
</span><span class="s2">"HR"</span><span class="p">,</span><span class="w">
</span><span class="s2">"IT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"CY"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LV"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LU"</span><span class="p">,</span><span class="w">
</span><span class="s2">"HU"</span><span class="p">,</span><span class="w">
</span><span class="s2">"MT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"NL"</span><span class="p">,</span><span class="w">
</span><span class="s2">"AT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"PL"</span><span class="p">,</span><span class="w">
</span><span class="s2">"PT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"RO"</span><span class="p">,</span><span class="w">
</span><span class="s2">"SI"</span><span class="p">,</span><span class="w">
</span><span class="s2">"SK"</span><span class="p">,</span><span class="w">
</span><span class="s2">"FI"</span><span class="p">,</span><span class="w">
</span><span class="s2">"SE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"IS"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LI"</span><span class="p">,</span><span class="w">
</span><span class="s2">"NO"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T14:42:58.5561438Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-03; European Economic Area, IPv6 and unknown"</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"189ca541-390a-4a36-843e-d6ee76c45b2b"</span><span class="p">,</span><span class="w">
</span><span class="nl">"includeUnknownCountriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T14:42:58.5561438Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="european-free-trade-area">European Free Trade Area</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/NamedLocations/REF-04%3B%20European%20Free%20Trade%20Area%2C%20IPv6%20and%20unknown.json">REF-04</a>, which you can access from my GitHub.</p>
<p>This contains EEA members: Iceland, Liechtenstein and Norway and non-EEA member: Switzerland, forming the EFTA.</p>
<p><em>Often combined with the <a href="#british-isles-common-travel-area">CTA</a>, <a href="#european-schengen-area-gibraltar">SA, Gibraltar</a> and <a href="#european-economic-area">EEA</a> to target the majority of Europe.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"04"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/namedLocations/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.countryNamedLocation"</span><span class="p">,</span><span class="w">
</span><span class="nl">"countriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"IS"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LI"</span><span class="p">,</span><span class="w">
</span><span class="s2">"NO"</span><span class="p">,</span><span class="w">
</span><span class="s2">"CH"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T14:43:00.2575629Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-04; European Free Trade Area, IPv6 and unknown"</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"185c3f18-c730-4500-a023-4f57ca1456ea"</span><span class="p">,</span><span class="w">
</span><span class="nl">"includeUnknownCountriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T14:43:00.2575629Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="european-union">European Union</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/NamedLocations/REF-05%3B%20European%20Union%2C%20IPv6%20and%20unknown.json">REF-05</a>, which you can access from my GitHub.</p>
<p>This contains all members of the European Union (EU) only.</p>
<p><em>This may not typically be used, as <a href="#european-schengen-area-gibraltar">SA, Gibraltar</a> or <a href="#european-economic-area">EEA</a> may be more appropriate.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"05"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/namedLocations/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.countryNamedLocation"</span><span class="p">,</span><span class="w">
</span><span class="nl">"countriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"BE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"BG"</span><span class="p">,</span><span class="w">
</span><span class="s2">"CZ"</span><span class="p">,</span><span class="w">
</span><span class="s2">"DK"</span><span class="p">,</span><span class="w">
</span><span class="s2">"DE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"EE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"IE"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GR"</span><span class="p">,</span><span class="w">
</span><span class="s2">"ES"</span><span class="p">,</span><span class="w">
</span><span class="s2">"FR"</span><span class="p">,</span><span class="w">
</span><span class="s2">"HR"</span><span class="p">,</span><span class="w">
</span><span class="s2">"IT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"CY"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LV"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"LU"</span><span class="p">,</span><span class="w">
</span><span class="s2">"HU"</span><span class="p">,</span><span class="w">
</span><span class="s2">"MT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"NL"</span><span class="p">,</span><span class="w">
</span><span class="s2">"AT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"PL"</span><span class="p">,</span><span class="w">
</span><span class="s2">"PT"</span><span class="p">,</span><span class="w">
</span><span class="s2">"RO"</span><span class="p">,</span><span class="w">
</span><span class="s2">"SI"</span><span class="p">,</span><span class="w">
</span><span class="s2">"SK"</span><span class="p">,</span><span class="w">
</span><span class="s2">"FI"</span><span class="p">,</span><span class="w">
</span><span class="s2">"SE"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T14:43:02.3787977Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-05; European Union, IPv6 and unknown"</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"13f4c7af-8f08-4b97-9d62-8cf21e6e521d"</span><span class="p">,</span><span class="w">
</span><span class="nl">"includeUnknownCountriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T14:43:02.3787977Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="trans-tasman-travel-arrangement">Trans-Tasman Travel Arrangement</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/NamedLocations/REF-06%3B%20Trans-Tasman%20Travel%20Arrangement%2C%20IPv6%20and%20Unknown.json">REF-06</a>, which you can access from my GitHub.</p>
<p>This contains Australia and New Zealand who have a travel agreement between both countries.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"06"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/namedLocations/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.countryNamedLocation"</span><span class="p">,</span><span class="w">
</span><span class="nl">"countriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"AU"</span><span class="p">,</span><span class="w">
</span><span class="s2">"NZ"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T17:44:34.6135093Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-06; Trans-Tasman Travel Arrangement, IPv6 and Unknown"</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"12a1a810-77cc-4050-862d-caed0dca1b56"</span><span class="p">,</span><span class="w">
</span><span class="nl">"includeUnknownCountriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T17:44:34.6135093Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="united-kingdom">United Kingdom</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/NamedLocations/United%20Kingdom%2C%20IPv6%20and%20unknown.json">UK</a>, which you can access from my GitHub.</p>
<p>This contains the United Kingdom of Great Britain and Northern Ireland only, using the two-digit country code “GB”.</p>
<p><em>Often used for UK GDPR data protection residency and combined with <a href="#european-economic-area">EEA</a> for EU GDPR data protection residency.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/namedLocations/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.countryNamedLocation"</span><span class="p">,</span><span class="w">
</span><span class="nl">"countriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"GB"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T17:00:27.1836021Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"United Kingdom, IPv6 and Unknown"</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1f090c87-d633-4118-bf6f-3f18a8e9be30"</span><span class="p">,</span><span class="w">
</span><span class="nl">"includeUnknownCountriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T17:00:27.1836021Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="united-states">United States</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/NamedLocations/REF-06%3B%20Trans-Tasman%20Travel%20Arrangement%2C%20IPv6%20and%20Unknown.json">US</a>, which you can access from my GitHub.</p>
<p>This contains the United States of America only, using the two-digit country code “US”.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"SVC"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/namedLocations/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.countryNamedLocation"</span><span class="p">,</span><span class="w">
</span><span class="nl">"countriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"US"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T14:43:07.2557482Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"United States, IPv6 and unknown"</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"10643880-d240-4e26-ba7e-49630255e53b"</span><span class="p">,</span><span class="w">
</span><span class="nl">"includeUnknownCountriesAndRegions"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-07T14:43:07.2557482Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>Wesley TrustFor the Azure AD Conditional Access policies, I'll be defining named locations, that can be targeted within the policies...Recommended Azure AD Conditional Access policies - GraphAPIConfig2021-04-06T00:00:00+01:002021-04-06T00:00:00+01:00https://www.wesleytrust.com/blog/graph-api-ca-config<p>Within GitHub, I’ve created the <a href="https://github.com/wesley-trust/GraphAPIConfig">GraphAPIConfig</a> repo, which contains a set of baseline recommended configurations for the Graph API. <em>This is set up as a template, so you can duplicate this and modify as appropriate. Please always grab the latest versions from GitHub.</em></p>
<p>This repo covers recommended definitions for default Azure AD groups, Conditional Access and Endpoint Manager (Intune) policies.</p>
<p>All of which will be created as part of either the Conditional Access pipeline, or a related pipeline (Azure AD groups, Endpoint Manager etc). I’ll be covering the design of these in a series of posts.</p>
<p>Firstly, to make full use of Conditional Access policies, there are dependencies of the following to consider:</p>
<ul>
<li>Azure AD
<ul>
<li><a href="https://www.wesleytrust.com/blog/graph-api-groups-config/">Groups</a></li>
<li><a href="https://www.wesleytrust.com/blog/graph-api-locations-config/">Named Locations</a></li>
</ul>
</li>
<li>Endpoint Manager
<ul>
<li><a href="https://www.wesleytrust.com/blog/graph-api-em-config/">Device Compliance</a></li>
<li><a href="https://www.wesleytrust.com/blog/graph-api-em-config/">App Protection</a></li>
</ul>
</li>
<li>Exchange Online
<ul>
<li>App enforced restrictions</li>
</ul>
</li>
<li>SharePoint Online (including OneDrive)
<ul>
<li>App enforced restrictions</li>
</ul>
</li>
</ul>
<p>I’m going to cover each of the dependencies in their own series of posts, but today I’m going to cover recommended Conditional Access baseline policies.</p>
<p><em>These aren’t intended to be fully exhaustive, they’re supposed to be customised to suit individual needs and are intended to serve as a good starting point (that I use in my personal Azure AD tenant). Azure AD P1 licensing is required to use Conditional Access.</em></p>
<p>These policies use the “beta” Microsoft Graph API (as at this date), as they make use of features in “Preview”. Typically it’s not recommended to use preview features, or a preview API in production, however, in this case, the alternative is to not make use of these features, so I consider this an acceptable risk.</p>
<h3 id="recommended-azure-ad-conditional-access-policies">Recommended Azure AD Conditional Access policies</h3>
<ul>
<li><a href="#block-access-for-all-cloud-apps-for-any-location-excluding-trusted-or-named-locations">Block access, for all cloud apps, for any location, excluding trusted or named locations</a></li>
<li><a href="#block-access-for-registering-security-information-for-any-location-excluding-trusted-or-named-locations-hybrid-joined-or-compliant-devices">Block access, for registering security information, for any location, excluding trusted or named locations, hybrid joined or compliant devices</a></li>
<li><a href="#block-access-for-guests-for-all-cloud-apps-except-approved-apps">Block access, for guests, for all cloud apps, except approved apps</a></li>
<li><a href="#block-access-for-all-cloud-apps-for-all-client-apps-supporting-legacy-authentication">Block access, for all cloud apps, for all client apps supporting legacy authentication</a></li>
<li><a href="#require-mfa-for-all-cloud-apps-for-any-location-excluding-trusted-locations-hybrid-joined-or-compliant-devices">Require MFA, for all cloud apps, for any location, excluding trusted locations, hybrid joined or compliant devices</a></li>
<li><a href="#require-hybrid-joined-or-compliant-device-for-all-cloud-apps-for-all-desktop-devices">Require hybrid joined or compliant device, for all cloud apps, for all desktop devices</a></li>
<li><a href="#require-approved-client-app-or-compliant-device-for-all-cloud-apps-for-all-mobile-devices">Require approved client app or compliant device, for all cloud apps, for all mobile devices</a></li>
<li><a href="#require-app-protection-policy-or-compliant-device-for-exchange-and-sharepoint-for-all-mobile-devices">Require app protection policy or compliant device, for Exchange and SharePoint, for all mobile devices</a></li>
<li><a href="#require-app-enforced-restrictions-for-exchange-and-sharepoint-for-all-browsers-excluding-hybrid-joined-or-compliant-devices">Require app-enforced restrictions, for Exchange and SharePoint, for all browsers, excluding hybrid joined or compliant devices</a></li>
<li><a href="#require-daily-sign-in-frequency-with-no-persistent-sessions-for-all-cloud-apps-for-all-browsers-excluding-hybrid-joined-or-compliant-devices">Require daily sign-in frequency with no persistent sessions, for all cloud apps, for all browsers, excluding hybrid joined or compliant devices</a></li>
<li><a href="#require-mfa-for-administrators-for-all-cloud-apps">Require MFA, for administrators, for all cloud apps</a></li>
<li><a href="#require-mfa-for-azure-management">Require MFA, for Azure Management</a></li>
<li><a href="#require-mfa-for-risky-sign-in-events-for-all-cloud-apps">Require MFA, for risky sign-in events, for all cloud apps</a></li>
<li><a href="#require-password-change-with-mfa-for-users-at-risk-for-all-cloud-apps">Require password change with MFA, for users at risk, for all cloud apps</a></li>
<li><a href="#require-mfa-for-registering-or-joining-devices">Require MFA, for registering or joining devices</a></li>
</ul>
<h2 id="block-access-for-all-cloud-apps-for-any-location-excluding-trusted-or-named-locations">Block access, for all cloud apps, for any location, excluding trusted or named locations</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-01%3BENV-P%3BVER-2%3B%20Block%20access%2C%20for%20all%20cloud%20apps%2C%20for%20any%20location%2C%20excluding%20trusted%20or%20named%20locations.json">REF-01</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc---">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc---">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: All cloud apps</li>
</ul>
<h4 id="conditions-----omit-in-toc---">Conditions <!-- omit in toc --></h4>
<ul>
<li>Locations: Include: Any location</li>
<li>Locations: Exclude: Selected named locations: MFA Trusted IPs, British Isles Common Travel Area, IPv6 and unknown</li>
</ul>
<p><em>It’s important to include IPv6 and unknown locations, to reduce the chance that legitimate users will be blocked</em></p>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc---">Grant <!-- omit in toc --></h4>
<ul>
<li>Block access</li>
</ul>
<h4 id="session-----omit-in-toc---">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc---">What does this do? <!-- omit in toc --></h3>
<p>This is used to restrict the ability to sign-in, to locations that are trusted (such as an office), or named locations (such as the countries that users would be likely to sign-in from). Blocking all signs-ins from locations other than these.</p>
<h3 id="why-is-this-useful----omit-in-toc---">Why is this useful? <!-- omit in toc --></h3>
<p>Where it’s reasonably safe to know where a user will be signing in from, restricting sign-ins to just these locations can reduce the likelihood of malicious sign-in attempts.</p>
<p>This can also be used in part to help meet data residency access requirements, such as restricting the ability to sign-in when outside of a certain country or area such as the <a href="https://www.wesleytrust.com/blog/graph-api-locations-config/#european-economic-area">European Economic Area</a>.</p>
<p>When combined with <a href="https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-continuous-access-evaluation">Continuous access evaluation</a>, this offers near real time protection for network location changes.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"01"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"9c6b6939-ded5-43ed-8d2a-70838fccb2ed"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"e7f5a73c-2029-4a34-8734-a21484843732"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeLocations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeLocations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"00000000-0000-0000-0000-000000000000"</span><span class="p">,</span><span class="w">
</span><span class="s2">"102afe99-db6a-49d1-bdb6-45f973812aaf"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:29.9780638Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-01;ENV-P;VER-2; Block access, for all cloud apps, for any location, excluding trusted or named locations"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"block"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"f3dc1672-18a9-493e-9b6f-e50bda10c2cc"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="block-access-for-registering-security-information-for-any-location-excluding-trusted-or-named-locations-hybrid-joined-or-compliant-devices">Block access, for registering security information, for any location, excluding trusted or named locations, hybrid joined or compliant devices</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-02%3BENV-P%3BVER-2%3B%20Block%20access%2C%20for%20registering%20security%20information%2C%20for%20any%20location%2C%20excluding%20trusted%20or%20named%20locations%2C%20hybrid%20joined%20or%20compliant%20devices.json">REF-02</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----1">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----1">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>User action: Register security information</li>
</ul>
<h4 id="conditions-----omit-in-toc----1">Conditions <!-- omit in toc --></h4>
<ul>
<li>Locations: Include: Any location</li>
<li>Locations: Exclude: Selected named locations: MFA Trusted IPs, British Isles Common Travel Area, IPv6 and unknown</li>
<li>Device state: Include: All device state</li>
<li>Device state: Exclude: Device Hybrid Azure AD joined, Device marked as compliant</li>
</ul>
<p><em>It’s important to include IPv6 and unknown locations, to reduce the chance that legitimate users will be blocked</em></p>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----1">Grant <!-- omit in toc --></h4>
<ul>
<li>Block access</li>
</ul>
<h4 id="session-----omit-in-toc----1">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----1">What does this do? <!-- omit in toc --></h3>
<p>This is used to restrict the ability to register security information (IE MFA registration), to only locations that are trusted, or named locations (such as the countries that users would be likely to sign in from). Or devices that are already hybrid joined or compliant with security policies. Blocking all attempts from locations other than these.</p>
<p><em>This can be customised to restrict further.</em></p>
<h3 id="why-is-this-useful----omit-in-toc----1">Why is this useful? <!-- omit in toc --></h3>
<p>Where it’s possible to restrict the ability to register this information to specific locations, such as only within a company office, or cloud environment, the possible attack vector is reduced so it’s less likely malicious registration could occur.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"02"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"urn:user:registersecurityinfo"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"0e9ab8c1-c538-446a-8ad9-0b2113d10ac7"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"b623e336-1aca-4837-a36d-c4feeb3d2a2d"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeLocations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeLocations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"00000000-0000-0000-0000-000000000000"</span><span class="p">,</span><span class="w">
</span><span class="s2">"102afe99-db6a-49d1-bdb6-45f973812aaf"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeDeviceStates"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeDeviceStates"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeDevices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeDevices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"Compliant"</span><span class="p">,</span><span class="w">
</span><span class="s2">"DomainJoined"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"deviceFilter"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:31.5706787Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-02;ENV-P;VER-2; Block access, for registering security information, for any location, excluding trusted or named locations, hybrid joined or compliant devices"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"block"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"50352922-ee14-4374-9378-2c275dce663b"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="block-access-for-guests-for-all-cloud-apps-except-approved-apps">Block access, for guests, for all cloud apps, except approved apps</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-03%3BENV-P%3BVER-2%3B%20Block%20access%2C%20for%20guests%2C%20for%20all%20cloud%20apps%2C%20except%20approved%20apps.json">REF-03</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----2">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Guests”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----2">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: All cloud apps</li>
<li>Cloud apps: Exclude: Office 365 Exchange Online, Office 365 SharePoint Online, Microsoft Planner, Microsoft Stream, Microsoft Teams</li>
</ul>
<h4 id="conditions-----omit-in-toc----2">Conditions <!-- omit in toc --></h4>
<ul>
<li>Client apps: Modern authentication clients: Browser, Mobile apps and desktop clients</li>
</ul>
<p><em>Other client apps will be blocked by another policy (IE disabling legacy authentication)</em></p>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----2">Grant <!-- omit in toc --></h4>
<ul>
<li>Block access</li>
</ul>
<h4 id="session-----omit-in-toc----2">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----2">What does this do? <!-- omit in toc --></h3>
<p>This is used to restrict the ability of guests and external users to only access approved cloud apps. Blocking all attempts to access other cloud apps.</p>
<p><em>This can be customised to restrict or relax the apps included within the policy.</em></p>
<h3 id="why-is-this-useful----omit-in-toc----2">Why is this useful? <!-- omit in toc --></h3>
<p>Typically it’s best to give the minimum amount of access for the tasks people will need to perform. So for example, where it’s possible to know that guests will only be using Microsoft Teams and SharePoint to collaborate, then restricting access to all other apps, such as the Azure portal enhances security.</p>
<p>As guests will then have more limited access to the cloud apps within the Azure AD tenant, this reduces the chance guests could view information they shouldn’t, or that a malicious user could attempt to exploit misconfigurations.</p>
<p>Combining this with approved external domains for sharing within SharePoint Online will enhance security further.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"03"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"browser"</span><span class="p">,</span><span class="w">
</span><span class="s2">"mobileAppsAndDesktopClients"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"00000002-0000-0ff1-ce00-000000000000"</span><span class="p">,</span><span class="w">
</span><span class="s2">"00000003-0000-0ff1-ce00-000000000000"</span><span class="p">,</span><span class="w">
</span><span class="s2">"00000004-0000-0ff1-ce00-000000000000"</span><span class="p">,</span><span class="w">
</span><span class="s2">"09abbdfd-ed23-44ee-a2d9-a627aa1c90f3"</span><span class="p">,</span><span class="w">
</span><span class="s2">"2634dd23-5e5a-431c-81ca-11710d9079f4"</span><span class="p">,</span><span class="w">
</span><span class="s2">"cc15fd57-2c6c-4117-a88c-83b1d56b4bbe"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"44e33914-4a7e-46f8-85f7-c10f75c39fb0"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"51864daf-1d49-4d7c-bade-3aeea99c458c"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:33.1289159Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-03;ENV-P;VER-2; Block access, for guests, for all cloud apps, except approved apps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"block"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3fc2f375-20ba-4f8c-94c8-3aad88e2638d"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="block-access-for-all-cloud-apps-for-all-client-apps-supporting-legacy-authentication">Block access, for all cloud apps, for all client apps supporting legacy authentication</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-04%3BENV-P%3BVER-2%3B%20Block%20access%2C%20for%20all%20cloud%20apps%2C%20for%20all%20client%20apps%20supporting%20legacy%20authentication.json">REF-04</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----3">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----3">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: All cloud apps</li>
</ul>
<h4 id="conditions-----omit-in-toc----3">Conditions <!-- omit in toc --></h4>
<ul>
<li>Client apps: Legacy authentication clients: Exchange ActiveSync clients, Other clients</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----3">Grant <!-- omit in toc --></h4>
<ul>
<li>Block access</li>
</ul>
<h4 id="session-----omit-in-toc----3">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----3">What does this do? <!-- omit in toc --></h3>
<p>This is used to restrict the ability of users to only authenticate with modern authentication clients. Blocking all attempts to access using legacy authentication clients.</p>
<p><em>This can be customised to allow ActiveSync, but this is not recommended as app protection policies will not apply.</em></p>
<h3 id="why-is-this-useful----omit-in-toc----3">Why is this useful? <!-- omit in toc --></h3>
<p>This is a <a href="https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/concept-fundamentals-security-defaults">Microsoft best practice</a>, as legacy authentication clients do not support the latest security features such as Conditional Access conditions like multi-factor authentication. This forms part of Microsoft Security Defaults for new Azure AD tenants.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"04"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"exchangeActiveSync"</span><span class="p">,</span><span class="w">
</span><span class="s2">"other"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"e3560bd8-19a9-464a-a37b-61a05fa6fef7"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"d77bcccc-1a7e-4d71-80d3-64bbcbff2bfc"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:35.0719324Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-04;ENV-P;VER-2; Block access, for all cloud apps, for all client apps supporting legacy authentication"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"block"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"794fdafa-39cc-4219-9df3-7d3e804dd779"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-mfa-for-all-cloud-apps-for-any-location-excluding-trusted-locations-hybrid-joined-or-compliant-devices">Require MFA, for all cloud apps, for any location, excluding trusted locations, hybrid joined or compliant devices</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-05%3BENV-P%3BVER-2%3B%20Require%20MFA%2C%20for%20all%20cloud%20apps%2C%20for%20any%20location%2C%20excluding%20trusted%20locations%2C%20hybrid%20joined%20or%20compliant%20devices.json">REF-05</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----4">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----4">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: All cloud apps</li>
</ul>
<h4 id="conditions-----omit-in-toc----4">Conditions <!-- omit in toc --></h4>
<ul>
<li>Locations: Include: Any location</li>
<li>Locations: Exclude: All trusted locations</li>
<li>Device state: Include: All device state</li>
<li>Device state: Exclude: Device Hybrid Azure AD joined, Device marked as compliant</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----4">Grant <!-- omit in toc --></h4>
<ul>
<li>Grant access: Require multi-factor authentication</li>
</ul>
<h4 id="session-----omit-in-toc----4">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----4">What does this do? <!-- omit in toc --></h3>
<p>This requires that users are challenged for multi-factor authentication during sign-in for compatible client apps. Unless they’re within a trusted location or have devices that are Hybrid Azure AD joined or marked as compliant.</p>
<p>As trusted locations are excluded, as well as specific devices, locations marked as trusted should be limited and devices that are Windows AD joined should be within a trust boundary (IE can only be joined by administrators or a trusted join workflow where security of the endpoint can be assured).</p>
<p>When used in combination with <a href="#require-hybrid-joined-or-compliant-device-for-all-cloud-apps-for-all-desktop-devices">REF-06</a>, common scenarios might include a Windows Virtual Desktop, or Windows Server multi-session environment, which are locked down within a cloud provider. When combined with Endpoint Manager, device security can be assured with a compliance policy which can be revoked.</p>
<h3 id="why-is-this-useful----omit-in-toc----4">Why is this useful? <!-- omit in toc --></h3>
<p>This is a <a href="https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/concept-fundamentals-security-defaults">Microsoft best practice</a>, where it’s recommend that users verify their identity where possible to reduce the chance that a compromised account can complete a successful sign-in.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"05"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"1217be06-fdb0-4ad4-bd2b-be95fa5cd39e"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"1e578ca6-5b86-48e1-ab8a-63dfea8b7374"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeLocations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeLocations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"AllTrusted"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeDeviceStates"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeDeviceStates"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeDevices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeDevices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"Compliant"</span><span class="p">,</span><span class="w">
</span><span class="s2">"DomainJoined"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"deviceFilter"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:36.8989474Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-05;ENV-P;VER-2; Require MFA, for all cloud apps, for any location, excluding trusted locations, hybrid joined or compliant devices"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"mfa"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"457c0169-b8d3-4469-9c16-9415ebce5a52"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-hybrid-joined-or-compliant-device-for-all-cloud-apps-for-all-desktop-devices">Require hybrid joined or compliant device, for all cloud apps, for all desktop devices</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-06%3BENV-P%3BVER-2%3B%20Require%20hybrid%20joined%20or%20compliant%20device%2C%20for%20all%20cloud%20apps%2C%20for%20all%20desktop%20devices.json">REF-06</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----5">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----5">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: All cloud apps</li>
</ul>
<h4 id="conditions-----omit-in-toc----5">Conditions <!-- omit in toc --></h4>
<ul>
<li>Device platforms: Include: Any device</li>
<li>Device platforms: Exclude: Android, iOS, Windows Phone</li>
<li>Client apps: Modern authentication clients</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----5">Grant <!-- omit in toc --></h4>
<ul>
<li>Grant access: Require device to be marked as compliant, Require Hybrid Azure AD joined device</li>
</ul>
<p><em>Require one of the selected controls</em></p>
<h4 id="session-----omit-in-toc----5">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----5">What does this do? <!-- omit in toc --></h3>
<p>This requires that users must sign-in from a device Hybrid Azure AD joined or marked as compliant. Applying to modern authentication clients only as legacy authentication is already blocked by another policy. This is a ‘catch all’ policy, so all device platforms are targeted, with mobile platforms excluded as they will be targeted in a separate policy.</p>
<p>This means that for desktop operating systems, device based MDM will be the only option, rather than user based “App Protection” policies which for desktop operating systems isn’t the most appropriate choice, as the majority will be corporate devices, than personally owned.</p>
<h3 id="why-is-this-useful----omit-in-toc----5">Why is this useful? <!-- omit in toc --></h3>
<p>This restricts access to devices where you’re able to verify the security, such as requiring encryption in an <a href="https://www.wesleytrust.com/blog/graph-api-em-config/">Endpoint Manager Compliance policy</a> so you can keep data safe when accessed on these devices.</p>
<p>This also allows for Hybrid Azure AD joined devices, such as Windows Virtual Desktop multi-session environments. Which as of this date, cannot be enrolled in Endpoint Manager (as it’s Windows Server based).</p>
<p><em>This is where it’s important to control the ability to join devices to Windows AD and ensure security policies are applied as appropriate.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"06"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"mobileAppsAndDesktopClients"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"aa1ab765-496c-4920-871f-96835e961771"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"3c1b73af-a7a2-4670-8dbb-5827184cc849"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includePlatforms"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludePlatforms"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"android"</span><span class="p">,</span><span class="w">
</span><span class="s2">"iOS"</span><span class="p">,</span><span class="w">
</span><span class="s2">"windowsPhone"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:38.6351451Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-06;ENV-P;VER-2; Require hybrid joined or compliant device, for all cloud apps, for all desktop devices"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"compliantDevice"</span><span class="p">,</span><span class="w">
</span><span class="s2">"domainJoinedDevice"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"97458381-1ece-4bc6-8a0e-7bd00ea53ab5"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-approved-client-app-or-compliant-device-for-all-cloud-apps-for-all-mobile-devices">Require approved client app or compliant device, for all cloud apps, for all mobile devices</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-07%3BENV-P%3BVER-2%3B%20Require%20approved%20client%20app%20or%20compliant%20device%2C%20for%20all%20cloud%20apps%2C%20for%20all%20mobile%20devices.json">REF-07</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----6">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----6">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: All cloud apps</li>
</ul>
<h4 id="conditions-----omit-in-toc----6">Conditions <!-- omit in toc --></h4>
<ul>
<li>Device platforms: Include: Any device</li>
<li>Device platforms: Exclude: Windows, macOS</li>
<li>Client apps: Modern authentication clients</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----6">Grant <!-- omit in toc --></h4>
<ul>
<li>Grant access: Require device to be marked as compliant, Require approved client app</li>
</ul>
<p><em>Require one of the selected controls</em></p>
<h4 id="session-----omit-in-toc----6">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----6">What does this do? <!-- omit in toc --></h3>
<p>This requires that users must sign-in from a device marked as compliant, or use an approved client app. Applying to modern authentication clients only as legacy authentication is already blocked by another policy. This is a ‘catch all’ policy, so all device platforms are targeted, with desktop platforms excluded as they will be targeted in a separate policy.</p>
<p>This means that for mobile operating systems, there is a choice to enrol a device for MDM, typically used for corporate devices, where administrators will have full control of the device, or users must use an approved client app (where app protection policy can be applied), typically used for personal devices. This can be combined with policy <a href="#require-app-protection-policy-or-compliant-device-for-exchange-and-sharepoint-for-all-mobile-devices">REF-08</a>, to further restrict access to supported apps that have App Protection policies enforced.</p>
<h3 id="why-is-this-useful----omit-in-toc----6">Why is this useful? <!-- omit in toc --></h3>
<p>This restricts access to devices where you’re able to verify the security, such as requiring encryption in an <a href="https://www.wesleytrust.com/blog/graph-api-em-config/">Endpoint Manager Compliance policy</a> or using an approved app (which could have app encryption from an <a href="https://www.wesleytrust.com/blog/graph-api-em-config/">Endpoint Manager App Protection policy</a>), so you can keep data safe when accessed on these devices.</p>
<p>There is an important distinction here though, as these approved apps could have an app protection policy applied, but if one isn’t applied, access is still granted.</p>
<p>So there could be a situation where a user is not correctly targeted for the app protection policy, but the Conditional Access policy still allows access as they’re using an approved app. So for supported apps, <a href="#require-app-protection-policy-or-compliant-device-for-exchange-and-sharepoint-for-all-mobile-devices">REF-08</a> can also be used to require an app protection policy.</p>
<p><a href="https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-conditional-access-grant#require-approved-client-app">A list of approved apps is available here</a></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"07"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"mobileAppsAndDesktopClients"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"25ecb756-5ccc-4ba2-bab6-1e5fc8b47390"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"5ba862c8-507d-4a3f-b840-915ba909855e"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includePlatforms"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludePlatforms"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"windows"</span><span class="p">,</span><span class="w">
</span><span class="s2">"macOS"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:40.214084Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-07;ENV-P;VER-2; Require approved client app or compliant device, for all cloud apps, for all mobile devices"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"compliantDevice"</span><span class="p">,</span><span class="w">
</span><span class="s2">"approvedApplication"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"6e9de2fc-30cd-40ea-a036-5ff013837bbc"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-app-protection-policy-or-compliant-device-for-exchange-and-sharepoint-for-all-mobile-devices">Require app protection policy or compliant device, for Exchange and SharePoint, for all mobile devices</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-08%3BENV-P%3BVER-2%3B%20Require%20app%20protection%20policy%20or%20compliant%20device%2C%20for%20Exchange%20and%20SharePoint%2C%20for%20all%20mobile%20devices.json">REF-08</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----7">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----7">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: Office 365 Exchange Online, Office 365 SharePoint Online</li>
</ul>
<h4 id="conditions-----omit-in-toc----7">Conditions <!-- omit in toc --></h4>
<ul>
<li>Device platforms: Include: Any device</li>
<li>Device platforms: Exclude: Windows, macOS</li>
<li>Client apps: Modern authentication clients</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----7">Grant <!-- omit in toc --></h4>
<ul>
<li>Grant access: Require device to be marked as compliant, Require app protection policy</li>
</ul>
<p><em>Require one of the selected controls</em></p>
<h4 id="session-----omit-in-toc----7">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----7">What does this do? <!-- omit in toc --></h3>
<p>This requires that users must sign-in from a device marked as compliant, or use a client app with an app protection policy applied for when accessing Exchange or SharePoint. Applying to modern authentication clients only as legacy authentication is already blocked by another policy. This is a ‘catch all’ policy, so all device platforms are targeted, with desktop platforms excluded as they will be targeted in a separate policy.</p>
<p>This means that for mobile operating systems, there is a choice to enrol a device for MDM, typically used for corporate devices, where administrators will have full control of the device, or users must use a client app with an app protection policy applied, typically used for personal devices. This extends security for supported apps, above just requiring an approved app, which applies for apps that do not support this control.</p>
<h3 id="why-is-this-useful----omit-in-toc----7">Why is this useful? <!-- omit in toc --></h3>
<p>This restricts access to devices where you’re able to verify the security, such as requiring encryption in an <a href="https://www.wesleytrust.com/blog/graph-api-em-config/">Endpoint Manager Compliance policy</a> or using an app that requires app encryption from an <a href="https://www.wesleytrust.com/blog/graph-api-em-config/">Endpoint Manager App Protection policy</a>, so you can keep data safe when accessed on these devices.</p>
<p>There are more apps that support app protection policies, than this Conditional Access policy can currently target, so it’s important to consider the use of <a href="#require-approved-client-app-or-compliant-device-for-all-cloud-apps-for-all-mobile-devices">REF-07</a> and targeting Endpoint Manager app protection policies (even though Conditional Access cannot enforce them).</p>
<p><a href="https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-conditional-access-grant#require-app-protection-policy">More information available here</a></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"08"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"mobileAppsAndDesktopClients"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"00000002-0000-0ff1-ce00-000000000000"</span><span class="p">,</span><span class="w">
</span><span class="s2">"00000003-0000-0ff1-ce00-000000000000"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"f151ad51-a08f-4bf6-ba2d-4ecf1075ea93"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"864d3780-b0c6-43a4-8b32-53e33b5d9bd5"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includePlatforms"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludePlatforms"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"windows"</span><span class="p">,</span><span class="w">
</span><span class="s2">"macOS"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:41.7933966Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-08;ENV-P;VER-2; Require app protection policy or compliant device, for Exchange and SharePoint, for all mobile devices"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"compliantDevice"</span><span class="p">,</span><span class="w">
</span><span class="s2">"compliantApplication"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"5b10dda8-3a78-4090-af59-4627fad1e561"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-app-enforced-restrictions-for-exchange-and-sharepoint-for-all-browsers-excluding-hybrid-joined-or-compliant-devices">Require app-enforced restrictions, for Exchange and SharePoint, for all browsers, excluding hybrid joined or compliant devices</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-09%3BENV-P%3BVER-2%3B%20Require%20app-enforced%20restrictions%2C%20for%20Exchange%20and%20SharePoint%2C%20for%20all%20browsers%2C%20excluding%20hybrid%20joined%20or%20compliant%20devices.json">REF-09</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----8">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----8">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: Office 365 Exchange Online, Office 365 SharePoint Online</li>
</ul>
<h4 id="conditions-----omit-in-toc----8">Conditions <!-- omit in toc --></h4>
<ul>
<li>Client apps: Browser</li>
<li>Device state: Include: All device state</li>
<li>Device state: Exclude: Device Hybrid Azure AD joined, Device marked as compliant</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----8">Grant <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
<h4 id="session-----omit-in-toc----8">Session <!-- omit in toc --></h4>
<ul>
<li>Use app enforced restrictions</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----8">What does this do? <!-- omit in toc --></h3>
<p>This requires that users have app-enforced restrictions (such as the inability to download data) when using a web browser accessing Exchange or SharePoint. Unless accessed using devices that are Hybrid Azure AD joined or marked as compliant.</p>
<p><em>This requires configuration changes for Exchange Online and SharePoint Online to enforce restrictions, <a href="https://docs.microsoft.com/en-gb/azure/active-directory/conditional-access/concept-conditional-access-session#application-enforced-restrictions">more info here</a>.</em></p>
<p><em>When configured in SharePoint Online, default Conditional Access policies are created and enabled, I remove these and replace with my recommended policies.</em></p>
<h3 id="why-is-this-useful----omit-in-toc----8">Why is this useful? <!-- omit in toc --></h3>
<p>This enhances data security by reducing data leakage on devices and in environments where you’re unable to verify the security, for example, as the devices may not have encryption enforced in an <a href="https://www.wesleytrust.com/blog/graph-api-em-config/">Endpoint Manager Compliance policy</a> or are not corporate owned and so are not Hybrid Azure AD joined devices.</p>
<p>This could be a security policy, where the goal is to take reasonable actions to control the flow of data, whilst balancing usability by still allowing read access to data in a web browser. As the policies are cumulative, other polices requiring MFA, or restricting access to certain locations would also apply in addition.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"09"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"browser"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"00000002-0000-0ff1-ce00-000000000000"</span><span class="p">,</span><span class="w">
</span><span class="s2">"00000003-0000-0ff1-ce00-000000000000"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"e0f518ce-ce0a-41f7-87cf-13d2d33aa896"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"e57028b1-8e6d-4cdf-8f04-88fa78a3a694"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeDeviceStates"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeDeviceStates"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeDevices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeDevices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"Compliant"</span><span class="p">,</span><span class="w">
</span><span class="s2">"DomainJoined"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"deviceFilter"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:43.4104464Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-09;ENV-P;VER-2; Require app-enforced restrictions, for Exchange and SharePoint, for all browsers, excluding hybrid joined or compliant devices"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"e9225761-c32b-4882-9c49-755508fdf427"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"cloudAppSecurity"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"signInFrequency"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"persistentBrowser"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applicationEnforcedRestrictions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"isEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-daily-sign-in-frequency-with-no-persistent-sessions-for-all-cloud-apps-for-all-browsers-excluding-hybrid-joined-or-compliant-devices">Require daily sign-in frequency with no persistent sessions, for all cloud apps, for all browsers, excluding hybrid joined or compliant devices</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-10%3BENV-P%3BVER-2%3B%20Require%20daily%20sign-in%20frequency%20with%20no%20persistent%20sessions%2C%20for%20all%20cloud%20apps%2C%20for%20all%20browsers%2C%20excluding%20hybrid%20joined%20or%20compliant%20devices.json">REF-10</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----9">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----9">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: All cloud apps</li>
</ul>
<h4 id="conditions-----omit-in-toc----9">Conditions <!-- omit in toc --></h4>
<ul>
<li>Client apps: Browser</li>
<li>Device state: Include: All device state</li>
<li>Device state: Exclude: Device Hybrid Azure AD joined, Device marked as compliant</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----9">Grant <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
<h4 id="session-----omit-in-toc----9">Session <!-- omit in toc --></h4>
<ul>
<li>Sign-in frequency: 1 day</li>
<li>Persistent browser session: Never persistent</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----9">What does this do? <!-- omit in toc --></h3>
<p>This requires that users must sign-in each day, with no persistent sessions allowed, when using a web browser. Unless accessed using devices that are Hybrid Azure AD joined or marked as compliant. This will override the “remember sign-in information” tenant wide.</p>
<h3 id="why-is-this-useful----omit-in-toc----9">Why is this useful? <!-- omit in toc --></h3>
<p>This enhances security, so if users are using a shared or guest computer for instance, sign-in information is not retained, and must be refreshed on a daily basis.</p>
<p>This attempts to find a balance between security and usability so the chance for user impersonation and malicious actions are reduced for users who may forget to sign-out, whilst still allowing them to sign-in for productivity.</p>
<p><em>This can be customised further depending on the required risk appetite and level of tolerable disruption to users.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"10"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"browser"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"fdcfdc2d-1107-4f3f-a035-bc618bdb476b"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"5983bd07-f078-4f95-a0e5-501641a52093"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeDeviceStates"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeDeviceStates"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeDevices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeDevices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"Compliant"</span><span class="p">,</span><span class="w">
</span><span class="s2">"DomainJoined"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"deviceFilter"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:45.4417413Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-10;ENV-P;VER-2; Require daily sign-in frequency with no persistent sessions, for all cloud apps, for all browsers, excluding hybrid joined or compliant devices"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ebbd0414-7ac5-4b79-8d36-b456cce0817d"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"applicationEnforcedRestrictions"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"cloudAppSecurity"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"signInFrequency"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"days"</span><span class="p">,</span><span class="w">
</span><span class="nl">"isEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"persistentBrowser"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"never"</span><span class="p">,</span><span class="w">
</span><span class="nl">"isEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-mfa-for-administrators-for-all-cloud-apps">Require MFA, for administrators, for all cloud apps</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-11%3BENV-P%3BVER-2%3B%20Require%20MFA%2C%20for%20administrators%2C%20for%20all%20cloud%20apps.json">REF-11</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----10">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Directory roles, all administrator roles are selected (so must be updated as new roles are added)</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----10">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: All cloud apps</li>
</ul>
<h4 id="conditions-----omit-in-toc----10">Conditions <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----10">Grant <!-- omit in toc --></h4>
<ul>
<li>Grant access: Require multi-factor authentication</li>
</ul>
<h4 id="session-----omit-in-toc----10">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----10">What does this do? <!-- omit in toc --></h3>
<p>This requires multi-factor authentication for all users holding administrator roles, irregardless of the app they are attempting to access, location or device.</p>
<p><em>The roles included can be customised to suit individual needs.</em></p>
<h3 id="why-is-this-useful----omit-in-toc----10">Why is this useful? <!-- omit in toc --></h3>
<p>This is a <a href="https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/concept-fundamentals-security-defaults">Microsoft best practice</a>, which enhances security as these users hold privileged roles and so every sign-in attempt is treated with elevated risk. This encourages users to not hold privileged roles permanently and instead to make use of alternatives such as Privileged Identity Management. This forms part of Microsoft Security Defaults for new Azure AD tenants.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"11"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"53a7352b-c546-47dd-9880-3bca0aa36534"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"fe930be7-5e62-47db-91af-98c3a49a38b1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"69091246-20e8-4a56-aa4d-066075b2a7a8"</span><span class="p">,</span><span class="w">
</span><span class="s2">"baf37b3a-610e-45da-9e62-d9d1e5e8914b"</span><span class="p">,</span><span class="w">
</span><span class="s2">"75941009-915a-4869-abe7-691bff18279e"</span><span class="p">,</span><span class="w">
</span><span class="s2">"f28a1f50-f6e7-4571-818b-6a12f2af6b6c"</span><span class="p">,</span><span class="w">
</span><span class="s2">"f023fd81-a637-4b56-95fd-791ac0226033"</span><span class="p">,</span><span class="w">
</span><span class="s2">"194ae4cb-b126-40b2-bd5b-6091b380977d"</span><span class="p">,</span><span class="w">
</span><span class="s2">"0964bb5e-9bdb-4d7b-ac29-58e794862a40"</span><span class="p">,</span><span class="w">
</span><span class="s2">"e8611ab8-c189-46e8-94e1-60213ab1f814"</span><span class="p">,</span><span class="w">
</span><span class="s2">"7be44c8a-adaf-4e2a-84d6-ab2649e08a13"</span><span class="p">,</span><span class="w">
</span><span class="s2">"11648597-926c-4cf3-9c36-bcebb0ba8dcc"</span><span class="p">,</span><span class="w">
</span><span class="s2">"a9ea8996-122f-4c74-9520-8edcd192826c"</span><span class="p">,</span><span class="w">
</span><span class="s2">"966707d0-3269-4727-9be2-8c3a10f19b9d"</span><span class="p">,</span><span class="w">
</span><span class="s2">"2b745bdf-0803-4d80-aa65-822c4493daac"</span><span class="p">,</span><span class="w">
</span><span class="s2">"d37c8bed-0711-4417-ba38-b4abe66ce4c2"</span><span class="p">,</span><span class="w">
</span><span class="s2">"4d6ac14f-3453-41d0-bef9-a3e0c569773a"</span><span class="p">,</span><span class="w">
</span><span class="s2">"74ef975b-6605-40af-a5d2-b9539d836353"</span><span class="p">,</span><span class="w">
</span><span class="s2">"3a2c62db-5318-420d-8d74-23affee5d9d5"</span><span class="p">,</span><span class="w">
</span><span class="s2">"8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2"</span><span class="p">,</span><span class="w">
</span><span class="s2">"729827e3-9c14-49f7-bb1b-9608f156bbb8"</span><span class="p">,</span><span class="w">
</span><span class="s2">"fdd7a751-b60b-444a-984c-02652fe8fa1c"</span><span class="p">,</span><span class="w">
</span><span class="s2">"62e90394-69f5-4237-9190-012177145e10"</span><span class="p">,</span><span class="w">
</span><span class="s2">"be2f45a1-457d-42af-a067-6ec1fa63bc45"</span><span class="p">,</span><span class="w">
</span><span class="s2">"0f971eea-41eb-4569-a71e-57bb8a3eff1e"</span><span class="p">,</span><span class="w">
</span><span class="s2">"6e591065-9bad-43ed-90f3-e9424366d2f0"</span><span class="p">,</span><span class="w">
</span><span class="s2">"29232cdf-9323-42fd-ade2-1d097af3e4de"</span><span class="p">,</span><span class="w">
</span><span class="s2">"44367163-eba1-44c3-98af-f5787879f96a"</span><span class="p">,</span><span class="w">
</span><span class="s2">"38a96431-2bdf-4b4c-8b6e-5d3d8abac1a4"</span><span class="p">,</span><span class="w">
</span><span class="s2">"b1be1c3e-b65d-4f19-8427-f6fa0d97feb9"</span><span class="p">,</span><span class="w">
</span><span class="s2">"e6d1a23a-da11-4be4-9570-befc86d067a7"</span><span class="p">,</span><span class="w">
</span><span class="s2">"17315797-102d-40b4-93e0-432062caca18"</span><span class="p">,</span><span class="w">
</span><span class="s2">"7698a772-787b-4ac8-901f-60d6b08affd2"</span><span class="p">,</span><span class="w">
</span><span class="s2">"158c047a-c907-4556-b7ef-446551a6b5f7"</span><span class="p">,</span><span class="w">
</span><span class="s2">"b0f54661-2d74-4c50-afa3-1ec803f12efe"</span><span class="p">,</span><span class="w">
</span><span class="s2">"3edaf663-341e-4475-9f94-5c398ef6c070"</span><span class="p">,</span><span class="w">
</span><span class="s2">"aaf43236-0c0d-4d5f-883a-6955382ac081"</span><span class="p">,</span><span class="w">
</span><span class="s2">"7495fdc4-34c4-4d15-a289-98788ce399fd"</span><span class="p">,</span><span class="w">
</span><span class="s2">"e3973bdf-4987-49ae-837a-ba8e231c7286"</span><span class="p">,</span><span class="w">
</span><span class="s2">"c4e39bd9-1100-46d3-8c65-fb160da0071f"</span><span class="p">,</span><span class="w">
</span><span class="s2">"9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3"</span><span class="p">,</span><span class="w">
</span><span class="s2">"644ef478-e28f-4e28-b9dc-3fdde9aa0b1f"</span><span class="p">,</span><span class="w">
</span><span class="s2">"c430b396-e693-46cc-96f3-db01bf8bb62a"</span><span class="p">,</span><span class="w">
</span><span class="s2">"0526716b-113d-4c15-b2c8-68e3c22b9f80"</span><span class="p">,</span><span class="w">
</span><span class="s2">"8329153b-31d0-4727-b945-745eb3bc5f31"</span><span class="p">,</span><span class="w">
</span><span class="s2">"eb1f4a8d-243a-41f0-9fbd-c7cdf6c5ef7c"</span><span class="p">,</span><span class="w">
</span><span class="s2">"b5a8dcf3-09d5-43a9-a639-8e29ef291470"</span><span class="p">,</span><span class="w">
</span><span class="s2">"3d762c5a-1b6c-493f-843e-55a3b42923d4"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:46.9810516Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-11;ENV-P;VER-2; Require MFA, for administrators, for all cloud apps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"mfa"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a3c22764-df26-412f-82c3-b3d9bcdbab63"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-mfa-for-azure-management">Require MFA, for Azure Management</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-12%3BENV-P%3BVER-2%3B%20Require%20MFA%2C%20for%20Azure%20Management.json">REF-12</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----11">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----11">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: Microsoft Azure Management</li>
</ul>
<h4 id="conditions-----omit-in-toc----11">Conditions <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----11">Grant <!-- omit in toc --></h4>
<ul>
<li>Grant access: Require multi-factor authentication</li>
</ul>
<h4 id="session-----omit-in-toc----11">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----11">What does this do? <!-- omit in toc --></h3>
<p>This requires multi-factor authentication for all users accessing Microsoft Azure Management.</p>
<h3 id="why-is-this-useful----omit-in-toc----11">Why is this useful? <!-- omit in toc --></h3>
<p>This is a <a href="https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/concept-fundamentals-security-defaults">Microsoft best practice</a>, which enhances security as the Azure portal allows for privileged activities, such as the ability to view, change or remove Azure resources (with the correct permissions on the resource) which does not require an Azure AD role, and so this reduces the chance of malicious activity from compromised accounts. This forms part of Microsoft Security Defaults for new Azure AD tenants.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"12"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"797f4846-ba00-4fd7-ba43-dac1f8f63013"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"8a9e3089-a6f2-4ce7-8376-d367c9a80315"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"75e2b7ce-45d6-4fd2-a88f-24e2711ab456"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:48.5904465Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-12;ENV-P;VER-2; Require MFA, for Azure Management"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"mfa"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c8107af6-4fd4-47d5-b52a-5608c43ee2d7"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-mfa-for-risky-sign-in-events-for-all-cloud-apps">Require MFA, for risky sign-in events, for all cloud apps</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-13%3BENV-P%3BVER-2%3B%20Require%20MFA%2C%20for%20risky%20sign-in%20events%2C%20for%20all%20cloud%20apps.json">REF-13</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----12">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----12">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: All cloud apps</li>
</ul>
<h4 id="conditions-----omit-in-toc----12">Conditions <!-- omit in toc --></h4>
<ul>
<li>Sign-in risk: Medium, High</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----12">Grant <!-- omit in toc --></h4>
<ul>
<li>Grant access: Require multi-factor authentication</li>
</ul>
<h4 id="session-----omit-in-toc----12">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----12">What does this do? <!-- omit in toc --></h3>
<p>This requires multi-factor authentication for sign-in events that Azure AD Identity Protection has determined to be at a medium or high risk level. This allows these users to be challenged with MFA when another policy would not have triggered this.</p>
<p><em>This uses Azure AD Identity Protection, so requires Azure AD P2 licensing.</em></p>
<h3 id="why-is-this-useful----omit-in-toc----12">Why is this useful? <!-- omit in toc --></h3>
<p>This reducing user frustration by not requiring MFA for every authentication workflow, but enhances security by still enforcing MFA for events with the highest risk or exposure to malicious activity.</p>
<p><em>This can be customised to increase or decrease the risk appetite for triggering MFA.</em></p>
<p><em>A user without MFA configured, will not be allowed to configure MFA when at risk, so it’s important that users have this configured in advance.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"13"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"high"</span><span class="p">,</span><span class="w">
</span><span class="s2">"medium"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"09497c93-b02b-41c5-acf9-2fa24f1c61df"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"34ffdbb1-0807-4c2c-8b8c-6892f6ac2103"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:50.463577Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-13;ENV-P;VER-2; Require MFA, for risky sign-in events, for all cloud apps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"mfa"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0671dc87-ed32-44f7-80c4-b8d75d4e492e"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-password-change-with-mfa-for-users-at-risk-for-all-cloud-apps">Require password change with MFA, for users at risk, for all cloud apps</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-14%3BENV-P%3BVER-2%3B%20Require%20password%20change%20with%20MFA%2C%20for%20users%20at%20risk%2C%20for%20all%20cloud%20apps.json">REF-14</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----13">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----13">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>Cloud apps: Include: All cloud apps</li>
</ul>
<h4 id="conditions-----omit-in-toc----13">Conditions <!-- omit in toc --></h4>
<ul>
<li>User risk: High</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----13">Grant <!-- omit in toc --></h4>
<ul>
<li>Grant access: Require multi-factor authentication, Require password change</li>
</ul>
<p><em>Require all of the selected controls</em></p>
<h4 id="session-----omit-in-toc----13">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----13">What does this do? <!-- omit in toc --></h3>
<p>This requires multi-factor authentication for users Azure AD Identity Protection has determined to be at a high risk level of being compromised (such as through leaked credentials). This allows these users to be challenged with MFA, which if successful, requires that they change their password.</p>
<p><em>This uses Azure AD Identity Protection, so requires Azure AD P2 licensing.</em></p>
<h3 id="why-is-this-useful----omit-in-toc----13">Why is this useful? <!-- omit in toc --></h3>
<p>This enhances security as accounts Identity Protection determines could be at risk of compromise, are required to complete MFA and then change their password, having their access blocked if they are not registered for MFA. Reducing the risk that malicious actions could be taken by the potentially compromised account.</p>
<p><em>This can be customised to increase or decrease the risk appetite for triggering the password change and also whether the user is blocked instead.</em></p>
<p><em>A user without MFA configured, will not be allowed to configure MFA when at risk, so it’s important that users have this configured in advance.</em></p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"14"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"high"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"All"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"9049a02d-ca8b-4770-824f-d58512853d72"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"5379848a-ba03-4087-a068-e923fddd6969"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-03-19T20:10:52.1294078Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-14;ENV-P;VER-2; Require password change with MFA, for users at risk, for all cloud apps"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AND"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"mfa"</span><span class="p">,</span><span class="w">
</span><span class="s2">"passwordChange"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"4a270024-e039-4cdd-8f31-be112cdab10e"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="require-mfa-for-registering-or-joining-devices">Require MFA, for registering or joining devices</h2>
<p>This definition is available here: <a href="https://github.com/wesley-trust/GraphAPIConfig/blob/main/AzureAD/ConditionalAccess/Policies/ENV-P/REF-15%3BENV-P%3BVER-2%3B%20Require%20MFA%2C%20for%20registering%20or%20joining%20devices.json">REF-15</a>, which you can access from my GitHub.</p>
<details>
<summary><em><strong>Assignments</strong></em></summary>
<h4 id="users-amp-groups-----omit-in-toc----14">Users & Groups <!-- omit in toc --></h4>
<ul>
<li>Inclusion: Group created by the pipeline, with the dynamic nested group “All Users”, added</li>
<li>Exclusion: Group created by the pipeline, with the nested group containing all accounts to be excluded, added</li>
</ul>
<h4 id="cloud-apps-or-actions-----omit-in-toc----14">Cloud apps or actions <!-- omit in toc --></h4>
<ul>
<li>User action: Register or join devices</li>
</ul>
<h4 id="conditions-----omit-in-toc----14">Conditions <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<details>
<summary><em><strong>Access controls</strong></em></summary>
<h4 id="grant-----omit-in-toc----14">Grant <!-- omit in toc --></h4>
<ul>
<li>Grant access: Require multi-factor authentication</li>
</ul>
<h4 id="session-----omit-in-toc----14">Session <!-- omit in toc --></h4>
<ul>
<li>None</li>
</ul>
</details>
<h3 id="what-does-this-do----omit-in-toc----14">What does this do? <!-- omit in toc --></h3>
<p>This requires multi-factor authentication for when users register or join their devices to Azure AD. When enrolling a device in Endpoint Manager (Intune) or using an App with an “App Protection” policy applied, the device is registered in Azure AD, and so would also be required to complete MFA.</p>
<p><em>This replaces the tenant wide setting, allowing customisation, such as including location based exclusions.</em></p>
<h3 id="why-is-this-useful----omit-in-toc----14">Why is this useful? <!-- omit in toc --></h3>
<p>This enhances security as registering or joining devices to Azure AD is a privileged activity, as devices may then be able to access corporate data. This verifies the sign-in event reducing the risk that the account has been compromised and malicious actions may be performed.</p>
<p>Example below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"REF"</span><span class="p">:</span><span class="w"> </span><span class="s2">"15"</span><span class="p">,</span><span class="w">
</span><span class="nl">"VER"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ENV"</span><span class="p">:</span><span class="w"> </span><span class="s2">"P"</span><span class="p">,</span><span class="w">
</span><span class="nl">"@odata.context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"</span><span class="p">,</span><span class="w">
</span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"userRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"signInRiskLevels"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"clientAppTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"all"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"locations"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"deviceStates"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"devices"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"clientApplications"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"applications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeApplications"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeUserActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"urn:user:registerdevice"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeAuthenticationContextClassReferences"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"users"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"includeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeUsers"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"includeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"1a00c4a2-e3c9-4d9e-a91e-2e69f828a2e6"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"excludeGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"c61e4a0b-56c8-4cc6-96ec-0878f929f56c"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"includeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"excludeRoles"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"createdDateTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2021-04-04T12:54:42.2651265Z"</span><span class="p">,</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REF-15;ENV-P;VER-2; Require MFA, for registering or joining devices"</span><span class="p">,</span><span class="w">
</span><span class="nl">"grantControls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"operator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OR"</span><span class="p">,</span><span class="w">
</span><span class="nl">"builtInControls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"mfa"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"customAuthenticationFactors"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
</span><span class="nl">"termsOfUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c9c3eb48-2d32-4014-aae7-aa613b529d07"</span><span class="p">,</span><span class="w">
</span><span class="nl">"modifiedDateTime"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"sessionControls"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>Wesley TrustI've designed a set of recommended baseline policies for Azure AD Conditional Access based on my experience and research...Manage Azure AD group relationships via the Graph API & PowerShell2021-04-01T00:00:00+01:002021-04-01T00:00:00+01:00https://www.wesleytrust.com/blog/graph-api-groups-relationship<p>Managing Azure AD group members is a dependency for the Conditional Access policies, as for (almost) every policy I’ll be adding members to groups. Such as the nested groups that will be added to the inclusion/exclusion groups created within the pipeline.</p>
<p>This creates a complete solution that can be deployed in an Azure Pipeline.</p>
<h3 id="managing-azure-ad-group-relationships">Managing Azure AD group relationships</h3>
<ul>
<li><a href="#get-azure-ad-group-relationships">Get Azure AD group relationships</a></li>
<li><a href="#create-azure-ad-group-relationships">Create Azure AD group relationships</a></li>
<li><a href="#remove-azure-ad-group-relationships">Remove Azure AD group relationships</a></li>
</ul>
<h2 id="get-azure-ad-group-relationships">Get Azure AD group relationships</h2>
<p>The first function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Groups/Relationship/Get-WTAzureADGroupRelationship.ps1">Get-WTAzureADGroupRelationship</a>, which you can access from my GitHub.</p>
<p>This gets the Azure AD group owners, memberOf or members, for the specific group IDs specified.</p>
<p>Examples below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone repo that contains the Graph API and ToolKit functions</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nt">--branch</span><span class="w"> </span><span class="nx">main</span><span class="w"> </span><span class="nt">--single-branch</span><span class="w"> </span><span class="nx">https://github.com/wesley-trust/GraphAPI.git</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nt">--branch</span><span class="w"> </span><span class="nx">main</span><span class="w"> </span><span class="nt">--single-branch</span><span class="w"> </span><span class="nx">https://github.com/wesley-trust/ToolKit.git</span><span class="w">
</span><span class="c"># Dot source function into memory</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="o">.</span><span class="n">\GraphAPI\Public\AzureAD\Groups\Relationship\Get-WTAzureADGroupRelationship.ps1</span><span class="w">
</span><span class="c"># Define Variables</span><span class="w">
</span><span class="nv">$ClientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"sdg23497-sd82-983s-sdf23-dsf234kafs24"</span><span class="w">
</span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"khsdfhbdfg723498345_sdfkjbdf~-SDFFG1"</span><span class="w">
</span><span class="nv">$TenantDomain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"wesleytrustsandbox.onmicrosoft.com"</span><span class="w">
</span><span class="nv">$GroupIDs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"gkg23497-43gf-983s-5fg36-dsf234kafs24"</span><span class="p">,</span><span class="s2">"hsw23497-hg5d-t59b-fd35k-dsf234kafs24"</span><span class="p">)</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"</span><span class="w">
</span><span class="nv">$Relationship</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"members"</span><span class="w">
</span><span class="c"># Create hashtable</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">ClientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w">
</span><span class="nx">ClientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w">
</span><span class="nx">TenantDomain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="nx">GroupIDs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GroupIDs</span><span class="w">
</span><span class="nx">Relationship</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Relationship</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Get the members for the specific group, splat the parameters (including the service principal to obtain an access token)</span><span class="w">
</span><span class="n">Get-WTAzureADGroupRelationship</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w">
</span><span class="c"># Or pipe specific group IDs to get the members, including an access token previously obtained</span><span class="w">
</span><span class="nv">$GroupIDs</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-WTAzureADGroupRelationship</span><span class="w"> </span><span class="nt">-AccessToken</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w"> </span><span class="nt">-Relationship</span><span class="w"> </span><span class="nv">$Relationship</span><span class="w">
</span><span class="c"># Or specify each parameter individually, including an access token previously obtained</span><span class="w">
</span><span class="n">Get-WTAzureADGroupRelationship</span><span class="w"> </span><span class="nt">-AccessToken</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w"> </span><span class="nt">-GroupIDs</span><span class="w"> </span><span class="nv">$GroupIDs</span><span class="w"> </span><span class="nt">-Relationship</span><span class="w"> </span><span class="nv">$Relationship</span><span class="w">
</span></code></pre></div></div>
</details>
<h3 id="what-does-this-do----omit-in-toc---">What does this do? <!-- omit in toc --></h3>
<ul>
<li>This sets specific variables, including the activity, the tags to be evaluated in the relationships, and the Graph Uri
<ul>
<li>A group relationship could consist of owners, memberOf or members which is validated</li>
</ul>
</li>
<li>An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline</li>
<li>The private function is then called, with the query altered as appropriate depending on the parameters</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Get-WTAzureADGroupRelationship</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with Azure AD group Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with Azure AD group Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude tag processing of groups"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludeTagEvaluation</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Azure AD group to get the members of, this must contain valid id(s)"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"id"</span><span class="p">,</span><span class="w"> </span><span class="s2">"GroupID"</span><span class="p">,</span><span class="w"> </span><span class="s2">"GroupIDs"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$IDs</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The group relationship to return, such as group members, owners or groups this group is a member of"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s2">"members"</span><span class="p">,</span><span class="w"> </span><span class="s2">"owners"</span><span class="p">,</span><span class="w"> </span><span class="s2">"memberOf"</span><span class="p">,</span><span class="w"> </span><span class="s2">"assignLicense"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Relationship</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Private\Invoke-WTGraphGet.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Variables</span><span class="w">
</span><span class="nv">$Activity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Getting Azure AD group </span><span class="nv">$Relationship</span><span class="s2">"</span><span class="w">
</span><span class="nv">$Uri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"groups"</span><span class="w">
</span><span class="nv">$Tags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"SVC"</span><span class="p">,</span><span class="w"> </span><span class="s2">"REF"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ENV"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="nx">Activity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Activity</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$ExcludeTagEvaluation</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"Tags"</span><span class="p">,</span><span class="w"> </span><span class="nv">$Tags</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Get Azure AD group relationship</span><span class="w">
</span><span class="nv">$QueryResponse</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Id</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$IDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Invoke-WTGraphGet</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Uri</span><span class="s2">/</span><span class="nv">$Id</span><span class="s2">/</span><span class="nv">$Relationship</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Return response if one is returned</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$QueryResponse</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$QueryResponse</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$WarningMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No group </span><span class="nv">$Relationship</span><span class="s2"> exist in Azure AD for any of the group IDs specified"</span><span class="w">
</span><span class="n">Write-Warning</span><span class="w"> </span><span class="nv">$WarningMessage</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="create-azure-ad-group-relationships">Create Azure AD group relationships</h2>
<p>The next function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Groups/Relationship/New-WTAzureADGroupRelationship.ps1">New-WTAzureADGroupRelationship</a>, which you can access from my GitHub.</p>
<p>This creates new Azure AD group relationships, which can be owners, members as well as assigned licences, this is used within the pipeline to add members to the Conditional Access inclusion/exclusion groups created in the pipeline.</p>
<p>Examples below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone repo that contains the Graph API functions</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nt">--branch</span><span class="w"> </span><span class="nx">main</span><span class="w"> </span><span class="nt">--single-branch</span><span class="w"> </span><span class="nx">https://github.com/wesley-trust/GraphAPI.git</span><span class="w">
</span><span class="c"># Dot source function into memory</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="o">.</span><span class="n">\GraphAPI\Public\AzureAD\Groups\Relationship\New-WTAzureADGroupRelationship.ps1</span><span class="w">
</span><span class="c"># Define Variables</span><span class="w">
</span><span class="nv">$ClientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"sdg23497-sd82-983s-sdf23-dsf234kafs24"</span><span class="w">
</span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"khsdfhbdfg723498345_sdfkjbdf~-SDFFG1"</span><span class="w">
</span><span class="nv">$TenantDomain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"wesleytrustsandbox.onmicrosoft.com"</span><span class="w">
</span><span class="nv">$GroupID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"gb5d3497-78jb-983s-hb5s6-gbv334kafs24"</span><span class="w">
</span><span class="nv">$RelationshipIDs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"gkg23497-43gf-983s-5fg36-dsf234kafs24"</span><span class="p">,</span><span class="s2">"hsw23497-hg5d-t59b-fd35k-dsf234kafs24"</span><span class="p">)</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"</span><span class="w">
</span><span class="nv">$Relationship</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"members"</span><span class="w">
</span><span class="c"># Create hashtable</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">ClientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w">
</span><span class="nx">ClientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w">
</span><span class="nx">TenantDomain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="nx">GroupID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GroupID</span><span class="w">
</span><span class="nx">RelationshipIDs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$RelationshipIDs</span><span class="w">
</span><span class="nx">Relationship</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Relationship</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Add new relationships to the specified group, splat the parameters (including the service principal to obtain an access token)</span><span class="w">
</span><span class="n">New-WTAzureADGroupRelationship</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w">
</span><span class="c"># Or pipe specific relationship IDs to create the association with the group, including an access token previously obtained</span><span class="w">
</span><span class="nv">$RelationshipIDs</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">New-WTAzureADGroupRelationship</span><span class="w"> </span><span class="nt">-AccessToken</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w"> </span><span class="nt">-GroupID</span><span class="w"> </span><span class="nv">$GroupID</span><span class="w"> </span><span class="nt">-Relationship</span><span class="w"> </span><span class="nv">$Relationship</span><span class="w">
</span><span class="c"># Or specify each parameter individually, including an access token previously obtained</span><span class="w">
</span><span class="n">New-WTAzureADGroupRelationship</span><span class="w"> </span><span class="nt">-AccessToken</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w"> </span><span class="nt">-GroupID</span><span class="w"> </span><span class="nv">$GroupID</span><span class="w"> </span><span class="nt">-RelationshipIDs</span><span class="w"> </span><span class="nv">$RelationshipIDs</span><span class="w"> </span><span class="nt">-Relationship</span><span class="w"> </span><span class="nv">$Relationship</span><span class="w">
</span></code></pre></div></div>
</details>
<h3 id="what-does-this-do----omit-in-toc----1">What does this do? <!-- omit in toc --></h3>
<ul>
<li>This sets specific variables, including the activity and the Graph Uri
<ul>
<li>A group relationship could consist of owners, members as well as assigned licences, which is validated</li>
</ul>
</li>
<li>An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline</li>
<li>A group ID is required to add a relationship, this forms part of the Uri, the request must in a specific format</li>
<li>To add a relationship, an object must be created in a specific format, this is done for each relationship ID</li>
<li>The private function is then called with the collection of object relationships to be added to the group</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">New-WTAzureADGroupRelationship</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with Azure AD group Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with Azure AD group Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Azure AD group to add the members or owners to, this must contain valid id(s)"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"GroupID"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The group relationship to add, such as group members or owners"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s2">"members"</span><span class="p">,</span><span class="w"> </span><span class="s2">"owners"</span><span class="p">,</span><span class="w"> </span><span class="s2">"assignLicense"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Relationship</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The relationship ids of the objects to add to the group"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s1">'RelationshipID'</span><span class="p">,</span><span class="w"> </span><span class="s1">'GroupRelationshipID'</span><span class="p">,</span><span class="w"> </span><span class="s1">'GroupRelationshipIDs'</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$RelationshipIDs</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Private\Invoke-WTGraphPost.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Variables</span><span class="w">
</span><span class="nv">$Activity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Adding Azure AD group </span><span class="nv">$Relationship</span><span class="s2">"</span><span class="w">
</span><span class="nv">$Uri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"groups"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="nx">Activity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Activity</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Relationship</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"assignLicense"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"Uri"</span><span class="p">,</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Uri</span><span class="s2">/</span><span class="nv">$Id</span><span class="s2">/</span><span class="nv">$Relationship</span><span class="s2">"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"Uri"</span><span class="p">,</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Uri</span><span class="s2">/</span><span class="nv">$Id</span><span class="s2">/</span><span class="nv">$Relationship</span><span class="s2">/</span><span class="se">`$</span><span class="s2">ref"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there are IDs, for each, create an appropriate object with the IDs</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RelationshipIDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Relationship</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"assignLicense"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Licences</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$RelationshipId</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$RelationshipIDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="s2">"disabledPlans"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">
</span><span class="s2">"skuId"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$RelationshipId</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$RelationshipObject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">addLicenses</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="nv">$Licences</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="nx">removeLicenses</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$RelationshipObject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$RelationshipId</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$RelationshipIDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="s2">"@odata.id"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://graph.microsoft.com/v1.0/directoryObjects/</span><span class="nv">$RelationshipId</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Add group relationship</span><span class="w">
</span><span class="n">Invoke-WTGraphPost</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-InputObject</span><span class="w"> </span><span class="nv">$RelationshipObject</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"There are no group </span><span class="nv">$Relationship</span><span class="s2"> to be added"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>
<h2 id="remove-azure-ad-group-relationships">Remove Azure AD group relationships</h2>
<p>The last function is <a href="https://github.com/wesley-trust/GraphAPI/blob/main/Public/AzureAD/Groups/Relationship/Remove-WTAzureADGroupRelationship.ps1">Remove-WTAzureADGroupRelationship</a>, which you can access from my GitHub.</p>
<p>This removes Azure AD group relationships, which can be owners, members as well as assigned licences.</p>
<p>Examples below:</p>
<details>
<summary><em><strong>Expand code block</strong></em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone repo that contains the Graph API functions</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nt">--branch</span><span class="w"> </span><span class="nx">main</span><span class="w"> </span><span class="nt">--single-branch</span><span class="w"> </span><span class="nx">https://github.com/wesley-trust/GraphAPI.git</span><span class="w">
</span><span class="c"># Dot source function into memory</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="o">.</span><span class="n">\GraphAPI\Public\AzureAD\Groups\Relationship\Remove-WTAzureADGroupRelationship.ps1</span><span class="w">
</span><span class="c"># Define Variables</span><span class="w">
</span><span class="nv">$ClientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"sdg23497-sd82-983s-sdf23-dsf234kafs24"</span><span class="w">
</span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"khsdfhbdfg723498345_sdfkjbdf~-SDFFG1"</span><span class="w">
</span><span class="nv">$TenantDomain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"wesleytrustsandbox.onmicrosoft.com"</span><span class="w">
</span><span class="nv">$GroupID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"gb5d3497-78jb-983s-hb5s6-gbv334kafs24"</span><span class="w">
</span><span class="nv">$RelationshipIDs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"gkg23497-43gf-983s-5fg36-dsf234kafs24"</span><span class="p">,</span><span class="s2">"hsw23497-hg5d-t59b-fd35k-dsf234kafs24"</span><span class="p">)</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"</span><span class="w">
</span><span class="nv">$Relationship</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"members"</span><span class="w">
</span><span class="c"># Create hashtable</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">ClientID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w">
</span><span class="nx">ClientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w">
</span><span class="nx">TenantDomain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="nx">GroupID</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GroupID</span><span class="w">
</span><span class="nx">RelationshipIDs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$RelationshipIDs</span><span class="w">
</span><span class="nx">Relationship</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Relationship</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Remove relationships from the specified group, splat the parameters (including the service principal to obtain an access token)</span><span class="w">
</span><span class="n">Remove-WTAzureADGroupRelationship</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w">
</span><span class="c"># Or pipe specific relationship IDs to remove the association with the group, including an access token previously obtained</span><span class="w">
</span><span class="nv">$RelationshipIDs</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Remove-WTAzureADGroupRelationship</span><span class="w"> </span><span class="nt">-AccessToken</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w"> </span><span class="nt">-GroupID</span><span class="w"> </span><span class="nv">$GroupID</span><span class="w"> </span><span class="nt">-Relationship</span><span class="w"> </span><span class="nv">$Relationship</span><span class="w">
</span><span class="c"># Or specify each parameter individually, including an access token previously obtained</span><span class="w">
</span><span class="n">Remove-WTAzureADGroupRelationship</span><span class="w"> </span><span class="nt">-AccessToken</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w"> </span><span class="nt">-GroupID</span><span class="w"> </span><span class="nv">$GroupID</span><span class="w"> </span><span class="nt">-RelationshipIDs</span><span class="w"> </span><span class="nv">$RelationshipIDs</span><span class="w"> </span><span class="nt">-Relationship</span><span class="w"> </span><span class="nv">$Relationship</span><span class="w">
</span></code></pre></div></div>
</details>
<h3 id="what-does-this-do----omit-in-toc----2">What does this do? <!-- omit in toc --></h3>
<ul>
<li>This sets specific variables, including the activity and the Graph Uri
<ul>
<li>A group relationship could consist of owners, members as well as assigned licences, which is validated</li>
</ul>
</li>
<li>An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline</li>
<li>A group ID is required to remove a relationship, the relationship IDs are also specified as part of the Uri</li>
<li>The private function is then called for each ID to be removed from the group</li>
</ul>
<p>The complete function as at this date, is below:</p>
<details>
<summary><em><strong>Expand code block</strong> (always grab the latest version from GitHub)</em></summary>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Remove-WTAzureADGroupRelationship</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client ID for the Azure AD service principal with Azure AD group Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Client secret for the Azure AD service principal with Azure AD group Graph permissions"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ClientSecret</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The initial domain (onmicrosoft.com) of the tenant"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TenantDomain</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The access token, obtained from executing Get-WTGraphAccessToken"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$AccessToken</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Specify whether to exclude features in preview, a production API version will be used instead"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">switch</span><span class="p">]</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The Azure AD group to remove the members or owners from, this must contain valid id(s)"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s2">"GroupID"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$ID</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The group relationship to remove, such as group members or owners"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s2">"members"</span><span class="p">,</span><span class="w"> </span><span class="s2">"owners"</span><span class="p">,</span><span class="w"> </span><span class="s2">"assignLicense"</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Relationship</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="w">
</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="p">,</span><span class="w">
</span><span class="n">ValueFromPipeLineByPropertyName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">,</span><span class="w">
</span><span class="n">HelpMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"The relationship ids of the objects to remove from the group"</span><span class="w">
</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">Alias</span><span class="p">(</span><span class="s1">'RelationshipID'</span><span class="p">,</span><span class="w"> </span><span class="s1">'GroupRelationshipID'</span><span class="p">,</span><span class="w"> </span><span class="s1">'GroupRelationshipIDs'</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">[]]</span><span class="nv">$RelationshipIDs</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">Begin</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Function definitions</span><span class="w">
</span><span class="nv">$Functions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="s2">"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Private\Invoke-WTGraphPost.ps1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"GraphAPI\Private\Invoke-WTGraphDelete.ps1"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="c"># Function dot source</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Function</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Functions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="o">.</span><span class="w"> </span><span class="nv">$Function</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Variables</span><span class="w">
</span><span class="nv">$Activity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Removing Azure AD group </span><span class="nv">$Relationship</span><span class="s2">"</span><span class="w">
</span><span class="nv">$Uri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"groups"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">Process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># If there is no access token, obtain one</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WTGraphAccessToken</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientID</span><span class="w"> </span><span class="nv">$ClientID</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-ClientSecret</span><span class="w"> </span><span class="nv">$ClientSecret</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-TenantDomain</span><span class="w"> </span><span class="nv">$TenantDomain</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$AccessToken</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Build Parameters</span><span class="w">
</span><span class="nv">$Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$AccessToken</span><span class="w">
</span><span class="nx">Activity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Activity</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ExcludePreviewFeatures</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Parameters</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"ExcludePreviewFeatures"</span><span class="p">,</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># If there are IDs, for each, where appropriate create an object and remove the group relationship with the appropriate function</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$RelationshipIDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Relationship</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"assignLicense"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$Licences</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$RelationshipId</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$RelationshipIDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="s2">"skuId"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$RelationshipId</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$RelationshipObject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
</span><span class="nx">addLicences</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w">
</span><span class="nx">removeLicenses</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
</span><span class="nv">$Licences</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Remove group relationship</span><span class="w">
</span><span class="n">Invoke-WTGraphPost</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-InputObject</span><span class="w"> </span><span class="nv">$RelationshipObject</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$RelationshipId</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$RelationshipIDs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Remove group relationship</span><span class="w">
</span><span class="n">Invoke-WTGraphDelete</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="err">@</span><span class="nx">Parameters</span><span class="w"> </span><span class="se">`
</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Uri</span><span class="s2">/</span><span class="nv">$Id</span><span class="s2">/</span><span class="nv">$Relationship</span><span class="s2">/</span><span class="nv">$RelationshipId</span><span class="s2">/</span><span class="se">`$</span><span class="s2">ref"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"There are no group </span><span class="nv">$Relationship</span><span class="s2"> to be removed"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="bp">$Error</span><span class="n">Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"No access token specified, obtain an access token object from Get-WTGraphAccessToken"</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$Error</span><span class="nx">Message</span><span class="w">
</span><span class="kr">throw</span><span class="w"> </span><span class="bp">$Error</span><span class="n">Message</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">End</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
</span><span class="nx">throw</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
</details>Wesley TrustFor Azure AD groups, owners or members of the group are defined as group 'relationships', this is a series of PowerShell functions to manage these...