Веб-перехватчики Центра партнеров

Область применения: Центр партнеров | Центр партнеров, управляемый 21Vianet | Центр партнеров для Microsoft Cloud for US Government

Соответствующие роли: глобальный администратор | Администратор выставления счетов | агент Администратор | Агент продаж | Агент helpdesk

API-интерфейсы веб-перехватчика Центра партнеров позволяют партнерам регистрировать события изменения ресурсов. Эти события доставляются в виде HTTP POS-адресов в зарегистрированный URL-адрес партнера. Чтобы получить событие из Центра партнеров, партнеры будут размещать обратный вызов, в котором Центр партнеров может опубликовать событие изменения ресурса. Событие будет подписано цифровой подписью, чтобы партнер смог убедиться, что он был отправлен из Центра партнеров. Уведомления веб-перехватчика активируются только в среде с последней конфигурацией для совместной продажи.

Партнеры могут выбрать события веб-перехватчика, например следующие примеры, поддерживаемые Центром партнеров.

  • Обнаружено событие мошенничества Azure ("azure-fraud-event-detected")

    Это событие возникает при обнаружении события мошенничества Azure.

  • Делегированное Администратор утвержденное событие "dap-admin-relationship-approved")

    Это событие возникает, когда делегированные права Администратор были утверждены клиентом клиента.

  • Отношение торгового посредника, принятое событием клиента ("торговых посредников, принятых клиентом")

    Это событие возникает при утверждении отношения торгового посредника клиентом.

  • Делегированное событие завершения связи Администратор ("dap-admin-relationship-terminated")

    Это событие возникает, когда делегированные права Администратор были завершены клиентом.

  • Связь Dap Администратор прекращена событием Майкрософт ("dap-admin-relationship-terminated-by-microsoft")

    Это событие возникает, когда корпорация Майкрософт завершает DAP между партнером и клиентом, когда DAP неактивна более 90 дней.

  • Детальное Администратор активированное событие назначения доступа ("granular-admin-access-assignment-activated")

    Это событие возникает, когда назначение доступа к привилегированным делегированным Администратор привилегий активируется партнером после назначения ролей Microsoft Entra определенным группам безопасности.

  • Детальное Администратор созданное событие назначения доступа ("granular-admin-access-assignment-created")

    Это событие возникает, когда назначение доступа к привилегированным делегированным Администратор создается партнером. Партнеры могут назначать утвержденные клиентом роли Microsoft Entra определенным группам безопасности.

  • Детализация Администратор удаленное событие назначения доступа ("granular-admin-access-assignment-deleted")

    Это событие возникает, когда назначение доступа к привилегированным делегированным Администратор удаляется партнером.

  • Детализация Администратор обновленном событии назначения доступа ("granular-admin-access-assignment-updated")

    Это событие возникает, когда назначение доступа к привилегированным делегированным Администратор обновляется партнером.

  • Детализированное событие активации связи Администратор ("granular-admin-relationship-activated")

    Это событие возникает при создании и активном для утверждаемого клиентом делегированного делегата Администратор привилегий.

  • Детализация Администратор утвержденных событий связи ("granular-admin-relationship-approved")

    Это событие возникает при утверждении клиентом детализации делегированных привилегий Администратор.

  • Детализированное событие Администратор отношения с истекшим сроком действия ("гранулярно-администратор-отношение-истекло")

    Это событие возникает при истечении срока действия детализации делегированных Администратор привилегий.

  • Детализированное событие обновления связи Администратор ("granular-admin-relationship-updated")

    Это событие возникает, когда степень детализации делегированных Администратор привилегий обновляется клиентом партнера или клиента.

  • Детализация Администратор автоматическое расширенное событие связи ("granular-admin-relationship-auto-extended")

    Это событие возникает, когда детализация делегированного Администратор привилегий автоматически расширяется системой.

  • Детализированное событие Администратор связь завершено ("granular-admin-relationship-terminated")

    Это событие возникает, когда гранулярные делегированные права Администратор прерваны партнером или клиентом.

  • Завершена миграция новой коммерции ("new-commerce-migration-completed")

    Это событие возникает при завершении миграции новой коммерции.

  • Создание новой коммерческой миграции ("new-commerce-migration-created")

    Это событие возникает при создании новой коммерческой миграции.

  • Сбой миграции новой коммерции ("new-commerce-migration-failed")

    Это событие возникает при сбое новой коммерческой миграции.

  • Сбой расписания миграции новой коммерции ("new-commerce-migration-schedule-failed")

    Это событие возникает при сбое нового расписания миграции коммерции.

  • Событие создания рефералов ("реферал-создано")

    Это событие возникает при создании ссылки.

  • Обновленное событие рефералов (обновлено рефералов)

    Это событие возникает при обновлении ссылки.

  • Связанное событие создания рефералов (созданное со связанными рефералами)

    Это событие возникает при создании связанной ссылки.

  • Связанное событие обновления рефералов (связанное с реферальным обновлением)

    Это событие возникает при обновлении связанной ссылки.

  • Обновленное событие подписки (обновление подписки)

    Это событие возникает при изменении подписки. Эти события будут создаваться при наличии внутренних изменений в дополнение к тому, когда изменения вносятся через API Центра партнеров.

    Примечание.

    Существует задержка до 48 часов между временем изменения подписки и активацией события обновления подписки.

  • Тестовое событие ("тестовое создание")

    Это событие позволяет самостоятельно подключиться и проверить регистрацию, запросить тестовое событие и отслеживать его ход выполнения. При попытке доставки события вы увидите сообщения об ошибках, полученные от Корпорации Майкрософт. Это ограничение применяется только к событиям, созданным тестом. Данные старше семи дней будут удалены.

  • Превышение порогового значения ("usagerecords-thresholdExceed")

    Это событие возникает, когда объем использования Microsoft Azure для любого клиента превышает бюджет расходов на использование (пороговое значение). Дополнительные сведения см. в разделе (Настройка бюджета расходов Azure для клиентов/ партнеров/set-an-azure-spending-budget-for-your-customers).

