コルネの進捗や備忘録が記されたなにか

進捗や成果物や備忘録てきななにかを雑に更新していきます。

Microsoft 365 ユーザーアカウントの作成を自動化する


スポンサードリンク

はじめに

今回は、Microsoft 365 ユーザーアカウントの作成の自動化方法を纏めたいと思います。

ケースとしては、新入社員や中途社員が増えた時や派遣社員の方やBPの方がプロジェクトに参入したときなどにMicrosoft 365 ユーザーアカウントを発行しなくてはならない。
などがあるかと思います。

よって今回のシナリオは以下となるように作成します。

  1. ユーザーの作成を依頼
  2. その依頼を担当者が承認
  3. Microfot 365 ユーザーアカウントを作成
  4. 依頼を行った相手に作成したアカウント情報を払い出し

を想定して作成します。

ユーザー作成依頼フォームを作成する

ユーザー作成を依頼するためのフォームの作成方法として、今回のケースで最も単純で簡単なものは Microsoft Forms かと思います。
ただMicrosoft Forms ですと、ユーザーが入力項目を動的に増やすということを行うことができません。
なので今回はユーザー作成依頼フォームをPower Apps で作成してみようと思います。

* この記事では私がPower Apps が好きなので入力フォームをPower Apps で作成することにしていますが、もちろんMicrosoft Forms でも十分運用可能なレベルかと思います。
  実際に運用される際は必要な機能や作成にかかる労力などから費用対効果を十分に考えたうえで作成することをお勧めします。

Power Apps でユーザー作成フォームを作成する

作成する入力フォームは以下のようなイメージです。

f:id:koruneko:20210131151838p:plain

+ ボタンを押すと以下のように入力フォームが増えて、ごみ箱のアイコンを押すと対象の入力フォームが消えます。
Request ボタンは全ての入力項目が入力されていないと押せない仕様にしています。

f:id:koruneko:20210131152036p:plain

今回このフォームの作成はメインでないため簡単に説明します。

まず入力フォームとなるギャラリーですが、以下の要素で構成されています。

f:id:koruneko:20210131152208p:plain

このコレクションに設定する ItemsApp.OnStart で定義しています。

App.OnStart

ClearCollect(
    userRequest,
    {
        ID:1,
        DisplayName:"",
        UserPrincipalName:"",
        FirstName:"",
        LastName:""
    }
)

ギャラリー内に設置されたテキストボックスは、コレクション userRequest と同期させたいです。
なのでテキストボックスの値が変更されたら都度それぞれ対応する項目を更新するようにします。

TextInput.OnChange

UpdateIf(userRequest, ID = ThisItem.ID, {LastName:Self.Text})

続いて削除アイコンです。
これは

  • 対象の項目のみを削除
  • 入力項目が1つしかない場合は非表示

としたいです。
なので以下のように設定します。

DeleteIcon.OnSelect

Remove(userRequest, ThisItem)

DeleteIcon.Visible

CountRows(userRequest) > 1

これでギャラリー内の要素の作成は完了です。

続いて入力項目を増やすためのボタンの作成です。
これはコレクションを増やすだけでよいので、以下のように設定します。

AddIcon.OnSelect

Collect(
    userRequest,
    {
        ID:Last(userRequest).ID + 1,
        DisplayName:"",
        UserPrincipalName:"",
        FirstName:"",
        LastName:""
    }
)

最後にリクエストボタンを作成します。
これには以下機能を実施します。

  • 入力項目が全て入力されていないときは非活性
  • ボタンが選択されると申請を行うフローを起動して、完了画面へ遷移

「入力項目が全て入力されていないときは非活性」というのは以下で実現できます。

RequestButton.DisplayMode

If(
    CountIf(
        ForAll(
            userRequest,
            IsBlank(DisplayName) || IsBlank(UserPrincipalName) || IsBlank(FirstName) || IsBlank(LastName)
        ),
        Value = true
     ) = 0,
     DisplayMode.Edit,
     DisplayMode.Disabled
)

「ボタンが選択されると申請を行うフローを起動して、完了画面へ遷移」ですが、フローの作成は後の章で行うのでここでは説明を一旦割愛します。
完了画面への遷移の作成は簡単です。

まず完了画面の作成ですが、これはテンプレートを用いましょう。

f:id:koruneko:20210131153606p:plain

「新しい画面」 > 「成功」を選択します。
これだけでそれっぽい画面の完成です。

f:id:koruneko:20210131153702p:plain

あとはボタンが選択されたときの動作にこの画面への遷移を定義するだけでよいので、

RequestBotton.OnSelect

