Microsoft Graph Java SDKでユーザーが特定グループのメンバーかを1リクエストで判定する方法(checkMemberGroupsとowners対応)

Microsoft Graph(Java SDK)で「特定ユーザーが特定グループのメンバーか」を確認したいとき、/groups/{id}/members を全件取得してループする実装は、ページングやスロットリングの原因になりやすく、レスポンスも重くなりがちです。この記事では1リクエストで判定する方法と、owners(オーナー)を含める場合の考え方を整理します。

目次

まず押さえる:グループの「members」と「owners」は別の関係

Microsoft Entra ID(旧 Azure AD)のグループは、メンバー(members)とオーナー(owners)を別の関係として持ちます。UIでは「オーナー=所属者」と見えることもありますが、API上は分かれているため、仕様を決めずに実装すると「想定外に判定できない/一覧に出ない」が起きます。

やりたいこと代表エンドポイント(v1.0)返るもの推移(ネスト)メモ
メンバー一覧/groups/{groupId}/members直接メンバー(directoryObjectの集合)非推移「直接」だけ。ネストは別API
ネスト含むメンバー一覧/groups/{groupId}/transitiveMembers推移メンバー(フラットな一覧)推移入れ子グループまで展開
オーナー一覧/groups/{groupId}/ownersオーナー(ユーザー/サービスプリンシパル)概念なしオーナーは権限者。グループによっては取得不可の注意あり
ユーザーが候補グループ群に属するか判定/users/{userId}/checkMemberGroups「候補のうち所属しているグループID」だけ推移最大20グループを一括判定
ユーザーが属する全グループIDを取得/users/{userId}/getMemberGroupsユーザーが属する全グループID推移最大11,000件。超えるとエラーになり得る

「members()」で取れるのはメンバーだけ?オーナーも含む?

Java SDKの members() は /groups/{id}/members に対応し、基本的にグループの直接メンバーを返します。この操作は非推移で、入れ子グループの中のユーザーまで自動で展開はされません。

一方でオーナーは /groups/{id}/owners の別関係です。オーナーが常にメンバーにも入っているとは限らないため、members だけで「所属者全員」を表現するのは危険です。

入れ子(推移)メンバーシップがあると、判定結果が食い違う例

典型例として、グループAのメンバーにグループBが追加され、ユーザーUはグループBのメンバー、という構造を考えます。

  • /groups/A/members:返るのは「B(グループ)」で、Uは直接メンバーではないので出てきません
  • /groups/A/transitiveMembers:B配下のUまで展開され、Uが返ります
  • /users/U/checkMemberGroups(候補にAを渡す):推移判定なのでAが返り、UはAに「所属している」と判定されます

この差は「どの所属を判定したいか(直接 or 推移)」を明確にしないと、バグに見える挙動になります。

1リクエストでメンバー判定するなら checkMemberGroups が手早い

「特定ユーザーが特定グループのメンバーか」を1回の呼び出しで判定したいなら、まず検討すべきは checkMemberGroups です。ユーザー(DirectoryObject)に対して、判定したいグループIDを最大20個まで渡すと、そのユーザーがメンバーであるグループIDだけが返ります。

返り値が「一致したIDだけ」なので、contains で判定でき、members一覧を全件取得して突合するより転送量と処理が小さくなります。

HTTPで見る checkMemberGroups(ユーザーに対して判定)

POST https://graph.microsoft.com/v1.0/users/{userId}/checkMemberGroups
Authorization: Bearer {access_token}
Content-Type: application/json

{
"groupIds": [
"{groupId}"
]
}

レスポンスは「一致したグループIDの配列」です。{groupId} が含まれていればメンバー、含まれていなければ非メンバーです。

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(Edm.String)",
  "value": [
    "{groupId}"
  ]
}

Java SDK(6.x系)の実装イメージ

SDKのバージョンによりクラス名や戻り値の型が変わることがありますが、6.x系(Kiotaベース)では概ね次の形です。ここでは認証(requestAdapter作成)は済んでいる前提にしています。

