パートナー センター Webhook

適用対象: パートナー センター | 21Vianet が運営するパートナー センター | Microsoft Cloud for US Government のパートナー センター

適切なロール: グローバル管理者 |課金管理 |管理 エージェント |販売エージェント |ヘルプデスク エージェント

パートナー センター Webhook API を使用すると、パートナーはリソース変更イベントに登録できます。 これらのイベントは、パートナーが登録した URL に HTTP POST の形式で配信されます。 パートナー センターからイベントを受信するために、パートナーは、パートナー センターがリソース変更イベントを POST できるコールバックをホストします。 イベントは、パートナーがパートナー センターから送信されたものであることを確認できるように、デジタル署名されます。 Webhook 通知は、共同販売の最新の構成を持つ環境にのみトリガーされます。

パートナーは、次の例のように、パートナー センターでサポートされている Webhook イベントから選択できます。

  • Azure Fraud イベントが検出されました ("azure-fraud-event-detected")

    このイベントは、Azure の不正アクセス イベントが検出されたときに発生します。

  • 委任された管理リレーションシップ承認済みイベント ("dap-admin-relationship-approved")

    このイベントは、委任された管理特権が顧客テナントによって承認されたときに発生します。

  • 顧客イベントによって承認されたリセラー関係 ("reseller-relationship-accepted-by-customer")

    このイベントは、リセラー関係が顧客テナントによって承認されたときに発生します。

  • 委任された管理リレーションシップ終了イベント ("dap-admin-relationship-terminated")

    このイベントは、委任された管理特権が顧客によって終了されたときに発生します。

  • Dap 管理リレーションシップが Microsoft イベントによって終了しました ("dap-admin-relationship-terminated-by-microsoft")

    このイベントは、DAP が 90 日を超える間非アクティブになったときに、Microsoft がパートナーテナントと顧客テナントの間で DAP を終了したときに発生します。

  • 詳細な管理アクセス割り当てアクティブ化イベント ("granular-admin-access-assignment-activated")

    このイベントは、Microsoft Entra ロールが特定のセキュリティ グループに割り当てられると、パートナーによって詳細な委任された管理特権アクセスの割り当てがアクティブになると発生します。

  • 詳細な管理アクセス割り当ての作成イベント ("詳細な admin-access-assignment-created")

    このイベントは、パートナーによって詳細な委任された管理特権アクセスの割り当てが作成されたときに発生します。 パートナーは、顧客が承認した Microsoft Entra ロールを特定のセキュリティ グループに割り当てることができます。

  • 詳細管理アクセス割り当て削除済みイベント ("詳細な admin-access-assignment-deleted")

    このイベントは、詳細な委任された管理特権アクセスの割り当てがパートナーによって削除されたときに発生します。

  • 詳細な管理アクセス割り当ての更新イベント ("詳細な admin-access-assignment-updated")

    このイベントは、パートナーによって詳細な委任された管理特権アクセスの割り当てが更新されたときに発生します。

  • 詳細な管理リレーションシップアクティブ化イベント ("詳細な admin-relationship-activated")

    このイベントは、細かい委任された管理特権が作成され、顧客が承認できるようにアクティブになると発生します。

  • 詳細な管理リレーションシップ承認済みイベント ("きめ細かい管理者関係-承認済み")

    このイベントは、きめ細かい委任された管理特権が顧客テナントによって承認されたときに発生します。

  • 詳細な管理リレーションシップの期限切れイベント ("詳細な admin-relationship-expired")

    このイベントは、詳細な委任された管理特権の有効期限が切れると発生します。

  • 詳細な管理リレーションシップの更新イベント ("granular-admin-relationship-updated")

    このイベントは、きめ細かい委任された管理特権がパートナー/顧客テナントによって更新されたときに発生します。

  • 詳細な管理リレーションシップの自動拡張イベント ("granular-admin-relationship-auto-extended")

    このイベントは、きめ細かい委任された管理特権がシステムによって自動的に拡張されるときに発生します。

  • 詳細な管理リレーションシップ終了イベント ("granular-admin-relationship-terminated")

    このイベントは、詳細な委任された管理特権がパートナー/顧客テナントによって終了されたときに発生します。

  • New Commerce Migration Completed ("new-commerce-migration-completed")

    このイベントは、新しいコマース移行が完了したときに発生します。

  • 新しいコマース移行の作成 ("new-commerce-migration-created")

    このイベントは、新しいコマース移行が作成されるときに発生します。

  • 新しいコマース移行に失敗しました ("new-commerce-migration-failed")

    このイベントは、新しいコマースの移行が失敗したときに発生します。

  • 新しいコマース移行スケジュールが失敗しました ("new-commerce-migration-schedule-failed")

    このイベントは、新しいコマース移行スケジュールが失敗したときに発生します。

  • Referral Created イベント ("referral-created")

    このイベントは、紹介の作成時に発生します。

  • 紹介更新イベント ("紹介更新")

    このイベントは、紹介が更新されたときに発生します。

  • 関連紹介作成イベント ("related-referral-created")

    このイベントは、関連する紹介が作成されるときに発生します。

  • 関連紹介更新イベント ("related-referral-updated")

    このイベントは、関連する紹介が更新されたときに発生します。

  • Subscription Updated イベント ("subscription-updated")

    このイベントは、サブスクリプションが変更されたときに発生します。 これらのイベントは、パートナー センター API を介して変更が行われた場合に加えて、内部の変更がある場合に生成されます。

    Note

    サブスクリプションが変更されてからサブスクリプションの更新イベントがトリガーされるまでに最大 48 時間の遅延があります。

  • Test イベント ("test-created")

    このイベントを使用すると、テスト イベントを要求し、その進行状況を追跡することで、登録を自己オンボードしてテストできます。 イベントの配信中に Microsoft から受信されているエラー メッセージを確認できます。 この制限は、"テストで作成された" イベントにのみ適用されます。 7 日より前のデータは消去されます。

  • Threshold Exceeded イベント ("usagerecords-thresholdExceeded")

    このイベントは、顧客の Microsoft Azure の使用量が使用量の支出予算 (しきい値) を超えると発生します。 詳細については、(顧客/パートナー センター/set-an-azure-spending-budget-for-your-customers の Azure 支出予算の設定) を参照してください。

