[PIM 3.1] Build Sheet: PIM for Groups End to End

Build a role-assignable group carrying three directory roles, bring it under PIM, and make the administrators eligible for membership rather than in it. One activation grants the tier; two of the steps cannot be undone.


By the end of this sheet you have a role-assignable group carrying three directory roles, brought under Privileged Identity Management, with an administrator population that is eligible for membership rather than in it. One activation grants all three roles, the audit trail proves it, and deactivation takes them all away again. Two of the steps cannot be undone, so they are flagged before you reach them.

Before you start

You need Microsoft Entra ID P2 or Microsoft Entra ID Governance licences covering every user who will be eligible for membership or ownership of the group, plus any approvers. You need at least Privileged Role Administrator, because that is the floor for creating a role-assignable group and for assigning directory roles to it. You should already have the work from the previous build sheet in place, meaning the roles you care about have deliberate PIM policies and your emergency access accounts are permanent, active and outside all of this. And you need to have decided your tier model before you touch the portal, because the group you are about to create carries an immutable property and a one-way onboarding step, so the cost of changing your mind afterwards is a new group and a repointing exercise rather than an edit.

The worked example throughout is a single bundle: Tier 1 Identity Operations, carrying User Administrator, Groups Administrator and Helpdesk Administrator. That trio is a real tier in most estates, it is entirely directory-facing, and being directory-facing is precisely why it is safe to bundle. The second worked example at the end shows a bundle you should not build, and what to do instead.


Step 1: Create the group, and get the name right the first time

In the Microsoft Entra admin center, go to Entra ID, then Groups, then New group. Set Group type to Security and Membership type to Assigned. Dynamic membership is not an option here and the portal will not let you combine it with what comes next.

Then the setting the whole sheet depends on: set Microsoft Entra roles can be assigned to the group to Yes. This is the property that lets the group hold a directory role, it is set at creation only, and it can never be changed afterwards. A group created without it cannot be converted by anyone, through any interface, ever. If you take one thing from this step, take that.

Name it before you save it, using a convention that will still make sense at the fiftieth group. Mine is SEC-PIM-Role-<Tier>-<Function>, giving SEC-PIM-Role-Tier1-IdentityOps for this example, with SEC-PIM-Role-Tier0-Directory and SEC-PIM-Role-Tier2-Servicedesk as its siblings. The SEC- prefix sorts every security-owned object together, PIM-Role- says immediately that this group carries directory roles and is under PIM management, the tier says how much authority is inside without opening it, and the function says what it is for. Put the roles the group carries into the group description, in words, because there is no view anywhere that shows you a group and its roles side by side at a glance, and in two years that description will be the only documentation that survived.

Assign owners with real care. Owners of a role-assignable group can manage its membership, which means an owner can grant everything the group carries without ever activating anything. Ownership is the quiet path around the restricted-management protection, so the owner list should be shorter than the eligible-member list, not longer. In a small team, name no owners at all and let Global Administrators and Privileged Role Administrators manage it, which is the tightest configuration available.


Step 2: Give the group its roles, active and permanent

This is the step that feels wrong the first time and is correct. Go to Entra ID, then Roles & admins, open User Administrator, and add the group as an assignment with type Active and no expiry. Repeat for Groups Administrator and Helpdesk Administrator.

You have just created a group that permanently holds three administrative roles, which sounds like exactly what this whole wave is arguing against. It is not, because the group has no members. The bundle is standing; nobody is inside it. This is the second of the two documented layerings, and it is the one that gives you a single activation for the whole tier. The just-in-time boundary has moved from the role to the membership, and the next step is where you put the gate.

Verify before continuing. Open the group and check its assigned roles list shows all three. A role that silently failed to attach here produces the worst kind of bug later: an activation that appears to succeed and grants two thirds of what the user needs, which they will report as an intermittent problem with the third role.


Step 3: Bring the group under PIM, knowing the door only opens one way

Go to ID Governance, then Privileged Identity Management, then Groups. Select Discover groups, find SEC-PIM-Role-Tier1-IdentityOps, then select Manage groups and confirm.

Read the warning it gives you and believe it. Once a group is managed by PIM, it cannot be taken out of management. There is no un-onboard action, no supported reversal, and no administrator who can undo it. That is not a reason to avoid the step, it is a reason to have finished Step 1 properly. If you are experimenting, experiment on a group you are willing to keep or delete outright.

