誰がどのMicrosoft 365のライセンスを持っているか、Graph APIをPower Automateから使って一覧取得する

## こんな人向けの記事です
Microsoft 365を活用している企業では、ライセンス管理はコストに直結するのでとても大切です。Microsoft 365管理センターからライセンス数は見えるし、誰にライセンスが付与されているかも確認したりダウンロードはできるけれど、それを自動化したい方向けの記事です。

定期的にCSVなどで決まった場所に出力することで、誰にいつ付けた、外した、付与済みユーザーの推移は？などの分析するためのデータを作ることができます。

マイクロソフトやサービスプロバイダーから年間契約でライセンスを購入したライセンスは在庫。必要なユーザーに対して割り当てますが、たとえば500ライセンスを購入しているのに、実際に割り当てられているユーザーが100人しかいなければ残りの400は使われないまま消えてゆく不良在庫。あー怖い。

瞬間のデータが取れれば推移が取れ、アクションが打てます。経費削減に直結し、Power BIで傾向分析と削減効果を見える化なんてしてみれば、あなたの評価は爆上げでしょう w。

## PowerShellを使う場合は
こちらの記事が参考になります。とっても簡単です。

https://qiita.com/KawasakiSE/items/76e22fd2d6a284eb6a6f

## まずはおさらいから・・・
### ユーザーからみた「持ってるライセンスは何？」
Microsoft 365管理センター＞　ユーザー＞　アクティブなユーザー　と進んで、適当なユーザー名をクリックして開き、「ライセンスとアプリ」の項目を開くと、そのユーザーに割り当てられているライセンスがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/3e0b8f69-8027-b42f-0903-8969d66c7431.png)

私のテナントは[開発者プログラム](https://learn.microsoft.com/ja-jp/office/developer-program/microsoft-365-developer-program)を使って作ったテストテナントなので、ライセンスの種類は少ないですが、一般的にはライセンスの欄にずらりと様々なライセンスが並んでいることでしょう。

下のほうにある「アプリ」に並んでいるのは、上部のライセンスの中身に含まれるもっと細かい「サービスプラン」と呼ばれる項目です。社内のユーザーにはMicrosoft 365 の機能の一部を使わせたくないような場合に、「サービスプラン」をオフにしたりします。

### ライセンスから見た「持ってるユーザーは誰？」
こんどは、どんなライセンスがあって、そこに誰が割り当てられているのかという視点から見てみます。こちらも開発者プランのテナントなので少ないですが、種類がユーザーの項目に表示されていたものと同じですね？
私のテナントの場合、25個のMicrosoft 365 E5ライセンスを持っていて、その中から18個がユーザーにすでに割り当てられていて、残り7個を誰かに必要なら割り当てる余裕のあることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/90e74c04-780f-06bc-dece-f9e49c90159f.png)

Microsoft 365 E5のライセンスを開いてみると、このライセンスが誰に割り当たっているのか一覧表示ができます。ユーザーのエクスポートもできるので、一覧がとれるじゃないか！　ということなのですが。毎月誰かが手作業でダウンロードするのは面倒だし忘れちゃいますよね？　ということで、Power Automateのクラウドフローを使って自動取得しようというのが今回の趣旨です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/d459a348-ae82-b41d-54ff-86a4886c7386.png)

## URLでSKUIDが取得できます
Microsoft 365 管理センターで誰にライセンスが割り当てられているか取得できるわけですが、この画面のURLに注目です。/licensedetailpage/　の後ろにある文字列。これがSKUIDです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/fa9c4ab3-74eb-fb0c-dd1f-580de4dd2495.png)