GraphServiceClient graphClient = new GraphServiceClient(requestAdapter);

List<String> groupIds = List.of(groupId);

CheckMemberGroupsPostRequestBody body = new CheckMemberGroupsPostRequestBody();
body.setGroupIds(groupIds);

// userId に対して判定(/users/{id}/checkMemberGroups)
var result = graphClient.users()
    .byUserId(userId)
    .checkMemberGroups()
    .post(body);

// 返却は「文字列IDの配列(value)」になるので、SDKの型に合わせて取り出す
List<String> matched = result.getValue();

boolean isMember = matched != null && matched.contains(groupId);

最大20グループ制限を超えるときの分割(Java例)

checkMemberGroups は1回で最大20グループまでという制約があります。チェック対象が多い場合は、20件ずつに分割して呼び出し、結果を結合します。

List<String> allCandidateGroupIds = ...; // 判定したい候補(例:ロールに紐づくグループID群)
Set<String> memberGroupIds = new HashSet<>();

for (int i = 0; i < allCandidateGroupIds.size(); i += 20) {
  List<String> chunk = allCandidateGroupIds.subList(i, Math.min(i + 20, allCandidateGroupIds.size()));

  CheckMemberGroupsPostRequestBody body = new CheckMemberGroupsPostRequestBody();
  body.setGroupIds(chunk);

  var res = graphClient.users().byUserId(userId).checkMemberGroups().post(body);
  if (res != null && res.getValue() != null) {
    memberGroupIds.addAll(res.getValue());
  }
}

// 目的のグループに所属しているか
boolean isMember = memberGroupIds.contains(targetGroupId);

「候補が多すぎる」なら getMemberGroups を先に取る手もある

候補グループIDが何百・何千とある場合、checkMemberGroups を分割して呼ぶより、getMemberGroups で「ユーザーが属するグループIDを一括取得」してから、アプリ側で突合する方が速い場面もあります。とはいえ getMemberGroups には返却上限(最大11,000件)があり、超える場合は別APIが推奨される点に注意してください。

重要:checkMemberGroups は owners(オーナー)を判定しない

ここが一番つまずきやすいポイントです。checkMemberGroups は「メンバー(members)」の判定であり、オーナー(owners)という関係は見ません。ユーザーが「オーナーとして設定されているだけ」で「メンバーではない」場合、チェックしてもヒットしない可能性があります。

また、オーナーはグループによってはGraphで取得できない制約があることや、サービスプリンシパルが一覧に出ない時期があることも、仕様として押さえておくと安全です。

運用で解決するか、ロジックで吸収するか

「所属=メンバーだけ」と割り切れるなら判定がシンプルになります。一方で「所属=メンバー+オーナー」という要件なら、実装も二段構えにしておくのが堅実です。

方針メリット注意点おすすめ場面
オーナーは必ずメンバーにも追加(運用/自動化)判定ロジックが単純(members判定だけで済む)例外が出ると破綻。動的グループでは運用が難しいことがある権限管理をメンバーシップに一本化したい
メンバー判定とオーナー判定を分ける(実装で吸収)データモデルに忠実で例外に強いAPI呼び出しが増える。キャッシュやバッチ化が必要「所属=メンバー+オーナー」が要件として明確

オーナーも含めた「所属判定」テンプレ

「所属=members または owners」と定義するなら、次のように組み立てると実装が読みやすくなります。

  • メンバー判定:checkMemberGroups で groupId が返るかを見る
  • オーナー判定:/groups/{groupId}/owners を取得して userId が含まれるかを見る(グループのオーナーは最大100)

「HTTP呼び出し自体を1回にしたい」場合は、GraphのJSONバッチ($batch)に「メンバー判定用のPOST」と「owners取得用のGET」をまとめると、ネットワーク往復を1回にできます。

「所属している人(オーナーも含めた全員)」を取得する方法