The same irreversibility applies through Microsoft Graph, and there it is much easier to trip. There is no explicit onboarding call. A group is onboarded implicitly the first time you create a schedule request against it or update its policy, which means a script written to configure a group can onboard it as a side effect. The group’s policy identifiers also change once it is onboarded, so anything that cached them stops working with an error that reads like a permissions failure and is not.


Step 4: Configure the member policy, and do not skip the owner policy

A PIM-managed group carries exactly two policies, Member and Owner, and they are configured independently. Open the group in PIM, go to Settings, and edit Member first.

SettingValueWhy
Activation maximum duration4 hoursThis is a shift-shaped tier rather than a task-shaped one. The three roles in the bundle cover the routine identity work of a working half-day, and a one-hour window on this population produces re-activations rather than discipline. It is deliberately longer than the Global Administrator window from the previous sheet, because it is deliberately less authority.
On activation, requireMicrosoft Entra Conditional Access authentication contextSame reasoning as everywhere else in this wave: the multifactor option reuses the claim the session already carries and therefore challenges nobody in the common case. Use multifactor authentication as a temporary value if the context is not built yet. When you do wire the context, never scope its Conditional Access policy to this group: during activation the user is not yet a member, so the policy would not apply.
Require justification on activationYesThe audit record for a bundle activation says the user joined a group. The justification is what tells a reviewer which of the three roles they actually came for.
Require approval to activateYesNot optional here. Microsoft recommends an approval process for eligible member assignments on any group used to elevate into Entra roles, because activation without approval exposes you to a less-privileged administrator, and the example it names is a Helpdesk Administrator, who can reset an eligible user’s password. Role-assignable groups do not close this. Their credential protection covers active members only, and a user who is merely eligible has not activated, so is not one. The step the documentation leaves unstated is the one that matters: reset that password, sign in as them, activate, and you hold the whole bundle without ever being assigned any of it. Approval puts a second human in the middle of that chain.
ApproversSEC-PIM-Approvers-Tier1, at least two peopleA group rather than named individuals, so the policy does not quietly stop working when someone leaves. Include at least one approver who is not themselves eligible for this bundle, because nobody can approve their own request and a closed loop of mutual approvers is not a second opinion.
Allow permanent eligible assignmentNo, expire eligible assignment after 6 monthsEligibility for a three-role bundle is a real standing liability even though it grants nothing today. Forced renewal is what stops a tier group accumulating former members of the tier.

Now edit Owner, and this is the policy people forget. An owner can add members, and a member holds three administrative roles, so ownership of this group is a higher privilege than membership of it. Configure the owner policy at least as strictly as the member policy: a shorter activation window, approval required, and no permanent eligible assignments. If you assigned no owners in Step 1 the policy governs nothing today, and setting it correctly anyway costs a minute and closes the door before somebody props it open.

One trap lives specifically in eligible ownership and it is worth knowing before it strands someone. Entra never allows the removal of a group’s last active owner. So if a group has one active owner and one eligible owner, the eligible owner activates, and the active owner is then removed from the group or leaves the tenant, the eligible owner’s ownership cannot be deactivated. PIM keeps retrying for thirty days, and if another active owner is added in that time the deactivation finally succeeds. If not, PIM gives up and that user simply remains an active owner. A just-in-time assignment that quietly becomes permanent is the exact failure this whole wave exists to prevent, so if you use eligible ownership at all, keep an active owner who is not going anywhere.


Step 5: Make the administrators eligible

Still in the group’s PIM view, go to Assignments, then Add assignments, select Member as the access type, choose the users, and set the assignment type to Eligible with an expiry six months out to match the policy.

Then remove what this replaces, which is the part people leave half-finished. Every user you just made eligible for the bundle almost certainly still holds one or more of User Administrator, Groups Administrator and Helpdesk Administrator directly, either standing or as an individual eligible assignment from your earlier work. Those direct assignments are now redundant and actively harmful, because they mean the user can get two thirds of the bundle without triggering the approval you just built. Go back to Roles & admins for each of the three roles and remove the direct assignments for anyone now covered by the group. A bundle you added without removing what it replaced is not a control, it is an extra route.


Step 6: The end-to-end test

Use a real eligible user who is not an approver. Have them open Privileged Identity Management, then My roles, then Groups, and activate membership of SEC-PIM-Role-Tier1-IdentityOps. They supply a duration up to four hours and a justification, the request goes pending, and an approver clears it.