ちなみに[KawasakiSEさんの記事で紹介されているこちらの記事](https://qiita.com/KawasakiSE/items/76e22fd2d6a284eb6a6f#skuid%E3%82%92%E5%8F%96%E5%BE%97%E3%81%97%E3%81%AA%E3%81%84%E3%81%A8%E3%81%84%E3%81%91%E3%81%BE%E3%81%9B%E3%82%93)のようにPowerShellで確認すると、Microsoft管理センターのURLに表示されている文字列と一致していることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/b6fdd2ff-25e3-bc6e-1f73-ad611960874e.png)

```PowerSHell:
Get-MgSubscribedSku | Select-Object SkuPartNumber,ConsumedUnits,SkuId
```

### Graph Explorerで確認してみる
では、みんな大好きGraph Explorerで取得していきましょう。
[Graph Explorer](https://developer.microsoft.com/ja-jp/graph/graph-explorer)

開いたばかりの状態だとまだ自テナントにサインインできていないので、右側にあるユーザーマークをクリックしてサインインしましょう。（サインインしない場合もサンプルデータで確認できます）
初期状態でURLの入力欄には　[https://graph.microsoft.com/v1.0/me]　と入っていると思います。そのまま「Run Query」ボタンをクリックすると、サインインしているユーザー自身の情報が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/0bb1f46b-c6ee-cc60-ecd7-9b4be5604400.png)

この状態だと情報過多なので、必要な情報だけを表示するように絞ります。これには [$selectを使用します。](https://learn.microsoft.com/ja-jp/graph/api/user-get?view=graph-rest-1.0&tabs=http#example-3-use-select-to-retrieve-specific-properties-of-a-user)

?$select=id,mail,desiplayname　を加えてみました。ずいぶんすっきりしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/6d2a7d48-f096-29f6-dea2-d933cf1b67f3.png)

さらに項目を追加します。今回知りたいのはライセンス情報なので、URLの入力欄の$selectの対象として licenseの文字を入力すると、取得できる候補が現れます。Graph Explorerを使うと、実際にどんな項目を取得できるのかが探せるのが素晴らしいですね。せっかくなので一つずつ試してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/c13a0731-9026-4ce0-bfed-6f34512cc58f.png)

## ライセンス関係の$Select項目
### licenseAssignmentStates
各SKUIDとアクティブかどうか、最後に更新された日時が表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/ada1a558-f3ee-3c68-a6a6-109062ace29a.png)

注目すべきは"disabledPlans"の項目です。　ためしにWhiteboardのサービスプランをOFFにしてみてから再度取得すると、"disabledPlans"にWhiteboardのSKUIDが表示されました。つまり、フルにライセンスを付与したなかのオフにしたサービスプランがこの項目に表示されるようです。引き算の考え方ですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/dc7ae554-ba87-48a3-4e74-c5591afa5d9e.png)

### licenseDetails
ライセンスの詳細のようですが、何にも表示されないですね。謎なので次に行きましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/93405473-d6c4-9c9b-ef1a-43dbb5b7112d.png)

### assignedLicenses
これはシンプルですね。私に付与されているライセンスのSKUIDが３つ並んでます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/79cab66d-2158-e592-44e5-45e064ea18d1.png)

Microsoft 365管理センターのユーザーから確認して比較するとイメージしやすいですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/07363184-a9ee-e5be-73c3-60a0264612ac.png)

### 他のユーザーについて調べるには？
今回のテーマは、特定のライセンスを誰が付与されているかの一覧を取ることでした。さきほどまでGraph Explorerに入力していたURL（エンドポイント）のmeの部分を　usersに変更すると、他のユーザーについて取得できます。　users/<ユーザーのオブジェクトID>　とすると特定のユーザーについて調べられます。ユーザーのオブジェクトIDを指定しない場合は、すべてのユーザーについて取得します。

さっそく試してみると、エラーが出ました。権限が足りないのでタブから許可してと書いてあります。

:::note alert
Forbidden - 403 - 1164 ms. Either the signed-in user does not have sufficient privileges, or you need to consent to one of the permissions on the Modify permissions tab
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/6693ad2d-b0a4-82de-2478-6ecfbe62371b.png)


自分の情報ならば見られましたが、他人の情報を見るには明示的に許可をしてやる必要があります。Graph Explorerもアプリなので、このアプリに他人の情報を見る許可を与えます。

「Modify permissions」を開くと許可できる項目が現れます。「Administrator consent required」がYesになっている項目は、とても強い権限なので管理者のOKがなければ与えられない権限です。Noとなっているものは、ユーザー自身が特別な許可がなくても閲覧できる部分に対するアクセスの許可です。こういうのを「委任」といいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/131a98c0-dbc1-cb10-e309-4476afb94f81.png)

:::note
「ユーザーの代わりに、アプリで組織内の他のユーザーのプロファイル プロパティの基本的なセットの読み取りを実行できるようにします。表示名、氏名、メール アドレス、写真が含まれます。」
:::
「Modify permissions」の中から「User.ReadBasic.All」の「Consent」をクリックしましょう。この項目は説明書きがあるように以下のような操作に対する許可です。

::: note warn
この操作をしているのが全体管理者の場合は、Consent on behalf of your organization のチェックは入れないようにしましょう。誰でも許可した項目を使えるようになってしまいますので慎重に。
:::

アプリに対する「User.ReadBasic.All」の許可を与えた後で、再度クエリーを実行してみると、自分だけではなく他のユーザーについてもライセンスの情報を取得することができているのがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/8a296917-ad91-0dd3-5c4b-68256d4046dd.png)


## いよいよ Power Automateの出番です。
Power AutomateからHTTP要求をするアクションを使います。Graph APIを操作するのに使えるのは、HTTP With Microsoft Entra ID(preauthorized)です。ややこしいのですが、(preauthorized)が以前のHTTP With Microsoft Entra IDでした。改名して括弧がつき、新たに登場したのがHTTP with Microsoft EntraID(無印）です。　無印のほうはあらかじめテナントの管理者がユーザーが利用できる要求のスコープを決めて利用させなければいけません。（この関係の公式文章はまた探しておきます）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/93af93c4-c328-acc0-0b46-40cb0f264e1e.png)