今後の Webhook イベントは、パートナーが制御していないシステムで変更されるリソースに対して追加され、可能な限り "リアルタイム" に近いイベントを取得するためにさらに更新が行われます。 ビジネスに価値を追加するイベントに関するパートナーからのフィードバックは、追加する新しいイベントを決定する際に役立ちます。

パートナー センターでサポートされている Webhook イベントの完全な一覧については、パートナー センター Webhook イベントに関するページを参照してください

前提条件

  • パートナー センターの認証に関するページで説明している資格情報。 このシナリオでは、スタンドアロン アプリとアプリ + ユーザーの両方の資格情報を使った認証がサポートされています。

パートナー センターからのイベントの受信

パートナー センターからイベントを受信するには、パブリックにアクセスできるエンドポイントを公開する必要があります。 このエンドポイントは公開されているため、通信がパートナー センターからの通信であることを検証する必要があります。 受信したすべての Webhook イベントは、Microsoft Root にチェーンされている証明書でデジタル署名されます。 イベントの署名に使用される証明書へのリンクも提供されます。 これにより、サービスを再デプロイまたは再構成しなくても、証明書を更新できます。 パートナー センターは、イベントの配信を 10 回試行します。 10 回試行してもイベントが配信されない場合、イベントはオフライン キューに移動され、配信時にそれ以上の試行は行われません。

次の例は、パートナー センターから投稿されたイベントを示しています。

POST /webhooks/callback
Content-Type: application/json
Authorization: Signature VOhcjRqA4f7u/4R29ohEzwRZibZdzfgG5/w4fHUnu8FHauBEVch8m2+5OgjLZRL33CIQpmqr2t0FsGF0UdmCR2OdY7rrAh/6QUW+u+jRUCV1s62M76jbVpTTGShmrANxnl8gz4LsbY260LAsDHufd6ab4oejerx1Ey9sFC+xwVTa+J4qGgeyIepeu4YCM0oB2RFS9rRB2F1s1OeAAPEhG7olp8B00Jss3PQrpLGOoAr5+fnQp8GOK8IdKF1/abUIyyvHxEjL76l7DVQN58pIJg4YC+pLs8pi6sTKvOdSVyCnjf+uYQWwmmWujSHfyU37j2Fzz16PJyWH41K8ZXJJkw==
X-MS-Certificate-Url: https://3psostorageacct.blob.core.windows.net/cert/pcnotifications-dispatch.microsoft.com.cer
X-MS-Signature-Algorithm: rsa-sha256
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length: 195

{
    "EventName": "test-created",
    "ResourceUri": "http://localhost:16722/v1/webhooks/registration/test",
    "ResourceName": "test",
    "AuditUri": null,
    "ResourceChangeUtcDate": "2017-11-16T16:19:06.3520276+00:00"
}