Now validate the thing that actually matters, which is not that the activation succeeded but that all three roles landed. Do not test that by behaviour. User Administrator on its own creates users, manages groups that are not role-assignable, and resets passwords for non-administrators, so a check built from those three actions passes even when the other two assignments never resolved, which is the exact failure the test exists to catch. Assert the resolved assignments instead: the group must carry all three role definition IDs, and the user’s membership instance must be active rather than merely eligible. The assertion is at the end of the runnable block below. If you want the sheet to stay operational, keep one behavioural action as a smoke test and pick one User Administrator cannot satisfy, such as permanently deleting a soft-deleted group from the deleted objects container, which needs Groups Administrator. Expect the directory itself to be current within seconds and expect a lag before every surface agrees; signing out and back in is the documented remedy and it is not a workaround, it is how token-based role state behaves.

Find the evidence in the Entra audit log. Group membership activations land under the GroupManagement category rather than the RoleManagement one you learned in the previous sheet, and that difference is the single most common reason people conclude their bundle activations are not being audited. Filter Service to PIM and you will see the request, the approval, and the completion, with the approver as the actor on the approval event and the requester on the Targets tab. Save that filter. A monitoring rule written only against RoleManagement will be blind to every bundle activation in the tenant.

Finish by deactivating and verifying removal. The user deactivates membership, or lets the four hours run out, and the directory drops them from the group within seconds. Re-run the assertion and require the membership instance to be gone, rather than retesting the one action you happened to pick. Then do one deliberately negative check: as a Global Administrator, add a test user to the group directly through Entra ID and Groups, bypassing PIM entirely. It will work. That is not a fault you have introduced, it is the documented behaviour that administrators and group owners can manage these groups through the ordinary Groups experience and override what PIM says. Seeing it work once, deliberately, is what stops you assuming a protection you do not have. Remove the test user, and turn that observation into the alert it deserves: a membership change on a tier group, made outside PIM, is one of the highest-value detections in the estate.


The second worked example: the bundle not to build

A natural next thought is a Tier 1 Collaboration bundle carrying Exchange Administrator, SharePoint Administrator and Teams Administrator, built exactly like the one above. Do not build it that way. Microsoft advises against reaching roles with permissions in SharePoint, Exchange or the Microsoft Purview portal through eligible membership of a group that holds the role, warning that it can take significant time for all the role’s permissions to become usable, and the SharePoint documentation puts the worst case at up to twenty-four hours. An access model that may take a day to take effect is not a just-in-time model.

There is a second, entirely separate reason to keep Exchange out of a role-assignable group, and it is a documented known issue rather than a timing problem: when an administrator role is assigned to a role-assignable group rather than to individual users, members of that group cannot access Rules, Organization, or Public Folders in the new Exchange admin center. Microsoft’s own workaround is to assign the role directly to the user instead of through the group. So the group route can cost you both a propagation delay and a chunk of the console, for a role whose administrators will notice both.

Two supported shapes work instead. Use the other layering: make the users permanent active members of the group and give the group an eligible role assignment, so what gets activated is the role rather than the membership. Or skip groups for these roles and use direct role PIM as the previous build sheet describes. I default to the second for workload administration, because it keeps the activation and the role in the same object and the diagnostics stay simple. The result is an estate that bundles its directory tiers and does not bundle its workload administrators, which looks inconsistent on a diagram and is the correct answer for a documented reason.


The same work as one runnable block

Groups are the part of PIM most worth scripting, because a tier model is a set of near-identical objects and hand-building the fourth one is where the inconsistencies enter. Note the permission split before you start, because it is the thing that stops working scripts: Group.ReadWrite.All creates the group, but managing the membership of a role-assignable group requires RoleManagement.ReadWrite.Directory, and Microsoft states plainly that Group.ReadWrite.All will not work for that. The PIM-for-Groups calls are a third surface again with their own scopes.

# Written against Microsoft.Graph PowerShell 2.38.1 and Microsoft Graph v1.0.
# Pin the module if you are scripting this for reuse. Cmdlet parameter sets move
# between releases, and a sheet that does not say what it was written against ages
# badly and fails in ways that look like your mistake.
$ErrorActionPreference = 'Stop'

# Four surfaces, four scopes. Group.ReadWrite.All CREATES the group;
# RoleManagement.ReadWrite.Directory is what managing a role-assignable
# group's membership requires (Group.ReadWrite.All will NOT do it);
# the PrivilegedEligibilitySchedule / PrivilegedAssignmentSchedule scopes
# are the PIM-for-Groups surface. Do not conflate them.
Connect-MgGraph -Scopes "Group.ReadWrite.All",
                        "RoleManagement.ReadWrite.Directory",
                        "PrivilegedEligibilitySchedule.ReadWrite.AzureADGroup",
                        "PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup",
                        "RoleManagementPolicy.ReadWrite.AzureADGroup",
                        "Directory.Read.All"

