Licensing¶
Seven examples, in the order the questions are asked: what the tenant owns, what one person
holds, whether a call may rely on it, who has what, how to change it, and what the change did
to the plans. Two more carry that state as a list: export_licences.py writes it, and
import_licences.py reads it back into assignLicense calls, sent as one batch.
Prerequisites¶
| Permission | Description | Reference |
|---|---|---|
Organization.Read.All |
Read subscribed SKUs and their service plans | Organization permissions |
User.Read.All |
Read users and their license assignments | User permissions |
User.ReadWrite.All |
Assign and remove licenses | User permissions |
Group.ReadWrite.All |
Assign a license to a group | Group permissions |
| Example | Writes | Question |
|---|---|---|
inventory.py |
— | What does the tenant own — seats, state, how many are left? --plans lists the plans inside each SKU, --csv writes one row per SKU |
matrix.py |
— | Who holds which SKU, and which service plans inside it are enabled or disabledPlans. --out licences.csv, --sku SPE_E5 |
whose.py |
— | Who holds this SKU (--sku), or what does this person hold (--user) with each plan's provisioning status? |
check.py |
— | Is the seat — or the capability inside it — actually held? --plan RECORDS_MANAGEMENT |
assign.py |
✓ | Add or --remove one licence, refusing the calls Graph would refuse: no usageLocation, no seat left, already assigned. --dry-run stops before the write |
group_license.py |
✓ | Licence a group once and let Graph keep its members licensed |
plans.py |
✓ | List the service plans of a SKU, or --plan-off EXCHANGE_S_ENTERPRISE for one user |
export_licences.py |
— | Every user's SKUs and disabled plans as CSV — the input the next script reads |
import_licences.py |
✓ | Give each user the licences a row asks for — one assignLicense per row, in one $batch. --dry-run prints the rows |
Every script is runnable and documents itself:
Two questions, not one¶
subscribedSkus says what the tenant buys; licenseDetails says what one seat grants.
A SKU part number is packaging, a service plan is capability — and a seat may carry a plan no
retail SKU name suggests:
from office365.graph_client import GraphClient
client = GraphClient(tenant="contoso.onmicrosoft.com").with_client_secret("client_id", "client_secret")
held = client.licences.get_all().execute_query().value
print(sorted(held.names)) # SKU part numbers and provisioned plan names
print(held.holds("RECORDS_MANAGEMENT")) # True for DEVELOPERPACK_E5
print([sku.sku_part_number for sku in held.carrying("RECORDS_MANAGEMENT")])
print(held.to_rows()) # one row per SKU: seats bought, handed out, left
mine = client.licences.get_user(client.me).execute_query().value # a user's licenseDetails
client.licences is a manager and sends nothing; get_all() / get_user(user) return a
ClientResult that execute_query() submits. Only a plan whose provisioningStatus is
Success counts, and nothing is cached: a grant changes the answer, and the library is not the
level that knows when.
A list, both ways¶
export_licences.py > licences.csv writes the state; import_licences.py licences.csv gives
each user what a row asks for, one assignLicense per row in one $batch. Nothing is read
first — the row addresses the user by UPN or id — and Graph's own preconditions are what refuses
a row, each refusal returning as its own sub-request's error. A licence a group grants cannot be
removed: the group owns it. For a population, group-based licensing is the way round; a
per-user fan-out is for a one-off list.