🧑‍💼

Microsoft Entra アクセス パッケージのカスタム拡張機能で質問の回答を使えるかを調査した件

2023/12/04に公開

本投稿は Microsoft Security Advent Calendar 2023 4日目に参加しております。
https://qiita.com/advent-calendar/2023/microsoft-security

やりたかったこと

Microsoft Entra と連携しているアプリに新規ゲスト ユーザーを申請ベースでプロビジョニングしたいが、連携アプリ側のアカウントの属性に、Entra ID で保持していない属性情報が必要なケースがある。
ユーザーからの申請情報や、別のデータベースにある情報から、連携アプリのアカウント属性にその属性を追加させることをしたい。

アクセス パッケージのカスタム拡張機能

今回検証したのは、Microsoft Entra Identity Governance のアクセス パッケージのカスタム拡張機能。

アクセス パッケージとは

Microsoft Entra で利用可能なアプリケーションへのアクセス権の割当や、グループ、チームへのメンバー登録をまとめて管理できるアクセス管理の機能です。こちらの機能を利用することで、新規ゲスト ユーザーから特定のアプリへのアクセス要求を受け取り、承認が得られればアクセス権を付与するということが可能になります。
詳細は日本マイクロソフトのサポート部署のブログ記事も参照ください。

カスタム拡張機能

アクセス パッケージに対する要求や割り当て期限が迫っている時など特定のイベントについて、事前に定義した Logic Apps を実行可能な機能です。
こちらを利用することで、申請に応じて、アプリ側が API などで操作できればアプリ独自の属性の設定も、プロビジョニングに合わせて実施できるはず。

今回調べたこと

カスタム拡張機能自体の利用方法は、日本マイクロソフトのサポート部署のブログ記事に乗っていますので、詳細はそちらを見ていただければと思います。
アクセス パッケージからカスタム拡張機能をキックした際に JSON 形式で申請情報が Logic Apps に連携されるのですが、その連携時の情報の詳細が分からなかったので詳細を調べました。

JSON フォーマット

Logic App への HTTP リクエストの JSON フォーマットは以下の形式。これはカスタム拡張機能で Logic Apps を構成すれば自動的に JSON スキーマが定義されていた。

{
    "properties": {
        "AccessPackage": {
            "properties": {
                "Description": {
                    "description": "AccessPackage-Description",
                    "type": "string"
                },
                "DisplayName": {
                    "description": "AccessPackage-DisplayName",
                    "type": "string"
                },
                "Id": {
                    "description": "AccessPackage-Id",
                    "type": "string"
                }
            },
            "type": "object"
        },
        "AccessPackageAssignmentRequestId": {
            "type": "string"
        },
        "AccessPackageCatalog": {
            "properties": {
                "Description": {
                    "description": "AccessPackageCatalog-Description",
                    "type": "string"
                },
                "DisplayName": {
                    "description": "AccessPackageCatalog-DisplayName",
                    "type": "string"
                },
                "Id": {
                    "description": "AccessPackageCatalog-Id",
                    "type": "string"
                }
            },
            "type": "object"
        },
        "Answers": {
            "type": "array"
        },
        "Assignment": {
            "properties": {
                "AssignmentPolicy": {
                    "properties": {
                        "DisplayName": {
                            "description": "AssignmentPolicy-DisplayName",
                            "type": "string"
                        },
                        "Id": {
                            "description": "AssignmentPolicy-Id",
                            "type": "string"
                        }
                    },
                    "type": "object"
                },
                "Id": {
                    "description": "Assignment-Id",
                    "type": "string"
                },
                "State": {
                    "description": "Assignment-State",
                    "type": "string"
                },
                "Status": {
                    "description": "Assignment-Status",
                    "type": "string"
                },
                "Target": {
                    "properties": {
                        "ConnectedOrganization": {
                            "properties": {
                                "Description": {
                                    "description": "Assignment-Target-ConnectedOrganization-Description",
                                    "type": "string"
                                },
                                "DisplayName": {
                                    "description": "Assignment-Target-ConnectedOrganization-DisplayName",
                                    "type": "string"
                                },
                                "Id": {
                                    "description": "Assignment-Target-ConnectedOrganization-Id",
                                    "type": "string"
                                }
                            },
                            "type": "object"
                        },
                        "DisplayName": {
                            "description": "Assignment-Target-DisplayName",
                            "type": "string"
                        },
                        "Id": {
                            "description": "Assignment-Target-Id",
                            "type": "string"
                        },
                        "ObjectId": {
                            "description": "Assignment-Target-ObjectId",
                            "type": "string"
                        }
                    },
                    "type": "object"
                }
            },
            "type": "object"
        },
        "CallbackConfiguration": {
            "properties": {
                "DurationBeforeTimeout": {
                    "type": "string"
                }
            },
            "type": "object"
        },
        "CallbackUriPath": {
            "type": "string"
        },
        "CustomExtensionStageInstanceId": {
            "type": "string"
        },
        "RequestType": {
            "type": "string"
        },
        "Requestor": {
            "properties": {
                "DisplayName": {
                    "description": "Requestor-DisplayName",
                    "type": "string"
                },
                "Id": {
                    "description": "Requestor-Id",
                    "type": "string"
                },
                "ObjectId": {
                    "description": "Requestor-ObjectId",
                    "type": "string"
                }
            },
            "type": "object"
        },
        "Stage": {
            "type": "string"
        },
        "State": {
            "type": "string"
        },
        "Status": {
            "type": "string"
        }
    },
    "type": "object"
}