Navigate(Screen2)

以上で入力フォームの作成は完了です。

ユーザー作成処理を作成する

Azure Automation

ここは以前紹介した、【Microsoft Teams】申請されたユーザーを指定のTeams のチームに自動で追加する と一部説明が被りますが再度紹介します。

Azure Automation のデプロイ

  1. Azure Portal にログインを行い、「リソースの作成」を選択します。
    f:id:koruneko:20200719174315p:plain

  2. 「オートメーション」を作成します。
    f:id:koruneko:20200719174423p:plain

  3. 必要項目を入力します。
    f:id:koruneko:20200719174647p:plain

    • 名前:任意の名前を入力します。
    • サブスクリプション:任意のサブスクリプションを選択します。
    • リソースグループ:任意のリソースグループを選択、もしくは新規作成します。
    • 場所:任意の場所を選択します。今回は、「東日本」を選択します。
    • Azure 実行アカウントの作成:「はい」を選択します。(詳細はこちら
  4. 入力が完了したら、「作成」を選択し、デプロイを行います。

  5. デプロイが無事完了しました。
    f:id:koruneko:20200719175301p:plain

モジュールを追加する

  1. 「共有リソース」より、「モジュール」を選択します。
    f:id:koruneko:20210204233452p:plain

  2. ギャラリーを参照を選択します。
    f:id:koruneko:20200719175739p:plain

  3. 検索窓より「AzureAD」と検索して、「AzureAD」を選択します。 f:id:koruneko:20210205005137p:plain

  4. インポートを行います。 f:id:koruneko:20210205005342p:plain f:id:koruneko:20210205005402p:plain

  5. モジュールが使用可能になりました。 f:id:koruneko:20210205013714p:plain

  6. 同様の手順で「MSOnline」も使用可能な状態にします。 f:id:koruneko:20210205015007p:plain

変数を利用する

共有リソースより、「変数」を選択します。

f:id:koruneko:20200722013829p:plain

「変数の追加」を選択して、変数の追加を行います。

f:id:koruneko:20200722014213p:plain

必要項目を入力します。
今回追加する変数は domain です。
自身のテナントのドメイン、"@xxx.co.jp"を設定します。

f:id:koruneko:20200722015132p:plain

  • 名前:任意の変数名を入力します。
  • 説明:変数の説明を記載します。
  • タイプ:変数の型をしていします。今回は「文字列」を指定します。
  • 値:変数の値を入力します。
  • 暗号化:暗号化を行うか選択します。

変数の設定が完了したら、PowerShell Runbook に戻ります。

先ほどのスクリプトに先ほど追加した変数の読み込みを追加します。

変数の読み込みは「Get-AutomationVariable -Name 'VariableName'」で行うことができます。

資格情報の追加

Microsoft 365 ユーザーの追加を行うことができるadmin のユーザー情報を記録します。

  1. 共有リソースより、「資格情報」を選択します。
    f:id:koruneko:20200719181415p:plain

  2. 「資格情報の追加」を選択します。
    f:id:koruneko:20200719181555p:plain

  3. 必要項目を入力します。
    f:id:koruneko:20200719181810p:plain

    • 名前:任意の名前を入力します。
    • 説明:後から確認したときにわかりやすいよう、対象の資格情報の説明を入力します。
    • ユーザー名:ユーザー名(上記説明の権限のあるユーザー)を入力します。
    • パスワード:パスワード(上記説明の権限のあるユーザーのパスワード)を入力します。
    • パスワードの確認入力:先ほど入力したパスワードをもう一度入力します。
  4. 「作成」を選択して完了です。

PowerShell スクリプトの登録

  1. プロセス オートメーションより「Runbook」を選択します。
    f:id:koruneko:20200719222338p:plain

  2. 「Runbook の作成」を選択します。
    f:id:koruneko:20200719222519p:plain

  3. 必要項目を入力します。
    f:id:koruneko:20200719222800p:plain

    • 名前:任意の名前を入力します。
    • Runbook の種類:「PowerShell」を選択します。
    • 説明:後から確認したときにわかりやすいよう、対象のRunbook の説明を入力します。
  4. 作成が完了するとPowerShell の編集画面が表示されます。
    f:id:koruneko:20200719223309p:plain

  5. Runbook には以下のように記載します。

Param
(
    [Parameter (Mandatory= $true)]
    $userRequest
)

# 変数の読込
$domain = Get-AutomationVariable -Name 'domain'

# 資格情報を読込
$myCredential = Get-AutomationPSCredential -Name 'Microsoft 365 admin Login'

# Microsoft 365 サブスクリプション用の Azure AD に接続する
Connect-MsolService -Credential $myCredential

# ユーザーの作成
$userRequest | ForEach-Object{
    $DisplayName = $_.DisplayName
    $FirstName = $_.FirstName
    $LastName = $_.LastName
    $UserPrincipalName = $_.UserPrincipalName + $domain

    New-MsolUser -DisplayName $DisplayName `
        -FirstName $FirstName `
        -LastName $LastName `
        -UserPrincipalName $UserPrincipalName `
        -UsageLocation JP `
        -LicenseAssignment xxx:O365_BUSINESS
}

送られてくる値はObject なので ForEach-Object でループ処理を行っています。

ユーザーの作成は、New-MsolUser で行っています。

使用可能なライセンス( -LicenseAssignment )は、Get-MsolAccountSku で確認を行ってください。

モジュールのインストールを行っていない場合は、PowerShell を使用して Microsoft 365 に接続する を参考にモジュールのインストールを行ってください。

なお、エラー処理は今回特に行っていないので、各自適宜行うようにしてください。

  1. テストを行う
    スクリプトの作成が完了したら、一度、テストを行っておきましょう。
    「テスト ウィンドウ」を選択して、パラメーターを入力後「開始」を選択します。

f:id:koruneko:20210208011752p:plain

f:id:koruneko:20210208011936p:plain

  1. 公開を行う
    作成が完了したら、「公開」を行ってください。
    これを行わないと、変更が反映されませんので、注意です。

f:id:koruneko:20210208012037p:plain

Power Automate

最後にPower Apps で入力された値を先ほど作成したPowerShell Runbook に渡すようにします。
また、このPower Automate ないで"承認"処理を行うようにします。

  1. Power Apps から呼び出されるので、トリガーには Power Apps を利用します。

f:id:koruneko:20210208012902p:plain

  1. Power Apps からはコレクションの内容をJSON で渡すようにするので、"JSON の解析"で内容を取得します。

f:id:koruneko:20210208013150p:plain

ちなみにスキーマには以下のように入力します。

{
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "DisplayName": {
                "type": "string"
            },
            "FirstName": {
                "type": "string"
            },
            "ID": {
                "type": "integer"
            },
            "LastName": {
                "type": "string"
            },
            "UserPrincipalName": {
                "type": "string"
            }
        }
    }
}
  1. 承認者に申請内容が分かりやすいように申請内容を表示させたいと思います。
    以下のように申請内容の文字列を作成します。

f:id:koruneko:20210208013458p:plain

"値"は以下のように設定しています。

表示名:@{items('Apply_to_each')?['DisplayName']}@{uriComponentToString('%0A')}
姓:@{items('Apply_to_each')?['FirstName']}@{uriComponentToString('%0A')}
名:@{items('Apply_to_each')?['LastName']}@{uriComponentToString('%0A')}
アカウント名:@{items('Apply_to_each')?['UserPrincipalName']}@{uriComponentToString('%0A')}
@{uriComponentToString('%0A')}

uriComponentToString('%0A') は改行を表しています。

  1. 承認を作成します。
    詳細には先ほど作成した文字列を設定します。

f:id:koruneko:20210208013804p:plain

なおこの詳細メッセージはOutlook やPower Automate で表示したときは改行されて表示されますが、Teams で表示したときは改行して表示されないため注意が必要です。

  1. 承認の結果によって処理を分けたいと思います。

f:id:koruneko:20210208014118p:plain

承認された場合は、PowerShell Runbook を呼び出します。
却下された場合は、Teams で申請者にその旨を知らせます。

  1. 承認された場合はPowerShell Runbook を実行したいので"ジョブの作成"を作成します。

f:id:koruneko:20210208014404p:plain

  • サブスクリプション:Azure Automation 作成時に使用したサブスクリプションと同様のものを選択
  • リソースグループ:Azure Automation 作成時に使用したサブスクリプションと同様のものを選択
  • Automation アカウント:先ほど作成したAutomation アカウントを選択
  • Runbook 名:先ほど作成したPowerShell Runbook を選択
  • ジョブの待機:「はい」を選択
  • Runbook Parameter:作成したパラメーターがこちらに表示されます。今回は"JSONの解析_コンテンツ"を選択します。

なおこのコネクタはPremium コネクタのため注意してください。

  1. 却下された場合は申請者にTeams でメッセージを送りたいので"メッセージをフローボットとしてユーザーに投稿する"を作成します。

f:id:koruneko:20210208014849p:plain

わかりにくいですが、メッセージにはMarkdown が利用できるため、文の末尾にスペースを2つ入力しています。
これで改行になります。

〇応答コメント  
@{items('Apply_to_each_2')?['comments']}  

〇要求内容  
@{variables('request')}

Power Apps からPower Automate を呼び出す

最後にPower Apps からPower Automate を呼び出す処理を作成します。

"アクション"から"Power Automate"を選択して、先ほど作成したフローを追加します。

f:id:koruneko:20210208015413p:plain

Power Apps からPower Automate を呼び出すときには、

  • コレクションの内容をJSON にしたもの
  • 申請者のe-mail

を渡します。

Request ボタンの OnSelect に以下のように設定します。

RequestButton.OnSelect

Create_Microsoft365_User.Run(JSON(userRequest, JSONFormat.Compact), User().Email);
Navigate(Screen2)

以上でMicrosoft 365 ユーザー作成依頼を作成することができました!!

ここまで作成してみて、恐らく皆さんはPowerShell Runbook で受けた値の処理方法が適切ではない(エラーとなる)のでは?と思われているかと思います。
次の章で解説します。

Power Automate からPowerShell Runbook へJSON を送った場合の型について

Power Automate からPowerShell Runbook へJSON を送った場合、上記の場合、変数 $userRequest にはJSON が設定されるので、以下のように記載すべきかと思います。
(私も最初そう考えて処理を記載していましたが、 Invalid JSON エラーが表示されて詰まっていました...)

$userRequest | ConvertFrom-Json | ForEach-Object {
    $fields = $_
    
    $fields | ForEach-Object{
        $DisplayName = $_.DisplayName
        $FirstName = $_.FirstName
        $LastName = $_.LastName
        $UserPrincipalName = $_.UserPrincipalName + $domain

        New-MsolUser -DisplayName $DisplayName `
            -FirstName $FirstName `
            -LastName $LastName `
            -UserPrincipalName $UserPrincipalName `
            -UsageLocation JP `
            -LicenseAssignment xxx:O365_BUSINESS
    }
}