# CONTEXT ASSERTION. Fill these in before you run anything below. A build sheet
# executed against the wrong tenant is the worst outcome this series can produce,
# and the check costs two lines.
$ExpectedTenantId = '<your tenant id>'
$ExpectedAccount  = '<the account you intend to be>'
$mgContext = Get-MgContext
if ($mgContext.TenantId -ne $ExpectedTenantId) { throw "Wrong tenant: $($mgContext.TenantId)" }
if ($mgContext.Account  -ne $ExpectedAccount)  { throw "Wrong identity: $($mgContext.Account)" }

# PIM assignment and activation requests are asynchronous. Poll to a terminal
# state before asserting success, and never remove access on the strength of a
# request that is still pending. The status values below are the documented set
# on the Graph request resource, which these requests inherit: Canceled, Denied,
# Failed, Granted, PendingAdminDecision, PendingApproval, PendingProvisioning,
# PendingScheduleCreation, Provisioned, Revoked, ScheduleCreated. Two of those
# are deliberately not in $done. ScheduleCreated is transitional on these
# surfaces: no Entra role or PIM for Groups example rests a request there, and
# every call below settles at Provisioned, or Revoked for a removal. Granted is
# where a future-dated activation parks until its start time, and it stays
# cancelable; nothing below is date-shifted, so Granted is transitional here
# too. Date-shift a scheduleInfo block and neither is safe to leave out.
function Wait-Pim {
  param([scriptblock]$Refresh, [int]$TimeoutSec = 120)
  $done     = @('Provisioned','Revoked')
  $failed   = @('Failed','Denied','Canceled')
  $waiting  = @('PendingApproval','PendingAdminDecision')
  $deadline = (Get-Date).AddSeconds($TimeoutSec)
  do {
    Start-Sleep -Seconds 5
    $r = & $Refresh
    if ($r.Status -in $waiting) {
      # Expected whenever the policy requires approval. Not a failure.
      Write-Host "Request is $($r.Status): waiting on an approver, which is the gate working. Re-run this poll after the decision."
      return $r.Status
    }
  } until ($r.Status -in ($done + $failed) -or (Get-Date) -gt $deadline)
  if ($r.Status -in $failed)  { throw "Request ended '$($r.Status)'." }
  if ($r.Status -notin $done) { throw "Still '$($r.Status)' after $TimeoutSec seconds." }
  $r.Status
}

# The three role definition IDs, declared once and used everywhere below. Never
# key a state change on a display name: names are localised and Microsoft renames
# them, which is how Password Administrator became Helpdesk Administrator.
$RoleIds = @{
  'User Administrator'     = 'fe930be7-5e62-47db-91af-98c3a49a38b1'
  'Groups Administrator'   = 'fdd7a751-b60b-444a-984c-02652fe8fa1c'
  'Helpdesk Administrator' = '729827e3-9c14-49f7-bb1b-9608f156bbb8'
}

# 1. Create the ROLE-ASSIGNABLE group. IsAssignableToRole is creation-time only
#    and immutable: get it wrong and the only fix is a new group.
#    Rollback: Remove-MgGroup -GroupId $group.Id, before step 3 only.
$group = New-MgGroup -DisplayName "SEC-PIM-Role-Tier1-IdentityOps" `
  -MailNickname "sec-pim-role-tier1-identityops" `
  -Description  "Tier 1 identity operations bundle: User Administrator, Groups Administrator, Helpdesk Administrator. Membership is JIT via PIM." `
  -SecurityEnabled -MailEnabled:$false `
  -IsAssignableToRole:$true

