Azure CLI で Azure AD アプリケーションを作成して既定のスコープを削除する

Azure CLI でアプリ登録すると既定で user_impersonation っていうスコープが作成されるけど使わないので消したい。

# Azure にログイン
az login

# アプリの新規登録
$app = az ad app create --display-name "DeleteScopeApp"

# アプリケーションIDを取得
$appId = ($app | ConvertFrom-Json).appId

# スコープを取得
$oauth2Permissions = ($app | ConvertFrom-Json).oauth2Permissions

# 既定のスコープを無効に設定する(有効だと削除できないため)
$oauth2Permissions[0].isEnabled = $false

# 変更したオブジェクトを JSON 形式に変換する
$oauth2Permissions = ConvertTo-Json -InputObject @($oauth2Permissions)

# JSON をファイルに出力する
$oauth2Permissions | Out-File -FilePath .\oauth2Permissions.json

# 無効化を実行する
az ad app update --id $appId --set oauth2Permissions=`@oauth2Permissions.json

# 削除を実行する
az ad app update --id $appId --set oauth2Permissions="[]"

# 不要なファイルを削除する
Remove-Item -Path .\oauth2Permissions.json

何故かは良くわからないのだけど設定用の JSON はファイルに書き出さないとうまくいかなかった。公式には以下の記載があるが Windows だとうまくいかない?

###Remove api permissions: disable default exposed scope first
default_scope=$(az ad app show --id $clientid | jq '.oauth2Permissions[0].isEnabled = false' | jq -r '.oauth2Permissions')
az ad app update --id $clientid --set oauth2Permissions="$default_scope"
az ad app update --id $clientid --set oauth2Permissions="[]"

docs.microsoft.com

こちらのサイトを参考にしました。

damienbod.com

これに関しては Azure AD モジュールを使った方がよいのかも。

www.techwatching.dev

Azure AD でデバイスコードフローを試す

Azure AD にアプリを登録してパブリッククライアントフローを有効化する。

# Azure CLI 
az login

# Create Application
$app = az ad app create --display-name "Device Code Flow App"

# Get Application ID
$appId = ($app | ConvertFrom-Json).appId

# Create Service Principal
az ad sp create --id $appId

# Enable Public Client Flow
az ad app update --id $appId --set publicClient=true

バイスコードフローでアクセストークンを取得する。

# Get Tenant ID
$tenantId = (az account show | ConvertFrom-Json ).tenantId

# Create HTTP Body for Getting Device Code
$body = @{
    client_id = $appId
    scope = "User.Read"
}

# Request Device Code
$response = Invoke-RestMethod -Method Post -Uri https://login.microsoftonline.com/$tenantId/oauth2/v2.0/devicecode -Body $body -ContentType "application/x-www-form-urlencoded"

# Copy User Code to Clipboard
$response.user_code | clip.exe

# Open Verification Url in Browser, Paste User Code and Authenticate
Start-Process $response.verification_uri

# Create HTTP Body for Getting Access Token
$body = @{
    grant_type = "urn:ietf:params:oauth:grant-type:device_code"
    client_id = $appId
    device_code = $response.device_code
}

# Request Access Token
$response = Invoke-RestMethod -Method Post -Uri https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token -Body $body -ContentType "application/x-www-form-urlencoded"

# Get Access Token 
$accessToken = $response.access_token

アクセストークンを使って Microsoft Graph API を呼び出す。

$headers = @{"Authorization" = "Bearer $accessToken"}
Invoke-RestMethod -Uri https://graph.microsoft.com/v1.0/me -Headers $headers

docs.microsoft.com

クライアントシークレットを環境変数から取得する

まず環境変数にクライアントシークレットを登録する。(値はダミー)

$clinetSecret = "bXw7Q~Pn0fv8NHXBZdWzgkRm2gzFd-.fsZx~O"
[System.Environment]::SetEnvironmentVariable("ClientSecret", $clinetSecret, "User")

登録後 PowerShell を再起動しないと環境変数を読み取れないので注意。

呼出し側。

$config = Get-Content -Path $PSScriptRoot\appsettings.json | ConvertFrom-Json

$applicationId = $config.applicationId
$directoryId = $config.directoryId

$scope = "https://graph.microsoft.com/.default"

$clientSecret = $env:ClientSecret # 環境変数から取得

$response = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$directoryId/oauth2/v2.0/token" -Method Post -Body @{client_id=$applicationId; scope=$scope; client_secret=$clientSecret; grant_type="client_credentials"}
$accessToken = $response.access_token

$headers = @{"Authorization" = "Bearer " + $accessToken}
$users = Invoke-WebRequest -Uri https://graph.microsoft.com/v1.0/users -Headers $headers | ConvertFrom-Json

$users.value | Select-Object UserPrincipalName

[PowerShell] appsettings.json から情報を取り出す

appsettings.json(値はダミー)

{
    "applicationId": "56c0ac2c-c858-4781-ad48-749312fb2fdd",
    "clientSecret": "bXw7Q~Pn0fv8NHXBZdWzgkRm2gzFd-.fsZx~O",
    "directoryId": "a9fc19ab-08cf-44a9-b2dc-1fd0542d6b4f"
}

呼出し側。appsettings.json と同じ階層に。

$config = Get-Content -Path $PSScriptRoot\appsettings.json | ConvertFrom-Json

$applicationId = $config.applicationId
$clientSecret = $config.clientSecret
$directoryId = $config.directoryId

$scope = "https://graph.microsoft.com/.default"

$response = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$directoryId/oauth2/v2.0/token" -Method Post -Body @{client_id=$applicationId; scope=$scope; client_secret=$clientSecret; grant_type="client_credentials"}
$accessToken = $response.access_token

$headers = @{"Authorization" = "Bearer " + $accessToken}
$users = Invoke-WebRequest -Uri https://graph.microsoft.com/v1.0/users -Headers $headers | ConvertFrom-Json

$users.value | Select-Object UserPrincipalName

PowerShell モジュールのバージョン管理

psm1 ファイルではバージョンを指定する方法がないのでインポートするとバージョンが 0.0 になります。

> Import-Module .\Modules\Greeting\Greeting.psm1
> Get-Module Greeting

ModuleType Version Name
---------- ------- ----
Script     0.0     Greeting

バージョンを管理するためにはマニフェストファイル(psd1)を用意する必要があります。マニフェストファイルのテンプレートは New-ModuleManifest で作成することができます。psd1 は psm1 と同じ階層に出力します。

New-ModuleManifest -Path .\Modules\Greeting\Greeting.psd1 -Author tamtamyarn -RootModule Greeting.psm1 -ModuleVersion 1.0.0

出力されたテンプレートは以下のようになります。

#
# モジュール 'Greeting' のモジュール マニフェスト
#
# 生成者: tamtamyarn
#
# 生成日: 2021/12/19
#

@{

# このマニフェストに関連付けられているスクリプト モジュール ファイルまたはバイナリ モジュール ファイル。
RootModule = 'Greeting.psm1'

# このモジュールのバージョン番号です。
ModuleVersion = '1.0.0'

# サポートされている PSEditions
# CompatiblePSEditions = @()

# このモジュールを一意に識別するために使用される ID
GUID = '586ac6fe-8f39-465c-bb0d-0bec9cac6ca7'

# このモジュールの作成者
Author = 'tamtamyarn'

# このモジュールの会社またはベンダー
CompanyName = '不明'

# このモジュールの著作権情報
Copyright = '(c) 2021 tamtamyarn. All rights reserved.'

# このモジュールの機能の説明
# Description = ''

# このモジュールに必要な Windows PowerShell エンジンの最小バージョン
# PowerShellVersion = ''

# このモジュールに必要な Windows PowerShell ホストの名前
# PowerShellHostName = ''

# このモジュールに必要な Windows PowerShell ホストの最小バージョン
# PowerShellHostVersion = ''

# このモジュールに必要な Microsoft .NET Framework の最小バージョン。 この前提条件は、PowerShell Desktop エディションについてのみ有効です。
# DotNetFrameworkVersion = ''

# このモジュールに必要な共通言語ランタイム (CLR) の最小バージョン。 この前提条件は、PowerShell Desktop エディションについてのみ有効です。
# CLRVersion = ''

# このモジュールに必要なプロセッサ アーキテクチャ (なし、X86、Amd64)
# ProcessorArchitecture = ''

# このモジュールをインポートする前にグローバル環境にインポートされている必要があるモジュール
# RequiredModules = @()

# このモジュールをインポートする前に読み込まれている必要があるアセンブリ
# RequiredAssemblies = @()

# このモジュールをインポートする前に呼び出し元の環境で実行されるスクリプト ファイル (.ps1)。
# ScriptsToProcess = @()

# このモジュールをインポートするときに読み込まれる型ファイル (.ps1xml)
# TypesToProcess = @()

# このモジュールをインポートするときに読み込まれる書式ファイル (.ps1xml)
# FormatsToProcess = @()

# RootModule/ModuleToProcess に指定されているモジュールの入れ子になったモジュールとしてインポートするモジュール
# NestedModules = @()

# このモジュールからエクスポートする関数です。最適なパフォーマンスを得るには、ワイルドカードを使用せず、エクスポートする関数がない場合は、エントリを削除しないで空の配列を使用してください。
FunctionsToExport = '*'

# このモジュールからエクスポートするコマンドレットです。最適なパフォーマンスを得るには、ワイルドカードを使用せず、エクスポートするコマンドレットがない場合は、エントリを削除しないで空の配列を使用してください。
CmdletsToExport = '*'

# このモジュールからエクスポートする変数
VariablesToExport = '*'

# このモジュールからエクスポートするエイリアスです。最適なパフォーマンスを得るには、ワイルドカードを使用せず、エクスポートするエイリアスがない場合は、エントリを削除しないで空の配列を使用してください。
AliasesToExport = '*'

# このモジュールからエクスポートする DSC リソース
# DscResourcesToExport = @()

# このモジュールに同梱されているすべてのモジュールのリスト
# ModuleList = @()

# このモジュールに同梱されているすべてのファイルのリスト
# FileList = @()

# RootModule/ModuleToProcess に指定されているモジュールに渡すプライベート データ。これには、PowerShell で使用される追加のモジュール メタデータを含む PSData ハッシュテーブルが含まれる場合もあります。
PrivateData = @{

    PSData = @{

        # このモジュールに適用されているタグ。オンライン ギャラリーでモジュールを検出する際に役立ちます。
        # Tags = @()

        # このモジュールのライセンスの URL。
        # LicenseUri = ''

        # このプロジェクトのメイン Web サイトの URL。
        # ProjectUri = ''

        # このモジュールを表すアイコンの URL。
        # IconUri = ''

        # このモジュールの ReleaseNotes
        # ReleaseNotes = ''

    } # PSData ハッシュテーブル終了

} # PrivateData ハッシュテーブル終了

# このモジュールの HelpInfo URI
# HelpInfoURI = ''

# このモジュールからエクスポートされたコマンドの既定のプレフィックス。既定のプレフィックスをオーバーライドする場合は、Import-Module -Prefix を使用します。
# DefaultCommandPrefix = ''

}

項目が多くてごちゃごちゃしているので最低限の要素以外は削除します。

@{

# このマニフェストに関連付けられているスクリプト モジュール ファイルまたはバイナリ モジュール ファイル。
RootModule = 'Greeting.psm1'

# このモジュールのバージョン番号です。
ModuleVersion = '1.0.0'

# このモジュールを一意に識別するために使用される ID
GUID = '586ac6fe-8f39-465c-bb0d-0bec9cac6ca7'

# このモジュールからエクスポートする関数です。最適なパフォーマンスを得るには、ワイルドカードを使用せず、エクスポートする関数がない場合は、エントリを削除しないで空の配列を使用してください。
FunctionsToExport = '*'

# このモジュールからエクスポートするコマンドレットです。最適なパフォーマンスを得るには、ワイルドカードを使用せず、エクスポートするコマンドレットがない場合は、エントリを削除しないで空の配列を使用してください。
CmdletsToExport = '*'

# このモジュールからエクスポートする変数
VariablesToExport = '*'

# このモジュールからエクスポートするエイリアスです。最適なパフォーマンスを得るには、ワイルドカードを使用せず、エクスポートするエイリアスがない場合は、エントリを削除しないで空の配列を使用してください。
AliasesToExport = '*'

}

これでモジュールをインポートすると指定したバージョンで読み込まれます。

> Import-Module .\Modules\Greeting
> Get-Module Greeting

ModuleType Version Name
---------- ------- ----
Script     1.0.0   Greeting

Import-Module 実行時にバージョン指定することで特定のバージョンのモジュールをインポートすることができます。まずバージョンごとにフォルダを分けます。

│   Main.ps1
│
└───Modules
    └───Greeting
        ├───1.0.0
        │       Greeting.psd1
        │       Greeting.psm1
        │
        └───2.0.0
                Greeting.psd1
                Greeting.psm1

psd1 ファイル内の ModuleVersion をフォルダー名のバージョンと合わせておきます。RequiredVersion で指定したバージョンを読み込むことができます。指定しない場合は最新バージョンが読み込まれます。

> Remove-Module Greeting
> Import-Module .\Modules\Greeting -RequiredVersion 1.0.0
> Get-Module Greeting

ModuleType Version Name
---------- ------- ----
Script     1.0.0   Greeting

> Remove-Module Greeting
> Import-Module .\Modules\Greeting -RequiredVersion 2.0.0
> Get-Module Greeting

ModuleType Version Name
---------- ------- ----
Script     2.0.0   Greeting

> Remove-Module Greeting
> Import-Module .\Modules\Greeting
> Get-Module Greeting

ModuleType Version Name
---------- ------- ----
Script     2.0.0   Greeting

PowerShell スクリプトモジュールを試す

PowerShell でモジュールを作ったことがなかったので試してみました。PowerShell のモジュールとしては C# で書くバイナリモジュールとPowerShell で書くスクリプトモジュールがありますが、今回の対象はスクリプトモジュールです。

とりあえずこんな感じで Main.ps1Greeting.psm1 を配置しました。

ModuleSample
    │   Main.ps1
    │
    └───Modules
        └───Greeting
                Greeting.psm1

Greeting.psm1スクリプトモジュールの本体です。とりあえず 1 個関数を書いておきます。

function Get-Hello {
    Write-Output "Hello World!"
}

呼出し側 Main.ps1 では Import-Module でモジュールを読み込みます。

Import-Module .\Modules\Greeting

Get-Hello

実行結果です。

> .\Main.ps1
Hello World!

Import-Module の代わりに using module を使ってモジュールを読み込むこともできます。

using module .\Modules\Greeting

Get-Hello

フォルダーを指定してモジュールを読み込む場合はフォルダー名(Greeting)とファイル名(Greeting.psm1)が同じである必要があります。フォルダー名と異なる場合でもファイル名を直接指定して読み込むことはできます。

using module .\Modules\Greeting\Greeting.psm1

Get-Hello

Export-ModuleMember を使うことで指定した関数だけをエクスポートすることができます。

function Get-Hello {
    Write-Output "Hello World!"
}

function Get-GoodMorning {
    Write-Output "Good Morning World!"
}

function  Get-GoodAfternoon {
    Write-Output "Good Afternoon World!"
}

function Get-GoodEvening {
    Write-Output "Good Evening World!"
}

function Get-GoodNight {
    Write-Output "Good Night World!"
}

$functionsToExport = @(
    "Get-Hello"
    "Get-GoodMorning"
    "Get-GoodAfternoon"
    "Get-GoodEvening"
)

Export-ModuleMember -Function $functionsToExport

Main.ps1 を以下のように書き換えておきます。

using module .\Modules\Greeting

Get-Hello
Get-GoodMorning
Get-GoodAfternoon
Get-GoodEvening
Get-GoodNight

モジュールを読み込みなおすために Remove-Module で読み込んだモジュールをいったん削除してから実行すると、エクスポートしなかった関数だけ実行に失敗します。

> Remove-Module Greeting
> .\Main.ps1
Hello World!
Good Morning World!
Good Afternoon World!
Good Evening World!
Get-GoodNight : 用語 'Get-GoodNight' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してください。

間違った挨拶をしないように、時間に応じた挨拶を返す Get-Greeting を定義して他の関数は非公開にしてみました。

function Get-Hello {
    Write-Output "Hello World!"
}

function Get-GoodMorning {
    Write-Output "Good Morning World!"
}

function  Get-GoodAfternoon {
    Write-Output "Good Afternoon World!"
}

function Get-GoodEvening {
    Write-Output "Good Evening World!"
}

function Get-GoodNight {
    Write-Output "Good Night World!"
}

function Get-Greeting {
    param (
        [DateTime]$DateTime = (Get-Date)
    )

    if ($DateTime.Hour -lt 5) {
        Get-GoodNight
        return
    }

    if ($DateTime.Hour -lt 12) {
        Get-GoodMorning
        return
    }

    if ($DateTime.Hour -lt 18) {
        Get-GoodAfternoon
        return
    }

    if ($DateTime.Hour -lt 24) {
        Get-GoodEvening
        return
    }
}

$functionsToExport = @(
    "Get-Hello"
    "Get-Greeting"
)

Export-ModuleMember -Function $functionsToExport