申請時の質問事項

アクセス パッケージには申請時に、質問を設定し、申請者に回答させる事が可能です。
例えば、その申請者の勤務地や年齢を回答させるなどが可能です。質問は申請者の設定言語に応じて質問文を切り替えたり、回答を選択式にするなどの設定も可能です。
こちらの質問事項で回答いただいた内容で、アプリ側のアカウント属性を更新するということを確認していきたいと思います。
上記の JSON フォーマットでは Answer 属性があり、多分ここに回答結果が入ってくるのではと推測。JSON スキーマの情報では Array 型で来ることしかわからず、詳細がわからないので、調査しました。

とりあえず、質問を含めたアクセス パッケージを作成し、実際にリクエストを投げてみて、そのリクエストの結果をサンプルに JSON スキーマを作成したものが以下

{
    "type": "object",
    "properties": {
        "answer": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "Value": {
                        "type": "string"
                    },
                    "DisplayValue": {
                        "type": "string"
                    },
                    "AnsweredQuestion": {
                        "type": "object",
                        "properties": {
                            "Choices": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "Text": {
                                            "type": "string"
                                        },
                                        "Localizations": {
                                            "type": "array",
                                            "items": {
                                                "type": "object",
                                                "properties": {
                                                    "Text": {
                                                        "type": "string"
                                                    },
                                                    "LanguageCode": {
                                                        "type": "string"
                                                    }
                                                },
                                                "required": [
                                                    "Text",
                                                    "LanguageCode"
                                                ]
                                            }
                                        },
                                        "ActualValue": {
                                            "type": "string"
                                        },
                                        "DisplayValue": {
                                            "type": "object",
                                            "properties": {
                                                "DefaultText": {
                                                    "type": "string"
                                                },
                                                "LocalizedTexts": {
                                                    "type": "array",
                                                    "items": {
                                                        "type": "object",
                                                        "properties": {
                                                            "Text": {
                                                                "type": "string"
                                                            },
                                                            "LanguageCode": {
                                                                "type": "string"
                                                            }
                                                        },
                                                        "required": [
                                                            "Text",
                                                            "LanguageCode"
                                                        ]
                                                    }
                                                }
                                            }
                                        }
                                    },
                                    "required": [
                                        "Text",
                                        "Localizations",
                                        "ActualValue",
                                        "DisplayValue"
                                    ]
                                }
                            },
                            "AllowsMultipleSelection": {
                                "type": "boolean"
                            },
                            "IsMultipleSelectionAllowed": {
                                "type": "boolean"
                            },
                            "Id": {
                                "type": "string"
                            },
                            "IsRequired": {
                                "type": "boolean"
                            },
                            "IsAnswerEditable": {},
                            "Text": {
                                "type": "object",
                                "properties": {
                                    "DefaultText": {
                                        "type": "string"
                                    },
                                    "LocalizedTexts": {
                                        "type": "array",
                                        "items": {
                                            "type": "object",
                                            "properties": {
                                                "Text": {
                                                    "type": "string"
                                                },
                                                "LanguageCode": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "Text",
                                                "LanguageCode"
                                            ]
                                        }
                                    }
                                }
                            },
                            "TextString": {
                                "type": "string"
                            },
                            "Localizations": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "Text": {
                                            "type": "string"
                                        },
                                        "LanguageCode": {
                                            "type": "string"
                                        }
                                    },
                                    "required": [
                                        "Text",
                                        "LanguageCode"
                                    ]
                                }
                            },
                            "Sequence": {
                                "type": "integer"
                            },
                            "Attribute": {}
                        }
                    }
                },
                "required": [
                    "Value",
                    "DisplayValue",
                    "AnsweredQuestion"
                ]
            }
        }
    }
}

めっちゃ複雑やん。。。選んでない選択肢や、質問のローカライゼーションの情報まで送ってきているとは・・・
というか、選択式の回答は現状 GUI 上に複数選択のオプションないのですが、送られているリクエストを見ると "AllowsMultipleSelection"、"IsMultipleSelectionAllowed" とかがあるので、将来的に複数選択できるようになるのですかね?気になるところです。

少し脱線しましたが、 Value の値をとれば回答してもらった回答を取得する事ができるように見えます。

Logic Apps での構成

細かい点は割愛しますが、カスタム拡張機能で Logic Apps を作成した時点で、リクエストの ID と Stage で条件分岐がされます。(不正な Logic Apps へのリクエストを防ぐためでしょうね)
それぞれの条件を通った所に、Answer 配列から回答を For-each で取り出して、連携したアプリへの API をリクエストするようにすればやりたい事は実現できそうです。

実装上の懸念

アプリ側のアカウントのプロビジョニングを Microsoft Entra から SCIM で実施するのと組み合わせたりしたい気もしますが、そうすると、SCIM でのアカウント作成を待って、カスタム拡張機能での属性の追加をしたりしなければいけないので、考えたりすることが多く出てきそうです。

この辺りは実際のプロビジョニング連携を行う機会があれば追って対応案などをまとめたいと思います。

Microsoft (有志)

Discussion