ただし、こちらのHTTP要求はプレミアムコネクタです。プレミアム無しでもGraph APIが利用できる方法についてはこちらの記事が参考になります。

https://qiita.com/MiyakeMito/items/7aa6d52f0c3dfc5d2823

HTTP要求ができるアクションはEntra ID、Outlook、Office365Users,Teams,Groups と複数みつかります。　よっ！　HTTP要求そろい踏み！　以下のエンドポイントに対してぜんぶ同時に試してみます。
```
https://graph.microsoft.com/v1.0/users?$select=id,mail,displayname,assignedLicenses
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/9cd88816-4b1b-9105-775b-c59347a91730.png)

今回のエンドポイントの場合だとOffice 365 Usersのアクションが取得できましたので、これをつかうことにします。（一番左のEntra ID(preauthorized)もいけるはずなのにおかしいな？)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/817ff9f0-e652-3d50-a351-982dac56621f.png)

Office 365 Users の HTTP要求アクションを選択しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/6ef7a150-fd85-e0e1-3d98-3ce988849f8f.png)

## 特定のライセンスにしぼって取得する
取得されるのはユーザーのメールアドレスと、その人がどのライセンスをもっているかのセットです。
すべてのライセンスとユーザーをいっぺんに抽出してもよいのですが、ラインセンスの種類が多いと出力されるデータも膨大になります。

そこで、指定した特定のライセンスだけに出力をしぼるようにエンドポイントのフィルターを追加します。HTTP要求の前に文字列変数を初期化してフィルターとエンドポイントURLの値を設定します。

フィルターには以下のような文字列を設定することで指定の値について絞り込むことができます。

参考：https://learn.microsoft.com/ja-jp/graph/filter-query-parameter?tabs=http#any-operator


```
&$filter=assignedLicenses/any(x:x/skuId eq <取得したいSKUID>)
```
フィルタの文字列をエンドポイントURLの後ろに変数としてくっつけます。エンドポイントURLに直接フィルタ部分も書いてやってよさそうなものですが、なぜかその場合エラーが出ました。こちらは原因不明。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/6b949a9d-74b2-9b7b-4fc6-96fa794b4ad3.png)

テスト実行してみると、確かに指定したユーザーのメールアドレスが取得できています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/ec194922-6e58-774b-28dd-e2c52df5608a.png)

## CSVにしてSharePointに保存
指定したSKUIDをもつユーザーをJSONで取得することができました。シンプルにCSVファイルにしてSharePoint Onlineのドキュメントライブラリに保存してみましょう。「開始」には下記のように指定します。
```
body('HTTP_要求を送信します')?['value']
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/072b131c-0687-7524-bb98-da65d9b1baee.png)

テスト実行してみると、指定した名前でCSVファイルが保存されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/a81775bc-78dd-de97-a440-5d4557f313e6.png)

CSVファイルにはエンドポイントURLに指定した$selectの項目を出力することができました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/b56ee889-6a04-1d5d-b9f0-8423253498be.png)



## まとめ
Graph Apiを使って特定のライセンスをもつユーザーの一覧を取得することができました。assignedLicensesの項目の中にはdisabledPlansの配列も含まれていますので、さらに細かくサービスプランの管理まで行うことも工夫次第でできそうです。

HTTP要求は複数の標準コネクタのアクションに含まれていますが、使うAPIの種類によって得意不得意な分野があるようです。プレミアムを使えなくてもつけるコネクタを見つけるには、今回やってみたような総当たりでチェックすると良いかと思います。

今回の方法で取得できるのは、ある時点のライセンスの状態ですから、定期的にファイル名に日付でも指定して実行し、Power BIを使えば時系列での分析もできます。

取得したライセンス一覧とアクセスログを突き合わせて、棚卸をするのもよいかもしれません。SKUIDの文字列さえ変更すれば複数のライセンスにも対応可能です。取得するSKUIDをリスト化してApply to eachで今回のフィルタ変数のID部分を差し替えてやれば一撃です。

みなさんのライセンス管理のお役に立てればうれしいです。


## 余談ですが
もしも取得した一覧を配列にしたいなら動作が遅いApply to eachを使わずに、「選択」を使った高速変換の方法があります。詳しくはこちらのブログをどうぞ。

https://mocabrown.com/blog/archives/6983

選択の「開始」にはHTTP要求のValueを式で指定します。
```
body('HTTP_要求を送信します')?['value']
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/d301d22a-90c0-af09-8359-0d2378bca207.png)
選択をマップモードにした状態で、マップにはメール項目だけを抜き出すように以下を設定します。
```
item()?['mail']
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/89e9ddff-32ba-b251-60d4-59cd28fe1be1.png)

実行してみると、特定のSKUID（ライセンス）をもつユーザーのメールアドレスだけが抽出できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285141/ae547efb-a8d9-6e9b-afbf-a06953ada5d4.png)