2017年8月14日月曜日

APIゲートウェイでSaaSのIDを簡単に管理する②

こんにちは、富士榮です。

前回に引き続き、CDATA API ServerとID管理システムを使ってSaaS(今回はSalesforce.com)へのプロビジョニングを簡単に行う、という仕組みを作っていきます。

前回は、API ServerをSalesforce.comのUserテーブルへ接続し、ユーザの取得をしましたが、今回はいよいよID管理システム(今回はAzure AD Connect)を使ってユーザを作成します。

やることは非常に単純で、API Serverで定義されたリソース(実際はSalesforce.comのUserテーブル)に対してJSON形式で作成したいユーザの情報をPOSTするだけです。

◆投げ込むJSONを確認する

Salesforce.comの開発者ドキュメントを見ると、Userテーブルに新規レコード(つまり新規ユーザ)を作成する際に必須となる属性を確認します。

 SOAP API開発者ガイド)User
 https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_objects_user.htm

これを見ると、以下の属性が必須との事です。
  • Username :ユーザ識別子。メールアドレス形式
  • Alias :ユーザの別名
  • Email :ユーザのメールアドレス
  • LastName :ユーザの姓
  • EmailEncodingKey :メールの文字コード
  • LanguageLocaleKey :ユーザの言語
  • LocaleSidKey :ユーザのロケール
  • ProfileId :ユーザのプロファイルのID
  • TimeZoneSidKey :ユーザのタイムゾーン
  • UserPermissionsOfflineUser :オフラインエディションの使用可否
  • UserPermissionsMarketingUser :キャンペーン管理の可否

ユーザを作成するには、最低限これらの値をJSON形式でAPI ServerのリソースへPOSTすればよい、ということなので、まずは直接POSTしてみます。

ただ、EmailEncodingKeyやProfileIdなど、何を値として設定すれば良いのかよくわからない属性もあるので、まずはSalesforce.com上に手動で作成したユーザをGETし、各属性にどのような値が入っているのかを確認すると以下のような値が入っていることがわかるので、この例をテンプレートにPOSTするデータを作成します。

属性
Usernamexxxxx@example.com
AliasFujie
Emailxxxxx@example.com
LastNameFujie
EmailEncodingKeyISO-2022-JP
LanguageLocaleKeyja
LocaleSidKeyja_JP
ProfileId(Chatter Free Userの場合)00e7F000001LotXQAS
TimeZoneSidKeyAsia/Tokyo
UserPermissionsOfflineUserfalse
UserPermissionsMarketingUserfalse


以下のようなJSONを作成し、Chrome ExtensionのAdvanced REST Clientを使ってAPI Serverに定義したリソースエンドポイントへPOSTをします。
{
  "Username": "test999@eidentity.jp",
  "Alias": "test999",
  "Email": "test999@eidentity.jp",
  "LastName": "Test",
  "EmailEncodingKey": "ISO-2022-JP",
  "LanguageLocaleKey": "ja",
  "LocaleSidKey": "ja_JP",
  "ProfileId": "00e7F000001LotXQAS",
  "TimeZoneSidKey": "Asia/Tokyo",
  "UserPermissionsOfflineUser": "false",
  "UserPermissionsMarketingUser": "false"
}


上手くいくと、HTTP 201 Createdが返ってきて実際にユーザが作成されます。


Salesforce.comの管理画面からユーザを確認すると実際に作成されていることがわかります。


◆管理エージェントの作成

投げ込むJSONがわかったところで、あとはID管理システムでJSONの生成からPOSTを自動化するだけです。

やることはFIM/MIM向けのECMA2エージェントとして以前公開したGeneric REST API Management Agentとよく似たようなことなので、このコードをカスタマイズしてみます。

細かいECMA2エージェントの開発方法は別エントリでそのうち解説しますが、以下の手順でGeneric REST API Management Agentを改造します。