ただこの場合、上でも述べている通り、 ConvertFrom-JsonInvalid JSON エラーが表示されてしまいます。

Runbook の実行結果より、"入力"の値を確認したところ、値は確かにJSON で渡ってきており、念のためフォーマットも確認しましたが、正しい形式でした。

そこで、 echo などで実際に変数 $userRequest に設定されている値を確認した結果、どうやらオブジェクトとして変数には設定されているようでした。

なので上記のように設定しているというわけです。

何故オブジェクトになるのか???については現在調査中です。
もしわかる方や参考になりそうなdocs などありましたら教えていただけますと助かります。

---2021/02/23 追記---

作成されたユーザー情報を連携する

大事な処理を作成するのを忘れていました。。。
作成されたMicrosoft 365 ユーザーの情報を申請者へ連携しなくてはなりません。

ユーザー作成の結果はPowerShell Runbook の出力から確認することができます。

ただこの結果は確認していただければわかる通り、 System.Object[] 型で出力されています。

このままの結果をPower Automate に渡しても加工が難しいのでJSON に変換したいと思います。
JSON に変換するためには、 ConvertTo-Json を利用します。

Param
(
    [Parameter (Mandatory= $true)]
    $userRequest
)

$domain = Get-AutomationVariable -Name 'domain'

