Microsoft 365を運用するうえで、Entra ID (旧Azure AD) の管理者ロールが混在・乱立しがちで、現在どのユーザーにどのロールが付与されているかを把握するのが難しい――こういった悩みを抱えている担当者の方は多いのではないでしょうか。
特に、グローバル管理者やPIM (Privileged Identity Management) を利用して一部のロールを運用している場合、 “Direct(直接割り当て)” と “PIM(適格性/アクティブの一時ロール)” が混在し、管理が煩雑になります。
そこで本記事では、Entra IDの管理者ロール割り当て情報を一括で取得し、CSVで出力するPowerShellスクリプトをご紹介します。
※本記事で案内している設定内容および変更方法は、作成日時点でのものであり、マイクロソフトの方針により変更される場合があります。
- スクリプトの概要(目的・特徴)
- 実行手順
- 実際のスクリプト
- まとめ
スクリプトの概要(目的・特徴)
今回ご紹介するスクリプトの主な特徴は、以下の通りです。
ロールの割り当てソースを判別
- Direct (直接割り当て) なのか
- PIM (Privileged Identity Managementによりアクティブ/適格性 機能が割り当てられているもの) なのか
ロール名の後に (PIM) または (Direct) と表示することで、どのユーザーがどの仕組みでロールを付与されているかを可視化します。
CSVファイルに整形して出力
各ユーザーごとに所属ロールをまとめて出力するため、台帳や監査レポートとの比較が容易です。
Microsoft Graph PowerShell モジュールを使用
従来のAzure AD PowerShellモジュールではなく、Microsoft Graph PowerShellを活用しており、今後の拡張やサポートにも適応しやすい構造です。
必要な前提条件
- グローバル管理者 (Tenant全体の管理権限)
- Microsoft Graph PowerShellのインストール (バージョン等は適宜最新を推奨)
スクリプトの全体像
スクリプト:Azure AD ロール割り当て情報の取得と CSV エクスポート
説明:ユーザーごとに保持しているロールをまとめ、1つの CSV ファイルに出力します。 ロール名の後のカッコ内に、AssignmentSource('PIM' または 'Direct')を表示します。
# ------------------------------- # スクリプト: Azure AD ロール割り当て情報の取得と CSV エクスポート(ユーザーごとのロール一覧・修正版) # 説明 : ユーザーごとに保持しているロールをまとめ、1つの CSV ファイルに出力します。 # ロール名の後のカッコ内に、AssignmentSource('PIM' または 'Direct')を表示します。 # ------------------------------- # 1. Microsoft Graph に接続 Connect-MgGraph -Scopes 'Directory.Read.All', 'RoleManagement.Read.Directory', 'User.Read.All', 'GroupMember.Read.All' # 2. PIM 管理の Eligible および Active のロールのスケジュール インスタンスを取得し、プロパティを追加 $PimEligibleAssignments = Get-MgRoleManagementDirectoryRoleEligibilityScheduleInstance -All | Select-Object PrincipalId, RoleDefinitionId, AssignmentState, @{Name='AssignmentSource';Expression={'PIM'}} $PimActiveAssignments = Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -All | Select-Object PrincipalId, RoleDefinitionId, AssignmentState, @{Name='AssignmentSource';Expression={'PIM'}} #3. 直接割り当てられたロールの割り当てを取得 $DirectAssignments = @() $Roles = Get-MgDirectoryRole -All foreach ($Role in $Roles) { $Members = Get-MgDirectoryRoleMember -DirectoryRoleId $Role.Id -All foreach ($Member in $Members) { $DirectAssignments += [PSCustomObject]@{ RoleDefinitionId = $Role.RoleTemplateId # ロールテンプレート ID を使用 PrincipalId = $Member.Id AssignmentState = 'DirectAssignment' AssignmentSource = 'Direct' } } } # 4. 全てのロール定義を取得し、Id をキー、DisplayName を値とするハッシュ テーブルを作成 $RoleDefinitionMap = @{} Get-MgRoleManagementDirectoryRoleDefinition -All | ForEach-Object { $RoleDefinitionMap[$_.Id] = $_.DisplayName } # 5. すべての割り当てを一つのコレクションにまとめる $AllAssignments = $DirectAssignments + $PimActiveAssignments + $PimEligibleAssignments # 6. 全てのユーザー ID を取得し、一度にユーザー情報を取得 $UserIds = $AllAssignments | Select-Object -ExpandProperty PrincipalId -Unique # ユーザー ID リストを分割して処理(ID の数が多すぎる場合の対策) $UserMap = @{} $ChunkSize = 100 # 適宜調整してください for ($i = 0; $i -lt $UserIds.Count; $i += $ChunkSize) { $Chunk = $UserIds[$i..([math]::Min($i + $ChunkSize - 1, $UserIds.Count - 1))] $UserIdStrings = $Chunk -join "','" $UserFilter = "Id in ('$UserIdStrings')" $ChunkUsers = Get-MgUser -Filter $UserFilter -All foreach ($User in $ChunkUsers) { $Email = if ($User.Mail) { $User.Mail } else { $User.UserPrincipalName } $UserMap[$User.Id] = @{ DisplayName = $User.DisplayName UserEmail = $Email } } } # 7. ユーザーごとに保持しているロールをまとめる # ユーザー ID でグループ化 $UserAssignments = $AllAssignments | Group-Object -Property PrincipalId # 各ユーザーのロールをリスト化 $UserRoleList = @() foreach ($Group in $UserAssignments) { $UserId = $Group.Name $UserInfo = $UserMap[$UserId] if ($UserInfo) { # ユーザーが保持しているロールの名前を取得 $Roles = $Group.Group | ForEach-Object { $RoleName = $RoleDefinitionMap[$_.RoleDefinitionId] $AssignmentSource = $_.AssignmentSource $RoleDisplay = "$RoleName ($AssignmentSource)" $RoleDisplay } # ユーザーオブジェクトを作成 $UserObject = [PSCustomObject]@{ DisplayName = $UserInfo.DisplayName UserEmail = $UserInfo.UserEmail Roles = $Roles } $UserRoleList += $UserObject } } # 8. ロールのカラム(列)を動的に生成する # ユーザーが持つ最大ロール数を取得 $MaxRoleCount = ($UserRoleList | ForEach-Object { $_.Roles.Count } | Measure-Object -Maximum).Maximum # 列を定義 $SelectColumns = @('DisplayName', 'UserEmail') for ($i = 1; $i -le $MaxRoleCount; $i++) { $SelectColumns += "Role$i" } # 9. 結果を整形 $Results = $UserRoleList | ForEach-Object { $UserProps = @{ DisplayName = $_.DisplayName UserEmail = $_.UserEmail } # ロールを動的にプロパティに追加 for ($i = 0; $i -lt $MaxRoleCount; $i++) { $RoleName = if ($i -lt $_.Roles.Count) { $_.Roles[$i] } else { $null } $UserProps["Role$($i + 1)"] = $RoleName } New-Object -TypeName PSObject -Property $UserProps } # 10. CSV ファイルにエクスポート # ファイルパスを実際のパスに設定してください $CsvFolderPath = "C:\temp\ADロール出力スクリプト\出力先" $CsvPath = Join-Path -Path $CsvFolderPath -ChildPath "UserRoles.csv" # フォルダーの存在を確認し、存在しない場合は作成 if (-Not (Test-Path -Path $CsvFolderPath)) { New-Item -ItemType Directory -Path $CsvFolderPath -Force | Out-Null } # 結果をエクスポート $Results | Select-Object $SelectColumns | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8 # 必要に応じて、UTF8 BOM を付与 $Utf8WithBomEncoding = New-Object System.Text.UTF8Encoding($true) $CsvContent = Get-Content -Path $CsvPath [System.IO.File]::WriteAllLines($CsvPath, $CsvContent, $Utf8WithBomEncoding) # 11. Microsoft Graph から切断 Disconnect-MgGraph
実行手順
ここでは、実際のスクリプトを例示しながら、実行までの手順を解説します。
必要な環境の準備
PowerShellを起動し、Microsoft Graph PowerShellがインストールされていない場合はInstall-Module Microsoft.Graph -Scope CurrentUserなどでインストールします。
なお、実行環境のPowerShellのバージョンは5.1以上を推奨します。
Graph への接続
最初に Connect-MgGraph -Scopes … を実行し、テナント側でグローバル管理者アカウントを使ってサインインします。
これによって、スクリプトがAzure AD (Entra ID)のロール割り当て情報にアクセスできるようになります。
スクリプトの実行
本スクリプトを .ps1 ファイルとして保存し、PowerShell上から .\ファイル名.ps1 のように実行します。
途中でユーザー一覧の取得やロール一覧の取得が走るため、ユーザー数やロール数が多い環境では多少時間がかかることもあります。
CSVの出力先を指定
スクリプト内の $CsvFolderPath でCSVファイルの保存先フォルダを指定できます。
今回のスクリプトでは “C:\temp\ADロール出力スクリプト\出力先” となっているため、ここを各自の環境に合わせて変更してください。
ファイル名は “UserRoles.csv” となり、先ほど指定したフォルダに自動保存されます。
結果の確認
スクリプトが正しく完了すると、UserRoles.csv が作成されます。
開いてみると、各ユーザーごとにRole1, Role2, …といった形式で割り当てられているロールが並んでいます。“(Direct)” と “(PIM)” が区別付きで表示されるので、台帳や監査上の突合が非常にやりやすくなります。
実際のスクリプト
# 1. Microsoft Graph に接続
Connect-MgGraph -Scopes 'Directory.Read.All', 'RoleManagement.Read.Directory', 'User.Read.All', 'GroupMember.Read.All'
解説
Microsoft Graph PowerShellにサインインするためのコマンドです。
このコマンドによってMicrosoft Graph PowerShellモジュールにサインインし、Azure AD (Entra ID) の情報を取得するための認可を受けます。
実行時にポップアップでログイン画面が表示されるので、グローバル管理者アカウントを使って認証します。
Scopesの指定により、ディレクトリ情報の閲覧やロール管理情報の閲覧などに必要な権限をまとめて要求しています。
# 2. PIM管理のEligible / Activeロール情報を取得
$PimEligibleAssignments = Get-MgRoleManagementDirectoryRoleEligibilityScheduleInstance -All | Select-Object PrincipalId, RoleDefinitionId, AssignmentState, @{Name='AssignmentSource';Expression={'PIM'}} $PimActiveAssignments = Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -All | Select-Object PrincipalId, RoleDefinitionId, AssignmentState, @{Name='AssignmentSource';Expression={'PIM'}}
解説
(1)まず、「Eligible(適格)」のPIM割り当て情報を取得
Get-MgRoleManagementDirectoryRoleEligibilityScheduleInstance -All
ここで「まだ有効化はしていないが、必要に応じてロールをアクティブ化できる人」をすべて集めます。
(2)次に、「Active(アクティブ)」のPIM割り当て情報を取得
Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -All
「現在すでにロールが有効で、管理操作ができる人」をすべて集めます。
(3)それぞれの結果に対して、Select-Objectで任意の項目だけを抜き出しながらAssignmentSource = 'PIM'というプロパティをつける
これで「このロールはPIM経由で付与されたものだよ」とマークして区別しやすくしています。
オプションの意味
通常、APIの呼び出しは複数ページに分割されることがありますが、「-All」をつけると全ページを一度に取得できます。 大量ユーザーが存在しても一括で情報を取りこぼさずに取得できます。
イメージしやすい例
- Eligible:有効化の“資格”があるだけの人
- Active :今まさに“有効”になっている人
両方ともPIMを使って割り当てられているので、AssignmentSource = 'PIM' と付けてわかりやすくしています。
# 3. 直接割り当てられたロールの割り当てを取得
$DirectAssignments = @() $Roles = Get-MgDirectoryRole -All foreach ($Role in $Roles) { $Members = Get-MgDirectoryRoleMember -DirectoryRoleId $Role.Id -All foreach ($Member in $Members) { $DirectAssignments += [PSCustomObject]@{ RoleDefinitionId = $Role.RoleTemplateId # ロールテンプレート ID を使用 PrincipalId = $Member.Id AssignmentState = 'DirectAssignment' AssignmentSource = 'Direct' } } }
解説
(1)最初に、$DirectAssignments という空のリスト(配列)を用意
ここに「直接割り当て(Direct)されたユーザーとロール」の情報を順次入れていきます。
(2)Get-MgDirectoryRole -All で、Azure AD (Entra ID) 上に存在するロール(例: Global Administrator やExchange Administratorなど)をすべて取得
-All を付けることで、複数ページに分かれた結果を一度に集めます。
(3)取得したそれぞれのロールに対して、Get-MgDirectoryRoleMember を呼び出して“そのロールが直接割り当てられているメンバー(ユーザー)”のリストを取得
foreach ($Role in $Roles) と foreach ($Member in $Members) で二重ループしているのは、各ロールに所属するメンバーを一人ずつ順番に処理するためです。
(4)1件ごとに [PSCustomObject] を作成
下記のように、ロールID・ユーザーID・割り当て状態などをまとめて1オブジェクトにします。
RoleDefinitionId = $Role.RoleTemplateId
PrincipalId = $Member.Id
AssignmentState = 'DirectAssignment'
AssignmentSource = 'Direct'
これらのオブジェクトを “$DirectAssignments” に+= で追加していくと、結果的に $DirectAssignments には「ロールA - ユーザーX」「ロールB - ユーザーY」…と、直接割り当ての一覧となります。
イメージしやすい例
- 直接割り当てとは、「あらかじめユーザーにグローバル管理者権限を付けている」「Exchange管理者ロールを付けている」等の状態
- PIM (一時的/承認あり) とは違い、このスクリプトで拾えるのは“恒常的にロールが割り当てられている”ユーザー
# 4. 全てのロール定義を取得し、ハッシュ テーブルを作成
$RoleDefinitionMap = @{} Get-MgRoleManagementDirectoryRoleDefinition -All | ForEach-Object { $RoleDefinitionMap[$_.Id] = $_.DisplayName }
解説
(1)Get-MgRoleManagementDirectoryRoleDefinition -All で、Azure AD(Entra ID)のロール定義をすべて取得
ここで言う“ロール定義” とは「Global Administrator」「SharePoint Administrator」などの管理者ロールを指します。
(2)それぞれのロール定義について、Id (内部ID) と DisplayName (表示名)を取り出して、$RoleDefinitionMap というハッシュテーブルに保存
ハッシュテーブルでは、キーとしてロールの内部IDが使われており、それに対応する値としてロール名(DisplayName)が設定されています。
イメージしやすい例
たとえば “62e90394-69f5-4237-9190-012177145e10” といったロールのIDを、“Global Administrator” としてわかりやすい名称で扱えるようになります。
こうすることで後の工程で CSVを見たときに「ロール名 (PIM)」と表示され、監査担当や管理者に優しいです。
# 5. すべての割り当てを一つのコレクションにまとめる
$AllAssignments = $DirectAssignments + $PimActiveAssignments + $PimEligibleAssignments
解説
(1) $DirectAssignments(直接割り当て)と、$PimActiveAssignments & $PimEligibleAssignments(PIM経由のアクティブ/適格)を結合して、$AllAssignments という一つのコレクションに追加
これにより、PIMとDirectの両方のロール割り当て情報が一か所にまとまります。
まとめた情報は、ユーザーIDやロールIDなどの基本プロパティを同じ形式で持ち、後の工程でまとめて扱えるようになります。
イメージしやすい例
DirectAssignmentsには「恒常的に付与されているロール」、PimActiveAssignments/PimEligibleAssignmentsには
「現在アクティブなロール」「将来アクティブ化できるロール」が格納されている- それらを
$AllAssignmentsに集約すれば、いわば「全ユーザー × 全ロール割り当て」のリストが完成し、余すところなくロール情報を取り扱える。
# 6. 全てのユーザー ID を取得し、一度にユーザー情報を取得
$UserIds = $AllAssignments | Select-Object -ExpandProperty PrincipalId -Unique # ユーザー ID リストを分割して処理(ID の数が多すぎる場合の対策) $UserMap = @{} $ChunkSize = 100 # 適宜調整してください for ($i = 0; $i -lt $UserIds.Count; $i += $ChunkSize) { $Chunk = $UserIds[$i..([math]::Min($i + $ChunkSize - 1, $UserIds.Count - 1))] $UserIdStrings = $Chunk -join "','" $UserFilter = "Id in ('$UserIdStrings')" $ChunkUsers = Get-MgUser -Filter $UserFilter -All foreach ($User in $ChunkUsers) { $Email = if ($User.Mail) { $User.Mail } else { $User.UserPrincipalName } $UserMap[$User.Id] = @{ DisplayName = $User.DisplayName UserEmail = $Email } } }
解説
(1) $UserIds = $AllAssignments | Select-Object -ExpandProperty PrincipalId -Unique
割り当て情報($AllAssignments)からユーザーID(PrincipalId)だけをユニークに抽出します。
(2)大量のユーザーがいる場合に備え、ChunkSize(例:100)ごとに小分けして Get-MgUser -Filter … で取得
(3)取得してきたDisplayNameやMailを $UserMap に保存。キーはユーザーID、値は表示名やメールアドレス
イメージしやすい例
- たとえば 500 個のユーザーIDがあり、そのまま
Get-MgUserで1度に呼ぶとクエリエラーが起こるかもしれない - そこで 100件ずつの小分けにし、
Id in ('…','…')の形で複数IDをまとめてフィルターして情報を取得する - 返ってきたユーザー情報は「ユーザーIDをキー」「表示名・メールアドレスを値」とする辞書(ハッシュテーブル)に格納しておく
# 7. ユーザーごとに保持しているロールをまとめる
# ユーザー ID でグループ化 $UserAssignments = $AllAssignments | Group-Object -Property PrincipalId # 各ユーザーのロールをリスト化 $UserRoleList = @() foreach ($Group in $UserAssignments) { $UserId = $Group.Name $UserInfo = $UserMap[$UserId] if ($UserInfo) { # ユーザーが保持しているロールの名前を取得 $Roles = $Group.Group | ForEach-Object { $RoleName = $RoleDefinitionMap[$_.RoleDefinitionId] $AssignmentSource = $_.AssignmentSource $RoleDisplay = "$RoleName ($AssignmentSource)" $RoleDisplay } # ユーザーオブジェクトを作成 $UserObject = [PSCustomObject]@{ DisplayName = $UserInfo.DisplayName UserEmail = $UserInfo.UserEmail Roles = $Roles } $UserRoleList += $UserObject } }
解説
(1) #5で作成した $AllAssignments (すべての割り当て) をユーザーID(PrincipalId)ごとにグルーピング
(2)ユーザーIDごとに、どのロールが割り当てられているかをリストアップ
(3)グループ化した情報を、DisplayName や UserEmail などの実際のユーザー情報と結びつけて、“ユーザーごとに保持しているロール一覧” をまとめた配列($UserRoleListなど)を作成
イメージしやすい例
たとえば $AllAssignments には、以下のような要素が混在しているとします。
(User1, RoleA), (User1, RoleB), (User2, RoleC), (User3, RoleA), …
これをユーザーIDごとにまとめると、以下のようになります。
- User1 → [RoleA, RoleB]
- User2 → [RoleC]
- User3 → [RoleA]
このような形で、ユーザーが持つロールの一覧(配列)が得られます。
最終的には DisplayName や UserEmail も含めて “ユーザー情報+ロールのリスト” を1行でまとめられるようになります。
# 8. ロールのカラム(列)を動的に生成する
# ユーザーが持つ最大ロール数を取得 $MaxRoleCount = ($UserRoleList | ForEach-Object { $_.Roles.Count } | Measure-Object -Maximum).Maximum # 列を定義 $SelectColumns = @('DisplayName', 'UserEmail') for ($i = 1; $i -le $MaxRoleCount; $i++) { $SelectColumns += "Role$i" }
解説
(1) ユーザーごとに保持しているロールの数は人によって異なるため、「最大ロール数」を先に調べる
(2)その数に応じて “Role1, Role2, …” という列名を動的に作成し、CSV出力に使えるように準備
これにより、Excelなどで開いたとき、ユーザーごとに同じ列構成でロール一覧を表示できるようになります。
イメージしやすい例
ユーザーAは3つのロールを持ち、ユーザーBは1つしか持っていない場合、列数が統一されていない場合、比較しにくい表になります。
そこで “MaxRoleCount = 3” の場合はRole1, Role2, Role3 の3列を用意し、持っていない分は空欄にしておくと、レポートとして整った表ができあがるので、Excelや他のツールで並び替えや検索がしやすくなります。
# 9. 結果を整形
$Results = $UserRoleList | ForEach-Object { $UserProps = @{ DisplayName = $_.DisplayName UserEmail = $_.UserEmail } # ロールを動的にプロパティに追加 for ($i = 0; $i -lt $MaxRoleCount; $i++) { $RoleName = if ($i -lt $_.Roles.Count) { $_.Roles[$i] } else { $null } $UserProps["Role$($i + 1)"] = $RoleName } New-Object -TypeName PSObject -Property $UserProps }
解説
(1) #7や#8で用意した「ユーザーごとのロール一覧」と「最大ロール数」を使って、“最終的に行データ(PSObject)として整形”
(2)ユーザーの基本情報(例: DisplayName, UserEmail)を $UserProps に入れ、さらに Role1, Role2… と必要な欄を埋める
for ($i = 0; $i -lt $MaxRoleCount; $i++) { … } のループで、ユーザーが持つロールを順番に当てはめます。
もしロール数が足りない場合は $null(空欄)にしてください。
(3)作成したオブジェクトを $Results に追加し、CSV出力しやすい形にまとめる
イメージしやすい例
ユーザーAはロールが3つ、ユーザーBは1つしかないケースの場合、以下のようになります。
- ユーザーA:Role1, Role2, Role3に値を入れて、Role4以降は空欄
- ユーザーB:Role1だけに値を入れ、Role2, Role3… は空欄
こうして整形されたデータを行単位で並べれば、見やすいテーブル(リスト)になります。
# 10. CSV ファイルにエクスポート
# ファイルパスを実際のパスに設定してください $CsvFolderPath = "C:\temp\ADロール出力スクリプト\出力先" $CsvPath = Join-Path -Path $CsvFolderPath -ChildPath "UserRoles.csv" # フォルダーの存在を確認し、存在しない場合は作成 if (-Not (Test-Path -Path $CsvFolderPath)) { New-Item -ItemType Directory -Path $CsvFolderPath -Force | Out-Null } # 結果をエクスポート $Results | Select-Object $SelectColumns | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8 # 必要に応じて、UTF8 BOM を付与 $Utf8WithBomEncoding = New-Object System.Text.UTF8Encoding($true) $CsvContent = Get-Content -Path $CsvPath [System.IO.File]::WriteAllLines($CsvPath, $CsvContent, $Utf8WithBomEncoding)
解説
(1)まず、出力先のパス($CsvFolderPath)を確認し、存在しなければフォルダを作成
(2)$Results に入っているデータ(一行ごとにユーザーとロール情報がまとめられたオブジェクト群)を Select-Object で列を指定し、Export-Csv コマンドでCSVファイルに書き出す
(3)文字化けしないように、最後にUTF-8 (BOM付き) へ書き換え
イメージしやすい例
$Results はすでに各行(ユーザー)が「DisplayName, UserEmail, Role1, Role2, …」などのプロパティを持っています。
以下のようなイメージです。
| DisplayName | UserEmail | Role1 | Role2 |
|---|---|---|---|
| 山田太郎 | taro@example.com | Global Admin (Direct) | Teams Admin (PIM) |
Export-Csv で「UserRoles.csv」として保存すれば、この表がそのままCSVの形になります。
# 11. Microsoft Graph から切断
Disconnect-MgGraph
解説
(1)Disconnect-MgGraph コマンドを呼び出し、Microsoft Graph への接続(セッション)を終了
これにより、スクリプト実行中に取得していた認証情報やトークンが解放されます。
まとめ
Entra IDの管理者ロール設定は、セキュリティ強化の観点でも非常に重要な要素です。
どのユーザーがどのロールを持ち、DirectかPIMかといった情報を一目で可視化できるこのスクリプトは、台帳や監査レポートとの突合にも役立ちます。
今回は、必要な権限こそグローバル管理者権限が求められるものの、一度セットアップしてしまえば「依頼時のみ実行」してCSVを比較する運用が可能です。
定期的にチェックする場合はスケジューラなどを活用することで、自動的にレポートを生成する仕組みにも発展させられるでしょう。
本記事で紹介したスクリプトが、皆様のAzure AD (Entra ID) ロール管理の効率化やセキュリティ強化に少しでもお役に立てれば幸いです。