【IBM Security Verify】棚卸用にISVの全ユーザー情報をCSV出力する。

# 1. はじめに
　IBM Security Verifyは、IBMが提供しているIDaaS製品です。OktaやAzure ADと同様に、IDaaS製品として豊富な機能とAPIが用意されています。
　IBM Security Verifyでは、CSVでユーザー情報一覧を一括登録するAPIはありますが、**CSVで全ユーザーの属性情報一覧をダウンロードするAPIは提供されていません。**（2023/5/20現在）
　そこで、IBM Security Verifyの全ユーザー情報を取得し、CSVで出力するPowerShellスクリプトを紹介します。

# 2. 実装前に2つの注意点
　IBM Security Verifyの既存APIである、ユーザーリスト取得APIを活用します。[APIリファレンスはこちら](https://docs.verify.ibm.com/verify/reference/getusers)
このAPIを使い、IBM Security Verifyのユーザー情報を全件取得するにあたり、以下2点の注意があります。

### 2.1. 全件取得するためのアルゴリズム
　ユーザーリスト取得APIは1回の実行で取得できるユーザー件数が**2500件**までという制限があります。そのため、2500件以上のユーザー数を出力させるためには、API実行を繰り返す必要がありますが、そのアルゴリズムが特殊です。[詳細はこちら](https://docs.verify.ibm.com/verify/docs/looking-up-users)

### 2.2. カスタム属性の取得
　ISVのデフォルトの属性だけでなく、カスタム属性として追加した属性をCSVエクスポートに含めたいケースも多いと思います。カスタム属性は、ユーザー情報取得APIの返り値のjsonの中で、以下のようにデータが格納されています。
```user.json
                "customAttributes": [
                    {
                        "values": [
                            "カスタム属性の値1"
                        ],
                        "name": "カスタム属性名1"
                    },
                    {
                        "values": [
                            "カスタム属性の値2"
                        ],
                        "name": "カスタム属性名2"
                    },
                    {
                        "values": [
                            "カスタム属性の値3"
                        ],
                        "name": "カスタム属性名3"
                    },
                ],
```
　カスタム属性の値が入っていないユーザーは、jsonから該当のカスタム属性の項目ごと省略されます。そのため、全ユーザー分のカスタム属性を取得するために、少々アルゴリズムを工夫することがあります。

# 3. ISVユーザー情報をCSV出力するPowerShellスクリプト
　上の注意点を考慮して作成したスクリプトが以下になります。
実行する際は、**ISVテナント名**、**ISVクライアントID**、**ISVクライアントシークレット**の3つの変数に値を設定してください。
また、customAttribute1,customAttribute2,customAttribute3の箇所に、ISVに設定したカスタム属性の名称をセットし、必要に応じてcustomAttributeの数を増減させてください。
※当記事は、スクリプトの動作を保証するものはありません。

```PowerShell:isv_csv_export.ps1
#
# ※※CSVに出力されるユーザー情報※※
# 1. preferred_username:ログインID
# 2. family_name:姓
# 3. given_name:名
# 4. email:メールアドレス
# 5. カスタム属性1 
# 6. カスタム属性2
# 7. カスタム属性3
# 8. activate:ユーザー状態(True:有効、False:無効)
# 9. id:ISV内部ID
# 10.created:ユーザー作成日時（ISVにユーザーが登録された日時）
# 11.lastLogin:最終ログイン日時（ISVに最後にログインした日時）
# 12.lastPwdChange:最終パスワード変更日時（ISVで最後にパスワード変更した日時）
#   

# ISVテナント名
$tenant_id = "XXXXX.verify.ibm.com"
# ISVクライアントID
$client_id = "XXXXX"
# ISVクライアントシークレット
$client_secret = "XXXXX"

$block = "2022-01-01T00:00:00Z"

#################################################
# アクセストークン取得関数
function getAccessToken{
    $headers=@{}
    $headers.Add("accept", "application/json")
    $headers.Add("content-type", "application/x-www-form-urlencoded")
    $get_url = 'https://' + $tenant_id + '/oidc/endpoint/default/token'
    $body = 'grant_type=client_credentials&client_id=' + $client_id + `
        '&client_secret=' + $client_secret

    try {
        $response = Invoke-WebRequest -Uri $get_url -Method POST -Headers $headers -ContentType 'application/x-www-form-urlencoded' -Body $body
    }catch{
        Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ 
        Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
        exit
    }
    $response_json = ConvertFrom-Json $response
    return $response_json.access_token
}


#################################################
# URLエンコーディング関数
function UrlEncode {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Text
    )
    $UrlEncodedText = [System.Web.HttpUtility]::UrlEncode($Text)
    Write-Output $UrlEncodedText
}


# アクセストークン取得
$access_token = getAccessToken
$masterlist = @()

$flag = $true