Будущие события веб-перехватчика будут добавлены для ресурсов, которые изменяются в системе, над которым партнер не управляет, и дальнейшие обновления будут сделаны, чтобы получить эти события как можно ближе к "в режиме реального времени". Отзывы партнеров о том, какие события добавляют ценность для своего бизнеса, будут полезны при определении новых событий, которые необходимо добавить.

Полный список событий веб-перехватчика, поддерживаемых Центром партнеров, см. в разделе "События веб-перехватчика Центра партнеров".

Необходимые компоненты

  • Учетные данные, описанные в статье о проверке подлинности в Центре партнеров. Этот сценарий поддерживает проверку подлинности с помощью автономных учетных данных приложения и приложения и пользователя.

Получение событий из Центра партнеров

Чтобы получать события из Центра партнеров, необходимо предоставить общедоступную конечную точку. Так как эта конечная точка предоставляется, необходимо убедиться, что обмен данными осуществляется из Центра партнеров. Все события веб-перехватчика, которые вы получаете, имеют цифровую подпись с сертификатом, который цепочки с 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"
}

Примечание.

Заголовок авторизации имеет схему "Подпись". Это сигнатура в кодировке 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. Если проверка выполнена успешно, обработайте сообщение.

Примечание.

По умолчанию маркер подписи будет отправлен в заголовке авторизации. Если в регистрации задано значение SignatureTokenTokenToMsSignatureHeader , маркер подписи будет отправлен в заголовке подписи x-ms-signature.

Модель событий

В следующей таблице описаны свойства события Центра партнеров.

Свойства

Имя Описание
EventName Имя события. В форме {resource}-{action}. Например, "test-created".
ResourceUri URI ресурса, который изменился.
Имя ресурса Имя измененного ресурса.
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"
}

API веб-перехватчика

Проверка подлинности

Все вызовы API веб-перехватчика проходят проверку подлинности с помощью маркера носителя в заголовке авторизации. Получение маркера доступа для доступа https://api.partnercenter.microsoft.com. Этот маркер является тем же маркером, который используется для доступа к остальным API Центра партнеров.

Получение списка событий

Возвращает список событий, которые в настоящее время поддерживаются 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" ]
}

Просмотр регистрации

Возвращает регистрацию событий веб-перехватчиков для клиента.

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" ]
}

Отправка тестового события для проверки регистрации

Создает тестовое событие для проверки регистрации веб-перехватчиков. Этот тест предназначен для проверки того, что вы можете получать события из Центра партнеров. Данные для этих событий будут удалены семь дней после создания первоначального события. Перед отправкой события проверки необходимо зарегистрировать событие "test-create" с помощью API регистрации.

Примечание.

При публикации события проверки существует ограничение в 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)

Проверка подписи

В следующем примере показано, как добавить атрибут авторизации в контроллер, получающий обратные вызовы из событий веб-перехватчика.

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}."));
            }
        }
    }
}