# 2. Give the group its three roles, ACTIVE and permanent, by ID from $RoleIds
#    above. The group is the standing bundle; nobody is inside it yet.
#    Rollback: same call per role with action = "adminRemove".
foreach ($roleId in $RoleIds.Values) {
  $req = New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter @{
    action           = "adminAssign"
    principalId      = $group.Id
    roleDefinitionId = $roleId
    directoryScopeId = "/"
    justification    = "Tier 1 identity operations bundle"
    scheduleInfo     = @{
      startDateTime = (Get-Date).ToUniversalTime()
      expiration    = @{ type = "noExpiration" }
    }
  }
  Wait-Pim { Get-MgRoleManagementDirectoryRoleAssignmentScheduleRequest `
               -UnifiedRoleAssignmentScheduleRequestId $req.Id }
}

# 3. Make an administrator ELIGIBLE for membership, expiring in 6 months.
#    NOTE: this call implicitly ONBOARDS the group to PIM. That is irreversible.
#    Rollback: the eligibility comes back off with action = "adminRemove".
#    The onboarding does not roll back at all, so be sure of $group.Id first.
$user = Get-MgUser -UserId "[email protected]"
$req = New-MgIdentityGovernancePrivilegedAccessGroupEligibilityScheduleRequest -BodyParameter @{
  accessId      = "member"
  principalId   = $user.Id
  groupId       = $group.Id
  action        = "adminAssign"
  justification = "Tier 1 identity operations, eligible membership"
  scheduleInfo  = @{
    startDateTime = (Get-Date).ToUniversalTime()
    expiration    = @{ type = "afterDuration"; duration = "P180D" }
  }
}
# 300 seconds, not the 120 default: this call also performs the PIM onboarding
# of the group, which is the slowest write in this sheet and the one you cannot undo.
Wait-Pim -TimeoutSec 300 -Refresh { Get-MgIdentityGovernancePrivilegedAccessGroupEligibilityScheduleRequest `
             -PrivilegedAccessGroupEligibilityScheduleRequestId $req.Id }

# 4. What the END USER runs to activate the bundle for four hours.
#    Rollback: same call with action = "selfDeactivate".
$req = New-MgIdentityGovernancePrivilegedAccessGroupAssignmentScheduleRequest -BodyParameter @{
  accessId      = "member"
  principalId   = $user.Id
  groupId       = $group.Id
  action        = "selfActivate"
  justification = "Ticket 41288: bulk licence reassignment"
  scheduleInfo  = @{
    startDateTime = (Get-Date).ToUniversalTime()
    expiration    = @{ type = "afterDuration"; duration = "PT4H" }
  }
}
# In a tenant built as Step 4 of this sheet instructs, approval is required, so
# this reports PendingApproval and stops. That is the correct result, not an error.
# Re-run this poll after the approver decides, then continue to step 5.
Wait-Pim { Get-MgIdentityGovernancePrivilegedAccessGroupAssignmentScheduleRequest `
             -PrivilegedAccessGroupAssignmentScheduleRequestId $req.Id }

# 5. VALIDATE. The pass condition is the resolved assignment list, not behaviour.

# 5a. The group must carry all three roles, asserted against $RoleIds.
$onGroup = (Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance `
              -Filter "principalId eq '$($group.Id)'").RoleDefinitionId
$missing = $RoleIds.GetEnumerator() | Where-Object { $_.Value -notin $onGroup }
if ($missing) {
  throw "Bundle incomplete. Not assigned to the group: $($missing.Name -join ', ')"
}

# 5b. The activation must have resolved. An eligible-but-not-active user has no
#     assignment schedule instance, and that is the failure a behavioural test hides.
$live = Get-MgIdentityGovernancePrivilegedAccessGroupAssignmentScheduleInstance `
          -Filter "groupId eq '$($group.Id)'" |
        Where-Object { $_.PrincipalId -eq $user.Id -and $_.AccessId -eq 'member' }
if (-not $live) { throw "Activation did not resolve for $($user.Id)." }

# 6. After deactivation or expiry, 5b must invert: re-run it and require an empty
#    result before you call the teardown clean. 5a is unchanged by deactivation,
#    because the roles belong to the group, not to the user.
#    (A single transitive view of the user exists, but only on the beta endpoint,
#     so it is deliberately not used here.)

Three things to expect from that block. Step 3 is the irreversible one, and it does not announce itself: there is no onboarding call to review before it happens, so a script that touches the wrong group has permanently changed it. Step 4 will fail against a role or group bound to an authentication context, because an interactive Graph token does not carry the acrs claim the activation demands; that failure is real, it is not a bug in your script, and the next article in this wave deals with it directly. And accessId is the parameter people miss: it takes member or owner, and omitting it or getting it wrong produces an eligibility for the wrong kind of access on the right group, which is easy to miss in a review because the group name looks correct.


Where this goes next

You have a tier that activates as one thing, expires as one thing, and is reviewed as one thing. What you do not yet have is a real price on the activation. Every gate in this sheet can still be satisfied by a session that was authenticated hours ago, which means an attacker holding a stolen session pays the same nothing your legitimate administrator pays. That is the gap the next two articles close, and it is the most consequential change available to a PIM deployment this year.


Governance
‹ Previous: [PIM 3] PIM for Groups: Privilege in Bundles
Next: [PIM 4] Activation Under Conditional Access: The Passwordless Elevation