May 10, 2025

Manage Azure DevOps Licences with Entra ID Access Packages – Part 1

Paying for Azure DevOps seats your teams never use? Many organizations discover they’re funding hundreds of paid licenses for people who haven’t signed in for months. If you already own Microsoft Entra ID P2 or the broader Entra Suite, a small dose of automation—plus well-designed Access Packages—can ensure that only genuinely active contributors receive (and keep) those paid licenses.

Curious how to put that license bloat on a diet? Keep reading—I’ll walk through the exact steps in the sections that follow.

Azure DevOps license tiers at a glance

Azure DevOps Services keeps its pricing simple, but only three access levels require you to reach for the credit card:

Access levelWhat it unlocksPrice*
StakeholderBasic work-item tracking and dashboards (read-only repos, no pipelines)Free for unlimited users
BasicFull Azure Boards, Repos, Artifacts and one parallel pipelineFirst 5 users free, then US $6 per user/month (pricing page) Microsoft Azure
Basic + Test PlansAll Basic features plus manual & exploratory testing toolsUS $52 per user/month (server pricing table) Microsoft Azure

* Prices shown are list prices in USD for Azure DevOps Services (cloud). Visual Studio subscribers get the equivalent licence included at no extra cost (Professional ≈ Basic, Enterprise ≈ Basic + Test Plans).

Take-away: once you cross the five-user threshold every inactive Basic licence is burning US $72 a year—and Basic + Test Plans burns US $624. That’s the spend we’ll target with automation in the next section.

How large organisations usually assign Azure DevOps licences

At scale, admins don’t hand-out licences one user at a time. Instead they combine Microsoft Entra ID security groups with Azure DevOps “group rules.” The workflow looks like this:

  1. Create Entra ID groups that mirror your access tiers—e.g.,
    azdo-basic, azdo-basic-test-plans, azdo-stakeholder.
    These groups sync automatically to Azure DevOps. Microsoft Learn
  2. Add a group rule in Azure DevOps that says
    “If a user is in azdo-basic, give them the Basic access level.”
    Group rules can also drop the user into project-level security groups so they inherit repos, boards, and pipeline permissions in one hit. Microsoft Learn
  3. Assign project permissions by nesting the same Entra groups (or their DevOps twins) into project-specific groups such as Contributors or Readers. This keeps licence management and permission scopes aligned. Microsoft Learn
  4. Rinse and repeat when new teams spin up: owners just add colleagues to the Entra group and the rule handles both the licence and the rights—no extra ticket.

Upsides

  • Central source of truth — HR or IT already manages Entra groups.
  • Instant onboarding — new hires get the right licence the moment their account lands in the group.
  • Fewer mistakes — no one forgets to grant repo or board access.

Downside (and the focus of this article)

When users change teams or leave the company, they often stay in those Entra groups indefinitely. Over time that can leave hundreds—or thousands—of paid Basic or Basic + Test Plans licences assigned to people who never log in.

In this post we’ll automate a fix so only active, genuine contributors keep a paid seat.

What are Access Packages?

Access Packages are part of Microsoft Entra’s entitlement-management feature set. Think of them as curated bundles of permissions—Entra ID groups, Teams, apps, even SharePoint sites—that users can request, time-box, and periodically renew. Instead of handing out one-off role assignments, you publish a package, set approval and expiration rules, and let automation handle the rest.
Official docs: Access-package overview | Entitlement Management in Entra ID

Now, let’s start developing our solution.

Step 1 – Wrap each paid licence in its own Access Package

We’ll start by converting static licence groups into self-service, policy-driven Access Packages and wiring those groups to Azure DevOps group rules.

1. Create the Entra ID groups

Add two new security groups in Entra ID (change the names according to your organization’s naming convention):

Group nameIntended licence
azdo-basicBasic access level
azdo-basic-testplansBasic + Test Plans access level

2. Build two Access Packages

In the Entra admin centre navigate to Identity Governance ➜ Entitlement management ➜ Access packages and create:

  1. “Azure DevOps – Basic”
    Resources: add the azdo-basic group as the only resource/role.
  2. “Azure DevOps – Basic + Test Plans”
    Resources: add the azdo-basic-testplans group.

Give each package a “Self-service” policy (users can request it), choose your approvers, and set an expiration/renewal window (for example 30 days). You can also skip approvals and make the process completely self-service.
🗒️ Reference docs: how to create an access package Microsoft Learn and entitlement-management overview Microsoft Learn.

3. Add Azure DevOps group rules

Switch to Azure DevOps Organisation settings ➜ Permissions ➜ Group rules and create two rules:

If member of Entra groupAssign this Azure DevOps access level
azdo-basicBasic
azdo-basic-testplansBasic + Test Plans

The rule engine watches your directory and upgrades/downgrades users automatically. How-to: assign access levels by group membership Microsoft Learn.

4. End-to-end flow

  1. A user opens the “Azure DevOps – Basic” Access Package, hits Request, and (optionally) adds a justification.
  2. When the request is approved (if approval is required), entitlement-management adds the user to azdo-basic group.
  3. Azure DevOps detects the group membership and, via the rule, grants the Basic licence. All in minutes, zero manual ticket.

Important: We’ve merely shifted licence assignment from static groups to Access Packages. Inactive users can still keep a seat forever if nobody revokes or lets the package expire. The real clean-up mechanism comes next—stay tuned.

High-level solution: auto-renew for the truly active, let the rest lapse

The timeline we’re working with

  1. Day 0 — User’s Access Package assignment starts (valid for 30 days).
  2. Day 15 — Entitlement-management schedules the “assignment expiring in 14 days” email (Microsoft sends these emails on day 15 for some reason).
  3. Day 29 — A second reminder goes out 1 day before expiry.
  4. Day 30 — Assignment ends; the user drops out of the Entra group, and Azure DevOps revokes the paid licence.

Our goal is to intervene before step 2, but only for people who still use Azure DevOps.

What the automation must do (multiple times during the day)

StepAction
1️⃣Get paid user entitlements from Azure DevOps (ignore stakeholder licenses and users with Visual Studio subscriptions) using: _apis/userentitlements
2️⃣For each paid entitlement, check activity by reading lastAccessedDate
3️⃣Query Access-package assignments that are expiring in 16 days or less (Graph endpoint identityGovernance/entitlementManagement/assignments).
4️⃣Decision
User active within the last 30 days? → Extend the assignment (New-MgEntitlementManagementAssignmentRequest).
Inactive? → Do nothing; the package—and the licence—will expire naturally.
5️⃣Remove directly assigned licenses
We should not allow paid licenses to be directly assigned. If a user has such a license, it should be downgraded to Stakeholder.
6️⃣Run multiple times during the day (Azure Function on a timer) so every expiring assignment is evaluated at least once before Microsoft’s 15-day email is sent.
7️⃣Re-evaulate group rules multiple times (as often as possible) during the day
This is because group rules are re-evaluated every 24 hours by default. We don’t want new users to wait up to 24 hours for their licenses.
Use undocumented internal API to trigger re-evaluation: _apis/MEMInternal/GroupEntitlementUserApplication?ruleOption=0

Net effect

  • Active users never see the nag email; their package auto-renews in the background and their licence persists.
  • Dormant accounts silently age out; finance recovers the US $6 – 52 per seat after 30 days of inactivity.
  • Self-service stays intact; if a lapsed user becomes active again, they can either log in (to reset last-access) or re-request the package via the My Access portal.

We’re now ready to design the Azure Function that implements those six steps—code in the next post. Stay tuned.


Useful docs for this stage