Note

Authorization ヘッダーには、"Signature" のスキームがあります。 これは、コンテンツの base64 でエンコードされた署名です。

コールバックを認証する方法

パートナー センターから受信したコールバック イベントを認証するには、次の手順に従います。

  1. 必要なヘッダーが存在することを確認します (Authorization、x-ms-certificate-url、x-ms-signature-algorithm)。

  2. コンテンツに署名するために使用する証明書をダウンロードします (x-ms-certificate-url)。

  3. 証明書チェーンを確認します。

  4. 証明書の "組織" を確認します。

  5. UTF8 エンコードを使用してコンテンツをバッファーに読み取ります。

  6. RSA 暗号化プロバイダーを作成します。

  7. 指定したハッシュ アルゴリズム (SHA256 など) で署名されたものとデータが一致するかどうかを確認します。

  8. 検証が成功した場合は、メッセージを処理します。

Note

既定では、署名トークンは Authorization ヘッダーで送信されます。 登録で SignatureTokenToMsSignatureHeader を true に設定した場合、署名トークンは代わりに x-ms-signature ヘッダーで送信されます。

イベント モデル

次の表では、パートナー センター イベントのプロパティについて説明します。

プロパティ

件名 説明
EventName イベントの名前です。 {resource}-{action} の形式。 たとえば、"test-created" などです。
ResourceUri 変更されたリソースの URI。
ResourceName 変更されたリソースの名前。
AuditUrl 省略可能。 監査レコードの URI。
ResourceChangeUtcDate リソースの変更が発生した日時 (UTC 形式)。

サンプル

次の例は、パートナー センター イベントの構造を示しています。

{
    "EventName": "test-created",
    "ResourceUri": "http://api.partnercenter.microsoft.com/webhooks/v1/registration/validationEvents/c0bfd694-3075-4ec5-9a3c-733d3a890a1f",
    "ResourceName": "test",
    "AuditUri": null,
    "ResourceChangeUtcDate": "2017-11-16T16:19:06.3520276+00:00"
}

Webhook API

認証

Webhook API へのすべての呼び出しは、承認ヘッダーのベアラー トークンを使用して認証されます。 アクセスするアクセス トークンを取得します https://api.partnercenter.microsoft.com。 このトークンは、パートナー センター API の残りの部分にアクセスするために使用されるのと同じトークンです。

イベントの一覧の取得

Webhook API で現在サポートされているイベントの一覧を返します。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration/events

要求の例