while ($flag) {
    # ユーザリスト取得APIのパラメータ
    $headers=@{}
    $headers.Add("accept", "application/scim+json")
    $headers.Add("Authorization", "Bearer " + $access_token)
    $encodedblock = UrlEncode -Text ('"'+$block+'"')
    $get_url = 'https://' + $tenant_id + '/v2.0/Users?filter=meta.created%20ge%20' + $encodedblock + '&sortBy=meta.created&sortOrder=ascending&count=2000'

    # ユーザリスト取得API実行
    try {
        $response = Invoke-WebRequest -Uri $get_url -Method GET -Headers $headers
    }catch{
        Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
        Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
        exit
    }
    # PowerShellのInvoke-WebRequestでISVデータを取得すると、日本語文字列が???になっていたのでUtf8エンコード
    $response_utf8 = [System.Text.Encoding]::Utf8.GetString($response.RawContentStream.GetBuffer())
    # その際、後ろにNUL(文字コード0000）が埋め込まれてjson変換できなかったので、NUL字を削除
    $res = $response_utf8 -replace "\u0000",""
    # json構造に変換
    $output = ConvertFrom-Json $res

    if ($output.totalResults -lt 2000) {
        $output = $output.Resources
        $masterlist += $output
        Write-Output $masterlist.Length
        $flag = $false
    }
    else {
        $output = $output.Resources
        # 配列の最後の2つのmeta.created が等しい場合は、最後の配列を削除
        while ($output[-1].meta.created -eq $output[-2].meta.created) {
            $output = $output[0..($output.Length - 2)]
        }

        # 末尾の配列のmeta.createdを変数に指定して削除
        $block = $output[-1].meta.created
        $output = $output[0..($output.Length - 2)]

        Write-Output $block
        $masterlist += $output
        Write-Output $masterlist.Length
    }
}

# ディレクトリパスを取得し、YYYYMMDD.csvファイルを生成
$scriptPath = $MyInvocation.MyCommand.Path
$scriptDirectory = Split-Path $scriptPath -Parent
$now = Get-Date
$strYM = $now.ToString("yyyyMMdd")
$csvPath = "$scriptDirectory\$strYM.csv"

# CSVファイルにヘッダ行を書き込む。
$header = "preferred_username,family_name,given_name,email,customAttribute1,customAttribute2,customAttribute3,activate,id,created,lastLogin,lastPwdChange"
$header | Out-File -FilePath $csvPath -Encoding UTF8 -Append

# ユーザー情報リストをCSVに出力
foreach ($i in $masterlist) {
    
    # カスタム属性の初期値 (booleanカスタム属性の場合はfalseをセットする)
    $customAttribute1 = ""
    $customAttribute2 = ""
    $customAttribute3 = ""
    #(カスタム属性を追加する場合は、ここに追加)

    # カスタム属性の属性名を一覧で取得しリスト化
    $custom_names = $i."urn:ietf:params:scim:schemas:extension:ibm:2.0:User".customAttributes.name
    $custom_names_list = $custom_names -split " "
    # カスタム属性の値を一覧で取得しリスト化
    $custom_values = $i."urn:ietf:params:scim:schemas:extension:ibm:2.0:User".customAttributes.values
    $custom_values_list = $custom_values -split " "

    # カスタム属性リストに属性名が存在する場合、それに対応する値をセットする
    if ($custom_names_list.Count -gt 0) {
        for ($k=0; $k -lt $custom_names_list.Count; $k++) {
            switch($custom_names_list[$k]){
                "customAttribute1" {$customAttribute1 = $custom_values_list[$k]; break}
                "customAttribute2" {$customAttribute2 = $custom_values_list[$k]; break}
                "customAttribute3" {$customAttribute3 = $custom_values_list[$k]; break}
                #(カスタム属性を追加する場合は、ここに追加)
            }
        }
    }

    # CSVにユーザー情報を出力
    $row = $i.userName + "," + $i.name.familyName + "," + $i.name.givenName + "," + $i.emails.value + "," + $customAttribute1 + ","  + $customAttribute2 + ","  + $customAttribute3 + "," + $i.active + "," + $i.id + "," + $i.meta.created + "," + $i."urn:ietf:params:scim:schemas:extension:ibm:2.0:User".lastLogin +  "," + $i."urn:ietf:params:scim:schemas:extension:ibm:2.0:User".pwdChangedTime
    $row | Out-File -FilePath $csvPath -Encoding UTF8 -Append
}

exit
```

# 4. 参考
本記事を書くにあたり、[@fitz](https://qiita.com/fitz) さんの以下qiita記事を参考にしました。
[IBM Security Verify API + Pythonでユーザー情報一覧を取得する](https://qiita.com/fitz/items/60a2d45d2d7f9aaadc01)

IBM公式ドキュメント
https://docs.verify.ibm.com/verify/reference/getusers
https://docs.verify.ibm.com/verify/docs/looking-up-users