結論としては、members と owners をそれぞれ取得し、IDで突合して統合(重複排除)するのが最も分かりやすく、仕様にも忠実です。同一人物が両方に現れるケースがあるため、重複排除が前提になります。

取得に使うAPI(まずは最小フィールドで)

  • メンバー一覧:GET /groups/{groupId}/members
  • オーナー一覧:GET /groups/{groupId}/owners

一覧系APIは対象が directoryObject の集合になりやすいので、実務では $select と OData cast(ユーザーだけに絞る)を組み合わせると扱いやすくなります。

GET https://graph.microsoft.com/v1.0/groups/{groupId}/members/microsoft.graph.user?$select=id,displayName,userPrincipalName
GET https://graph.microsoft.com/v1.0/groups/{groupId}/owners/microsoft.graph.user?$select=id,displayName,userPrincipalName

$filter や OData cast を使う場合、ConsistencyLevel: eventual と $count が必要になるケースがあるため、必要になったタイミングで付ける方針にするとハマりにくいです。

統合の考え方(重複排除)

// 疑似コード(DirectoryObject / User の扱いはSDKに合わせて読み替え)
Map<String, Object> peopleById = new HashMap<>();

for (Object m : members) {
peopleById.put(getId(m), m);
}
for (Object o : owners) {
peopleById.put(getId(o), o); // 既に存在すれば上書きされるだけ(重複排除)
}

// peopleById.values() が「メンバー+オーナー」の統合結果

入れ子グループも含めた「全メンバー」を取りたい場合

セキュリティグループで「グループがグループを含む」構造がある場合、/groups/{id}/members だけではネスト内のユーザーまで出てきません。その場合は /groups/{id}/transitiveMembers を使うと、推移的に展開されたフラットな一覧を取得できます。

権限(Permission)設計のポイント

Graphは「動くまでに時間がかかった」の原因が、コードよりも権限不足(403)であることが珍しくありません。最初は最小権限で構成し、必要に応じて段階的に強い権限へ広げるのが安全です。

API最小権限の例(代表)補足
checkMemberGroups(他ユーザー)User.ReadBasic.All + GroupMember.Read.Allより強い権限として Directory.Read.All なども選択肢
/groups/{id}/membersGroupMember.Read.AllHidden membership の読み取りでは追加権限が必要
/groups/{id}/ownersGroupMember.Read.AllExchange作成/配布リスト/オンプレ同期のグループでは取得できない場合がある

また、Graphのベストプラクティスとしても「最小権限(least privilege)」が推奨されています。アプリの監査・運用まで見据えるなら、ドキュメントの permissions セクションをベースに設計するのが近道です。

よくある落とし穴と対策

403 Forbidden(権限不足)

  • アプリ登録で必要スコープが付与されていない
  • 管理者同意(Admin consent)が未実施
  • 委任権限の場合、サインインユーザー側のロールや可視性制約(Hidden membership)に引っかかっている

Graph公式のトラブルシュートでは、トークン取得フローや権限・同意の確認が推奨されています。

429 Too Many Requests(スロットリング)

members一覧を全件取得してループする実装は、リクエスト数と転送量が増えやすく、スロットリングの温床です。checkMemberGroupsで「判定」を寄せると、API回数と転送量が大きく減ることが多いです。429が出た場合は、Retry-After を尊重しつつ指数バックオフで再試行します。

404 Not Found が直後だけ出る

ユーザーやグループを作成した直後に参照すると、複製(レプリケーション)遅延で一時的に404になることがあります。時間を置いてリトライし、改善しない場合は待ち時間を伸ばします。

まとめ:メンバー判定は checkMemberGroups、所属者一覧は members+owners の統合

  • 「特定ユーザーが特定グループのメンバーか」を1回で判定したいなら checkMemberGroups
  • checkMemberGroups は推移(入れ子)判定。直接だけを見たい場合は /groups/{id}/members を検討
  • owners は members と別管理。所属に含めたいなら owners も別途取得して統合する
  • 大量アクセスではキャッシュ・リトライ(429対策)・最小権限の設計が効く

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次