GET /webhooks/v1/registration/events
content-type: application/json
authorization: Bearer eyJ0e.......
accept: */*
host: api.partnercenter.microsoft.com

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 183
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: c0bcf3a3-46e9-48fd-8e05-f674b8fd5d66
MS-RequestId: 79419bbb-06ee-48da-8221-e09480537dfc
X-Locale: en-US

[ "subscription-updated", "test-created", "usagerecords-thresholdExceeded" ]

イベントを受け取るために登録する

指定されたイベントを受け取るテナントを登録します。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration

要求の例

POST /webhooks/v1/registration
Content-Type: application/json
Authorization: Bearer eyJ0e.....
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length: 219

{
    "WebhookUrl": "{{YourCallbackUrl}}",
    "WebhookEvents": ["subscription-updated", "test-created"]
}

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 346
Content-Type: application/json; charset=utf-8
content-encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: 718f2336-8b56-4f42-93ac-54896047c59a
MS-RequestId: f04b1b5e-87b4-4d95-b087-d65fffec0bd2

{
    "SubscriberId": "e82cac64-dc67-4cd3-849b-78b6127dd57d",
    "WebhookUrl": "{{YourCallbackUrl}}",
    "WebhookEvents": [ "subscription-updated", "test-created" ]
}

登録を表示する

テナントの Webhook イベント登録を返します。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration

要求の例

GET /webhooks/v1/registration
Content-Type: application/json
Authorization: Bearer ...
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 341
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: c3b88ab0-b7bc-48d6-8c55-4ae6200f490a
MS-RequestId: ca30367d-4b24-4516-af08-74bba6dc6657
X-Locale: en-US

{
    "WebhookUrl": "{{YourCallbackUrl}}",
    "WebhookEvents": ["subscription-updated", "test-created"]
}

イベント登録を更新する

既存のイベント登録を更新します。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration

要求の例

PUT /webhooks/v1/registration
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOR...
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length: 258

{
    "WebhookUrl": "{{YourCallbackUrl}}",
    "WebhookEvents": ["subscription-updated", "test-created"]
}

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 346
Content-Type: application/json; charset=utf-8
content-encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: 718f2336-8b56-4f42-93ac-54896047c59a
MS-RequestId: f04b1b5e-87b4-4d95-b087-d65fffec0bd2

{
    "SubscriberId": "e82cac64-dc67-4cd3-849b-78b6127dd57d",
    "WebhookUrl": "{{YourCallbackUrl}}",
    "WebhookEvents": [ "subscription-updated", "test-created" ]
}

テスト イベントを送信して登録を検証する

Webhook の登録を検証するテスト イベントを生成します。 このテストは、パートナー センターからイベントを受信できることを検証するためのものです。 これらのイベントのデータは、最初のイベントが作成されてから 7 日後に削除されます。 検証イベントを送信する前に、登録 API を使用して"test-created" イベントに登録する必要があります。

Note

検証イベントを投稿する場合、スロットル制限は 1 分あたり 2 要求です。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration/validationEvents

要求の例

POST /webhooks/v1/registration/validationEvents
MS-CorrelationId: 3ef0202b-9d00-4f75-9cff-15420f7612b3
Authorization: Bearer ...
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length:

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 181
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: 04af2aea-d413-42db-824e-f328001484d1
MS-RequestId: 2f498d5a-a6ab-468f-98d8-93c96da09051
X-Locale: en-US

{ "correlationId": "04af2aea-d413-42db-824e-f328001484d1" }

イベントが配信されたことを確認する

検証イベントの現在の状態を返します。 この検証は、イベント配信の問題のトラブルシューティングに役立ちます。 応答には、イベントの配信を試行するたびに結果が含まれます。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration/validationEvents/{correlationId}

要求の例

GET /webhooks/v1/registration/validationEvents/04af2aea-d413-42db-824e-f328001484d1
MS-CorrelationId: 3ef0202b-9d00-4f75-9cff-15420f7612b3
Authorization: Bearer ...
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 469
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: 497e0a23-9498-4d6c-bd6a-bc4d6d0054e7
MS-RequestId: 0843bdb2-113a-4926-a51c-284aa01d722e
X-Locale: en-US

{
    "correlationId": "04af2aea-d413-42db-824e-f328001484d1",
    "partnerId": "00234d9d-8c2d-4ff5-8c18-39f8afc6f7f3",
    "status": "completed",
    "callbackUrl": "{{YourCallbackUrl}}",
    "results": [{
        "responseCode": "OK",
        "responseMessage": "",
        "systemError": false,
        "dateTimeUtc": "2017-12-08T21:39:48.2386997"
    }]
}

署名の検証の例

コールバック コントローラーシグネチャのサンプル (ASP.NET)

[AuthorizeSignature]
[Route("webhooks/callback")]
public IHttpActionResult Post(PartnerResourceChangeCallBack callback)

署名の検証

次の例は、Webhook イベントからコールバックを受信しているコントローラーに Authorization 属性を追加する方法を示しています。

namespace Webhooks.Security
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Security.Cryptography;
    using System.Security.Cryptography.X509Certificates;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web.Http;
    using System.Web.Http.Controllers;
    using Microsoft.Partner.Logging;

    /// <summary>
    /// Signature based Authorization
    /// </summary>
    public class AuthorizeSignatureAttribute : AuthorizeAttribute
    {
        private const string MsSignatureHeader = "x-ms-signature";
        private const string CertificateUrlHeader = "x-ms-certificate-url";
        private const string SignatureAlgorithmHeader = "x-ms-signature-algorithm";
        private const string MicrosoftCorporationIssuer = "O=Microsoft Corporation";
        private const string SignatureScheme = "Signature";

        /// <inheritdoc/>
        public override async Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            ValidateAuthorizationHeaders(actionContext.Request);

            await VerifySignature(actionContext.Request);
        }

        private static async Task<string> GetContentAsync(HttpRequestMessage request)
        {
            // By default the stream can only be read once and we need to read it here so that we can hash the body to validate the signature from microsoft.
            // Load into a buffer, so that the stream can be accessed here and in the api when it binds the content to the expected model type.
            await request.Content.LoadIntoBufferAsync();

            var s = await request.Content.ReadAsStreamAsync();
            var reader = new StreamReader(s);
            var body = await reader.ReadToEndAsync();

            // set the stream position back to the beginning
            if (s.CanSeek)
            {
                s.Seek(0, SeekOrigin.Begin);
            }

            return body;
        }

        private static void ValidateAuthorizationHeaders(HttpRequestMessage request)
        {
            var authHeader = request.Headers.Authorization;
            if (string.IsNullOrWhiteSpace(authHeader?.Parameter) && string.IsNullOrWhiteSpace(GetHeaderValue(request.Headers, MsSignatureHeader)))
            {
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Authorization header missing."));
            }

            var signatureHeaderValue = GetHeaderValue(request.Headers, MsSignatureHeader);
            if (authHeader != null
                && !string.Equals(authHeader.Scheme, SignatureScheme, StringComparison.OrdinalIgnoreCase)
                && !string.IsNullOrWhiteSpace(signatureHeaderValue)
                && !signatureHeaderValue.StartsWith(SignatureScheme, StringComparison.OrdinalIgnoreCase))
            {
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, $"Authorization scheme needs to be '{SignatureScheme}'."));
            }

            if (string.IsNullOrWhiteSpace(GetHeaderValue(request.Headers, CertificateUrlHeader)))
            {
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest, $"Request header {CertificateUrlHeader} missing."));
            }

            if (string.IsNullOrWhiteSpace(GetHeaderValue(request.Headers, SignatureAlgorithmHeader)))
            {
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest, $"Request header {SignatureAlgorithmHeader} missing."));
            }
        }

        private static string GetHeaderValue(HttpHeaders headers, string key)
        {
            headers.TryGetValues(key, out var headerValues);

            return headerValues?.FirstOrDefault();
        }

        private static async Task VerifySignature(HttpRequestMessage request)
        {
            // Get signature value from either authorization header or x-ms-signature header.
            var base64Signature = request.Headers.Authorization?.Parameter ?? GetHeaderValue(request.Headers, MsSignatureHeader).Split(' ')[1];
            var signatureAlgorithm = GetHeaderValue(request.Headers, SignatureAlgorithmHeader);
            var certificateUrl = GetHeaderValue(request.Headers, CertificateUrlHeader);
            var certificate = await GetCertificate(certificateUrl);
            var content = await GetContentAsync(request);
            var alg = signatureAlgorithm.Split('-'); // for example RSA-SHA1
            var isValid = false;

            var logger = GetLoggerIfAvailable(request);

            // Validate the certificate
            VerifyCertificate(certificate, request, logger);

            if (alg.Length == 2 && alg[0].Equals("RSA", StringComparison.OrdinalIgnoreCase))
            {
                var signature = Convert.FromBase64String(base64Signature);
                var csp = (RSACryptoServiceProvider)certificate.PublicKey.Key;

                var encoding = new UTF8Encoding();
                var data = encoding.GetBytes(content);

                var hashAlgorithm = alg[1].ToUpper();

                isValid = csp.VerifyData(data, CryptoConfig.MapNameToOID(hashAlgorithm), signature);
            }

            if (!isValid)
            {
                // log that we were not able to validate the signature
                logger?.TrackTrace(
                    "Failed to validate signature for webhook callback",
                    new Dictionary<string, string> { { "base64Signature", base64Signature }, { "certificateUrl", certificateUrl }, { "signatureAlgorithm", signatureAlgorithm }, { "content", content } });

                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Signature verification failed"));
            }
        }

        private static ILogger GetLoggerIfAvailable(HttpRequestMessage request)
        {
            return request.GetDependencyScope().GetService(typeof(ILogger)) as ILogger;
        }

        private static async Task<X509Certificate2> GetCertificate(string certificateUrl)
        {
            byte[] certBytes;
            using (var webClient = new WebClient())
            {
                certBytes = await webClient.DownloadDataTaskAsync(certificateUrl);
            }

            return new X509Certificate2(certBytes);
        }

        private static void VerifyCertificate(X509Certificate2 certificate, HttpRequestMessage request, ILogger logger)
        {
            if (!certificate.Verify())
            {
                logger?.TrackTrace("Failed to verify certificate for webhook callback.", new Dictionary<string, string> { { "Subject", certificate.Subject }, { "Issuer", certificate.Issuer } });

                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Certificate verification failed."));
            }

            if (!certificate.Issuer.Contains(MicrosoftCorporationIssuer))
            {
                logger?.TrackTrace($"Certificate not issued by {MicrosoftCorporationIssuer}.", new Dictionary<string, string> { { "Issuer", certificate.Issuer } });

                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, $"Certificate not issued by {MicrosoftCorporationIssuer}."));
            }
        }
    }
}