Microsoft Graph API で「ライセンスが付与されている非アクティブなユーザー」を抽出したく色々調べた時のメモです。Azure ADは明示的に割り当て解除しない限り、ライセンスが割り当てられたままとなってしまいます。この仕様によって不要ライセンスの外し忘れが稀によく起こります。そこで、Graph API経由でアクティブでないユーザーを抽出して、ライセンスの棚卸等に役立てることにしました。
なお、グループを使ってライセンス割り当てをしている場合、アクセスレビュー で簡単に不要ユーザーを抽出し、ライセンス剥奪できます。アクセスレビューを使えばAPIを叩く必要もないので、お手軽に済ませたい場合はアクセスレビューを使うのが良いと思います。アクセスレビューを使うケースは別の記事にまとめてあります。
Microsoft Graph APIのエンドポイントについて
Microsoft Graph APIのエンドポイントには安定板である v1.0
とβ版である beta
が存在します。現在サインイン日時は beta でしか取得できないので、今回は beta を利用します。beta の利用は、突然の仕様変更が発生したとしても困らない場合にのみ留めましょう。バージョンの詳細については以下に詳しく書かれています。
ユーザーのサインイン日時を取得するAPI
ユーザーのサインイン日時は signInActivity です。このリソースはデフォルトでレスポンスに含まれておらず、$select=signInActivity
を指定してリクエストを投げることでのみ取得できます。
リクエスト:
https://graph.microsoft.com/beta/users?$select=userPrincipalName,signInActivity
レスポンス:
{
'@odata.context': 'https://graph.microsoft.com/beta/$metadata#users(userPrincipalName,signInActivity)',
'value':[{
'userPrincipalName': 'xxx',
'id': 'xxx',
'signInActivity': {
'lastSignInDateTime': '2022-06-26T14:27:46Z',
'lastSignInRequestId': 'xxx',
'lastNonInteractiveSignInDateTime': '2022-06-26T14:27:53Z',
'lastNonInteractiveSignInRequestId': 'xxx'
}
},
... ユーザー分繰り返し ...,
]
}
以下のいずれかに該当する場合 signInAcvtivity
はレスポンスに含まれない仕様のようです。
- サインイン日時が2020年4月以前
- そもそも一回もサインインしたことがない
テストユーザーを作成して一度もサインインしていない状態でデータを取得してみると、以下のように signInActivity
自体が存在しないことを確認できます。
{
'@odata.context': 'https://graph.microsoft.com/beta/$metadata#users(userPrincipalName,signInActivity)',
'value':[{
'userPrincipalName': 'xxx',
'id': 'xxx'
},
... ユーザー分繰り返し ...,
]
}
このパラメータは $filter
クエリをサポートしているので、「特定の日時より前に最後にサインインした」という条件で絞りこみができます。ただし、残念ながら他のフィルタと併用ができないため今回は利用していません。
このリソースを使う際の注意点は user リソースの種類(beta) | Microsoft Docs の signInActivity に詳しく記載されています。現在はbeta版で将来的に諸々変わる可能性があるので、利用前に改めてチェックすることをオススメします。
ユーザーに付与されているライセンスを取得するAPI
ユーザーに付与されているライセンスは assignedLicenses に含まれています。userリソースにデフォルトで含まれていますが、以下の例はわかりやすくするために select で指定しています。
リクエスト:
https://graph.microsoft.com/beta/users?$select=userPrincipalName,assignedLicenses
レスポンス:
{
'@odata.context': 'https://graph.microsoft.com/beta/$metadata#users(userPrincipalName,assignedLicenses),
'value': [{
'userPrincipalName': 'xxx',
'assignedLicenses': [{
'disabledPlans': [],
'skuId': 'b05e124f-c7cc-45a0-a6aa-8cf78c946968'}
]}
}]
}
割り当てられているライセンスは skuId
という識別子で一意に特定されます。「Enterprise Mobility + Security E5」のようにライセンス名が直接埋め込まれるわけではありません。skuId は以下にGUIDとして一覧掲載されています。
b05e124f-c7cc-45a0-a6aa-8cf78c946968
は「ENTERPRISE MOBILITY + SECURITY E5」のGUIDに一致するので、EMS E5のライセンスが付与されていることがわかります。このように 「特定のライセンスの付与状態」を確認したい場合は、自身が調べたいライセンスの skuId をあらかじめ把握しておく必要があります。
assignedLicenses は $filter
クエリをサポートしています。any演算子 と一緒にクエリを投げることで、特定のライセンスが付与されているユーザーを一覧取得できます。$filter=assignedLicenses/any(l:l/skuId eq b05e124f-c7cc-45a0-a6aa-8cf78c946968)
のようにフィルター指定することで、EMS E5が割り当てられているすべてのユーザーが取得できます。ただし、この演算子もまた併用できないようです。and
などでフィルタを組み合わせると以下のエラーが返ってきてしまいました。
{
'error': {
'code': 'Request_UnsupportedQuery',
'message': 'Complex query on property assignedLicenses is not supported.',
'innerError': {
'date': '2022-06-28T15:15:53',
'request-id': 'xxx',
'client-request-id': 'xxx'
}
}
}
まとめると
「サインイン日時と一緒に特定のライセンスが割り当てられたユーザー」は以下URLで取得できます。
https://graph.microsoft.com/beta/users?$select=userPrincipalName,assignedLicenses,signInActivity,accountEnabled&$filter=assignedLicenses/any(l:l/skuId eq <ライセンスのGUID>)
サインイン日時に関わらずアカウントの状態(有効 or 無効)も確認したいので、select に accountEnabled
も指定しています。
PythonでGraph APIにリクエストを送信する
AzureAD関連で何かするときはPowerShellのコマンドレットを使うと楽なことが多いですが、MacOSではそれらが使えません。そこで今回はPythonのライブラリ(プレビュー版)を利用することにしました。まだプレビュー版ですが、今回実現したいことは問題なく実装できました。
クレデンシャルを作成・設定する
特定ユーザーに紐づけずバッチ処理的に実行したかったので、Web API を呼び出すデーモン アプリ - アプリの登録 | Microsoft Docs の方法で作成したクレデンシャルを利用することにしました。ClientSecretCredential
を使うことで、ClientID と Client Secret での認証ができます。
import os
from azure.identity import ClientSecretCredential
client_credential = ClientSecretCredential(
tenant_id=os.getenv('AZURE_TENANT_ID'),
client_id=os.getenv('AZURE_CLIENT_ID'),
client_secret=os.getenv('AZURE_CLIENT_SECRET'))
beta版のエンドポイントを指定する
signInActivity は beta でないと取得できないため、api_version=APIVersion.beta
でAPIバージョンを指定する必要があります。"beta"
のように文字列を直接指定しても同じ結果が得られます。
from msgraph.core import APIVersion, GraphClient
client = GraphClient(credential=client_credential, api_version=APIVersion.beta)
# これも上と同じ
client = GraphClient(credential=client_credential, api_version="beta")
また、 api_version でバージョンを指定せずとも、リクエスト送信する際のURLをフルパスにすればbetaエンドポイントにリクエスト送信できます。バージョン指定するか、URLで直接指定するかは好きな方をどうぞ。
まとめると
最終的に、以下のようなコードで「サインイン日時と一緒に EMS E5ライセンスが割り当てられたユーザー」を取得しました。
import os
from azure.identity import ClientSecretCredential
from msgraph.core import APIVersion, GraphClient
if __name__ == "__main__":
client_credential = ClientSecretCredential(
tenant_id=os.getenv('AZURE_TENANT_ID'),
client_id=os.getenv('AZURE_CLIENT_ID'),
client_secret=os.getenv('AZURE_CLIENT_SECRET'))
client = GraphClient(credential=client_credential, api_version=APIVersion.beta)
# URLを作成
ems_e5_sku_id = "b05e124f-c7cc-45a0-a6aa-8cf78c946968"
s = "userPrincipalName,accountEnabled,assignedLicenses,signInActivity"
f = f"assignedLicenses/any(l:l/skuId eq {ems_e5_sku_id})"
next_link = f"/users?$select={s}&$filter={f}"
# ユーザーをすべて取得
users = []
while next_link is not None:
result = client.get(next_link).json()
users = result["value"]
next_link = result.get("@data.nextLink", None)
print(users)
取得したデータをCSV書き出ししてExcelやスプレッドシートに取り込めばライセンスの棚卸などに役立てられます。メンバーを削除する で抽出したユーザーをグループから削除すればライセンスの剥奪もできます。今回は人間の判断を挟みたかったため、ライセンス剥奪まではスクリプト化しませんでした。