1.スキーマ定義(GetSchemaメソッド)
 先にリストアップした必須属性を定義します。Username属性をAnchor属性として設定をしています。こんなコードになります。
                SchemaType personType = SchemaType.Create("Person", false);
                personType.Attributes.Add(SchemaAttribute.CreateAnchorAttribute("Username", AttributeType.String));
                personType.Attributes.Add(SchemaAttribute.CreateSingleValuedAttribute("Alias", AttributeType.String));
                personType.Attributes.Add(SchemaAttribute.CreateSingleValuedAttribute("Email", AttributeType.String));
                personType.Attributes.Add(SchemaAttribute.CreateSingleValuedAttribute("LastName", AttributeType.String));
                personType.Attributes.Add(SchemaAttribute.CreateSingleValuedAttribute("EmailEncodingKey", AttributeType.String));
                personType.Attributes.Add(SchemaAttribute.CreateSingleValuedAttribute("LanguageLocaleKey", AttributeType.String));
                personType.Attributes.Add(SchemaAttribute.CreateSingleValuedAttribute("LocaleSidKey", AttributeType.String));
                personType.Attributes.Add(SchemaAttribute.CreateSingleValuedAttribute("ProfileId", AttributeType.String));
                personType.Attributes.Add(SchemaAttribute.CreateSingleValuedAttribute("TimeZoneSidKey", AttributeType.String));
                personType.Attributes.Add(SchemaAttribute.CreateSingleValuedAttribute("UserPermissionsOfflineUser", AttributeType.Boolean));
                personType.Attributes.Add(SchemaAttribute.CreateSingleValuedAttribute("UserPermissionsMarketingUser", AttributeType.Boolean));
                Schema schema = Schema.Create();
                schema.Types.Add(personType);
                return schema;

 こうすることで管理エージェントで管理対象属性を選択できるようになります。


2.パラメータ定義(GetConfigParametersメソッド)
 API Server上のリソースURIや接続に使うAPI ServerユーザのAuthTokenを指定できるようにします。こんなコードになります。接続パラメータの部分だけで良いのでConnectivityの部分にパラメータを追加しています。
                switch (_page)
                {
                    case ConfigParameterPage.Capabilities:
                        break;
                    case ConfigParameterPage.Connectivity:
                        _configParametersDefinitions.Add(ConfigParameterDefinition.CreateLabelParameter("Connection parameters"));
                        _configParametersDefinitions.Add(ConfigParameterDefinition.CreateStringParameter(ConstDefinition.CFG_RESOURCE_URI, ""));
                        _configParametersDefinitions.Add(ConfigParameterDefinition.CreateStringParameter(ConstDefinition.CFG_AUTH_TOKEN, ""));
                        break;
                    case ConfigParameterPage.Global:
                        break;
                    case ConfigParameterPage.Partition:
                        break;
                    case ConfigParameterPage.RunStep:
                        break;
                    case ConfigParameterPage.Schema:
                        break;
                }

尚、テストなので、ValidateConfigParametersについては処理を入れておらず、指定されたパラメータの値の確認などは行っていません。

これで、管理エージェント設定画面で接続関連のパラメータを指定することが出来るようになります。ここにAPI ServerのリソースURIとAuthTokenを指定します。