# 資格情報を読込
$myCredential = Get-AutomationPSCredential -Name 'Microsoft 365 admin Login'

# Microsoft 365 サブスクリプション用の Azure AD に接続する
Connect-MsolService -Credential $myCredential

# ユーザーの作成
$userRequest | ForEach-Object{
    $DisplayName = $_.DisplayName
    $FirstName = $_.FirstName
    $LastName = $_.LastName
    $UserPrincipalName = $_.UserPrincipalName + $domain

    New-MsolUser -DisplayName $DisplayName `
        -FirstName $FirstName `
        -LastName $LastName `
        -UserPrincipalName $UserPrincipalName `
        -UsageLocation JP `
        -LicenseAssignment korune:DEVELOPERPACK
} | ConvertTo-Json -Compress

これで結果がJSON で出力されるようになりました。

続いてPower Automate でのフローの編集です。

呼び出したPowerShell Runbook の出力結果を取得するには"ジョブの出力を取得します"というコネクタを利用する必要があります。

f:id:koruneko:20210223110131p:plain

その際、Runbook を呼び出した"ジョブの作成"コネクタの"ジョブの待機"は"はい"を選びましょう。

これでユーザーを作成した結果の情報をPower Automate でも扱えるようになったので、あとはデータを加工して必要な情報だけを申請者に共有するようにします。

まずはデータの加工です。
結果はJSON で送られてきているので、JSON の加工を行います。

f:id:koruneko:20210223111047p:plain

コンテンツ

@{body('ジョブの出力を取得します')?['body']}

スキーマ

{
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "Password": {
                "type": "string"
            },
            "ExtensionData": {
                "type": "object",
                "properties": {}
            },
            "AlternateEmailAddresses": {
                "type": "array"
            },
            "AlternateMobilePhones": {
                "type": "array"
            },
            "AlternativeSecurityIds": {},
            "BlockCredential": {
                "type": "boolean"
            },
            "City": {},
            "CloudExchangeRecipientDisplayType": {},
            "Country": {},
            "Department": {},
            "DirSyncProvisioningErrors": {},
            "DisplayName": {
                "type": "string"
            },
            "Errors": {},
            "Fax": {},
            "FirstName": {
                "type": "string"
            },
            "ImmutableId": {},
            "IndirectLicenseErrors": {},
            "IsBlackberryUser": {
                "type": "boolean"
            },
            "IsLicensed": {
                "type": "boolean"
            },
            "LastDirSyncTime": {},
            "LastName": {
                "type": "string"
            },
            "LastPasswordChangeTimestamp": {
                "type": "string"
            },
            "LicenseAssignmentDetails": {
                "type": "array",
                "items": {
                    "type": "string"
                }
            },
            "LicenseReconciliationNeeded": {
                "type": "boolean"
            },
            "Licenses": {
                "type": "array",
                "items": {
                    "type": "string"
                }
            },
            "LiveId": {
                "type": "string"
            },
            "MSExchRecipientTypeDetails": {},
            "MSRtcSipDeploymentLocator": {},
            "MSRtcSipPrimaryUserAddress": {},
            "MobilePhone": {},
            "ObjectId": {
                "type": "string"
            },
            "Office": {},
            "OverallProvisioningStatus": {
                "type": "integer"
            },
            "PasswordNeverExpires": {
                "type": "boolean"
            },
            "PasswordResetNotRequiredDuringActivate": {},
            "PhoneNumber": {},
            "PortalSettings": {},
            "PostalCode": {},
            "PreferredDataLocation": {},
            "PreferredLanguage": {},
            "ProxyAddresses": {},
            "ReleaseTrack": {},
            "ServiceInformation": {},
            "SignInName": {
                "type": "string"
            },
            "SoftDeletionTimestamp": {},
            "State": {},
            "StreetAddress": {},
            "StrongAuthenticationMethods": {
                "type": "array"
            },
            "StrongAuthenticationPhoneAppDetails": {
                "type": "array"
            },
            "StrongAuthenticationProofupTime": {},
            "StrongAuthenticationRequirements": {
                "type": "array"
            },
            "StrongAuthenticationUserDetails": {},
            "StrongPasswordRequired": {
                "type": "boolean"
            },
            "StsRefreshTokensValidFrom": {
                "type": "string"
            },
            "Title": {},
            "UsageLocation": {
                "type": "string"
            },
            "UserLandingPageIdentifierForO365Shell": {},
            "UserPrincipalName": {
                "type": "string"
            },
            "UserThemeIdentifierForO365Shell": {},
            "UserType": {
                "type": "integer"
            },
            "ValidationStatus": {
                "type": "integer"
            },
            "WhenCreated": {}
        }
    }
}

上記の結果を申請者 / 管理者に送るためのメッセージに加工します。

f:id:koruneko:20210223111303p:plain

@{uriComponentToString('%0A')}-----------------------------------------------@{uriComponentToString('%0A')}
DisplayName:@{items('Apply_to_each_3')?['DisplayName']}@{uriComponentToString('%0A')}
FirstName:@{items('Apply_to_each_3')?['FirstName']}@{uriComponentToString('%0A')}
LastName:@{items('Apply_to_each_3')?['LastName']}@{uriComponentToString('%0A')}
UserPrincipalName:@{items('Apply_to_each_3')?['UserPrincipalName']}@{uriComponentToString('%0A')}
Password:@{items('Apply_to_each_3')?['Password']}@{uriComponentToString('%0A')}

@{uriComponentToString('%0A')} は改行です。

あとはこのメッセージをTeams で共有してあげれば完了です!

f:id:koruneko:20210223111428p:plain

Power Automate からPowerShell Runbook への値渡しについて

【PowerShell Runbook】 入力パラメータの型検証にて検証結果を纏めてありますのでもしよければ参考にしてください。

shibatea さんコメントなどでのアドバイスありがとうございました!


スポンサードリンク