3.インポート処理関連(OpenImportConnection/GetImportEntriesメソッド)
 まずはOpenImportConnectionで、設定パラメータからリソースURIとAuthTokenの値を取り出し、後続のGetImportEntriesで使えるようにグローバル変数にセットしておきます。
                resource_uri = _configParameters[ConstDefinition.CFG_RESOURCE_URI].Value.ToString();
                auth_token = _configParameters[ConstDefinition.CFG_AUTH_TOKEN].Value.ToString();

 次に、API Server(を経由してSalesforce.com)からユーザ情報を取得するGetImportEntriesです。やることはシンプルで、API ServerのリソースURIへAuthToken付きのGETリクエストを投げ、取得したJSONをパースしてConnector Space上のエントリに追加していくだけです。尚、本来はページング処理などをする必要があるのですが、今回はテストなので省略しています。
                var _csentries = new List<CSEntryChange>();
                string _importEntriesJSON = utils.GetContentsWithAccessToken(resource_uri, auth_token, null);
                var _getImportEntriesByObjectTypeResult = JObject.Parse(_importEntriesJSON).SelectToken("value").ToString();
                var _importObjectJSONArray = JArray.Parse(_getImportEntriesByObjectTypeResult);
                foreach (var _importObjectJSON in _importObjectJSONArray)
                {
                    var _csentryChange = CSEntryChange.Create();
                    _csentryChange.ObjectModificationType = ObjectModificationType.Add;
                    _csentryChange.ObjectType = "Person";
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("Alias", _importObjectJSON["Alias"].ToString()));
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("Email", _importObjectJSON["Email"].ToString()));
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("Username", _importObjectJSON["Username"].ToString()));
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("LastName", _importObjectJSON["LastName"].ToString()));
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("EmailEncodingKey", _importObjectJSON["EmailEncodingKey"].ToString()));
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("LanguageLocaleKey", _importObjectJSON["LanguageLocaleKey"].ToString()));
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("LocaleSidKey", _importObjectJSON["LocaleSidKey"].ToString()));
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("ProfileId", _importObjectJSON["ProfileId"].ToString()));
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("TimeZoneSidKey", _importObjectJSON["TimeZoneSidKey"].ToString()));
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("UserPermissionsOfflineUser", false));
                    _csentryChange.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("UserPermissionsMarketingUser", false));
                    _csentries.Add(_csentryChange);
                }
                var _importReturnInfo = new GetImportEntriesResults();
                _importReturnInfo.MoreToImport = false;
                _importReturnInfo.CSEntries = _csentries;
                return _importReturnInfo;

utils.GetContentsWithAccessTokenの中ではx-cdata-authtokenヘッダにAuthTokenをセットし、リソースURIにGETリクエストを投げているだけです。
                _httpClient.DefaultRequestHeaders.Add("x-cdata-authtoken", _accessToken);
                return await _httpClient.GetStringAsync(_url);


4.エクスポート処理関連(OpenExportConnection/PutExportEntriesメソッド)
 こちらは先のインポートとは逆で、実際にAPI Serverへユーザを出力(プロビジョニング)する処理です。
 OpenExportConnectionではインポート時と同じくリソースURIとAuthTokenを設定から取得し、PutExportEntriesでConnector Space内のプロビジョニング対象ユーザを取得してJSON形式に整形してリソースURIへPOSTを行います。
                foreach (CSEntryChange _csentryChange in _csentries)
                {
                    // build json to POST
                    var _attr = new Dictionary<string, string>();
                    // anchor attribute
                    _attr.Add("Username", _csentryChange.DN.ToString());
                    switch (_csentryChange.ObjectModificationType)
                    {
                        case ObjectModificationType.Add:
                        case ObjectModificationType.Update:
                        foreach (string _attribName in _csentryChange.ChangedAttributeNames)
                        {
                            var _attributeChange = _csentryChange.AttributeChanges[_attribName];
                            var _valueChanges = _attributeChange.ValueChanges;
                            if (_valueChanges != null)
                            {
                                foreach (var _valueChange in _valueChanges)
                                {
 if (_valueChange.ModificationType == ValueModificationType.Add)
 {
// new value
_attr.Add(_attribName, _valueChange.Value.ToString());
break;
}
 }
                                }
                            }
                            // build json
                            string _exportDataJSON = JsonConvert.SerializeObject(_attr);
                            string _exportResult = utils.PostContentsWithAccessToken(resource_uri, auth_token, _exportDataJSON, null);

                            _exportEntriesResults.CSEntryChangeResults.Add(
                                CSEntryChangeResult.Create(_csentryChange.Identifier,
_csentryChange.AttributeChanges,
MAExportError.Success));
                                break;
                            case ObjectModificationType.Delete:
                                // NOT Implemented
                                break;

先ほどと同じく、utils.PostContentsWithAccessTokenではAuthTokenをヘッダにつけてJSONをリソースURIへPOSTしています。

大きくはこれで終わりです。
後はビルドしてAzure AD Connectが読み込めるようにExtensionsフォルダ(初期値はC:\Program Files\Microsoft Azure AD Sync\Extensions)へDLLファイルをコピーしておきます。

参考までにソースは以下のレポジトリにあげておきます。
https://github.com/fujie/apiserver

◆管理エージェントの設定

*繰り返しになりますが、Azure AD ConnectへカスタムManagement Agentを組み込む場合、所定の手続きを踏まないとサポート対象外となります。今回は手元にちょうどあった環境を使ったのでAzure AD Connectを使いましたが、本番環境ではMIM(Microsoft Identity Manager)やExgen LDAP Managerなど汎用的なID管理ツールを使ってください。
次は、Azure AD ConnectのSynchronization Serviceに開発したECMA2エージェントを設定します。
コネクタの種類にExtensible Connectivity 2.0を選択し、DLLをロードします。



後は、先に示した画面の様に接続設定でリソースURI、AuthTokenを設定し、管理対象の属性を選択すれば管理エージェント自体の構成は終了です。

次は、Run Profileの設定です。これを設定しないとAzure AD Connectはインポート、同期、エクスポートの各処理を実行してくれません。また、プロファイルの名前をFull Import、Full Synchronization、Export、など決められた名称にしておかないと30分に一回の自動処理の流れに乗せてくれず処理がスタックするので命名は大事です。
尚、今回作った管理エージェントにはDelta Importの処理を実装していないので、プロファイルの定義は行えません。イベントログにDelta Importの定義が無いのでスキップしますよ~という警告が出ますが特に影響はないので無視します。


これでSynchronization Service側の設定は完了です。

◆同期ルールを定義する

後は実際のMetaverse上のデータと先ほど作成した管理エージェントのConnector Space上のエントリの属性のマッピング(同期ルール)の設定を行います。FIMやMIMではポータルでISR/OSRを定義してルールを設定するのですが、今回はAzure AD ConnectなのでSynchronization Rules Editorを使います。個人的にはFIM/MIMの設定画面よりもこのルールエディタの方が使いやすいので好きだったりします。

今回はプロビジョニングだけでいいので、アウトバウンド同期ルールを定義します。
DirectionはOutbound、Connectorは先ほど作成したAPI Serverを選択してAdd new ruleをクリックしてルールを作成します。(以下は作成後の画面)

LinkTypeにprovisionを選択することで新規にユーザを作成するためのルールとして扱われます。

MetaverseとConnector SpaceのユーザのマッチングはUserprincipalNameおよびUsernameを使うので、Join rulesは以下のように設定しています。

後は、本命の属性のマッピングです。
エンコードやロケール、プロファイルIDなどは面倒なので固定値を入れて、ユーザ固有の値だけMetaverseの情報をマッピングします。

これでSaveをするとすべての設定が完了です。

◆テスト

後は、ローカルのAD上にユーザを作成、Azure AD Connectの同期ジョブが動くと、Azure ADへのユーザ作成に加えて、API Serverを経由してSalesforce.comへもユーザが作成されます。

この辺りは前回も紹介したデモの動画を見てもらった方が早いと思います。



Azure AD Connectを使った関係で色々とトリッキーなこともしていますが、少なくともID管理システム側でSalesforce.com自体の存在を殆ど意識せずに構成をすることが出来ていることはご理解いただけるのではないでしょうか?
スキーマの設定についても動的に取得する様に管理エージェントを構成すれば、API Serverの先にあるのがSalesforce.comだろうがDynamics365だろうが基本設定を変える必要は全くなく、API Serverが上手くハンドリングをしてくれますので、従来の個別開発に比べて相当簡単に各種SaaSなどへのプロビジョニングを構成することが可能になります。

今後、クラウド・サービスの活用がますます増えてくると思われますので、既存のID管理システムを効率よく対応させていくための一つの選択肢として、API Serverのようなデータ統合プラットフォームを活用するのはアリかも知れませんね。

0 件のコメント: