Я хотел бы программно отображать и управлять классическими (старыми) виртуальными машинами в Azure. Для управляемых это не проблема, есть библиотеки, а остальные API работают, но как только я получил вызов старого API для классического вывода, я получил 403 (Запрещено).
Код в порядке? Нужно ли мне управлять учетными данными для старого API в другом месте?
Мой код здесь:
static void Main(string[] args)
{
string apiNew = "https://management.azure.com/subscriptions/xxxxxxxxxxxxxxxxxxxxxxxx/providers/Microsoft.Compute/virtualMachines?api-version=2018-06-01";
string apiOld = "https://management.core.windows.net/xxxxxxxxxxxxxxxxxxxxxxxx/services/vmimages"
AzureRestClient client = new AzureRestClient(credentials.TenantId, credentials.ClientId, credentials.ClientSecret);
//OK - I can list the managed VMs.
string resultNew = client.GetRequestAsync(apiNew).Result;
// 403 forbidden
string resultOld = client.GetRequestAsync(apiOld).Result;
}
public class AzureRestClient : IDisposable
{
private readonly HttpClient _client;
public AzureRestClient(string tenantName, string clientId, string clientSecret)
{
_client = CreateClient(tenantName, clientId, clientSecret).Result;
}
private async Task<string> GetAccessToken(string tenantName, string clientId, string clientSecret)
{
string authString = "https://login.microsoftonline.com/" + tenantName;
string resourceUrl = "https://management.core.windows.net/";
var authenticationContext = new AuthenticationContext(authString, false);
var clientCred = new ClientCredential(clientId, clientSecret);
var authenticationResult = await authenticationContext.AcquireTokenAsync(resourceUrl, clientCred);
var token = authenticationResult.AccessToken;
return token;
}
async Task<HttpClient> CreateClient(string tenantName, string clientId, string clientSecret)
{
string token = await GetAccessToken(tenantName, clientId, clientSecret);
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
return client;
}
public async Task<string> GetRequestAsync(string url)
{
return await _client.GetStringAsync(url);
}
}
ОБНОВЛЕНИЕ 1:
Детали ответа:
HTTP/1.1 403 Forbidden
Content-Length: 288
Content-Type: application/xml; charset=utf-8
Server: Microsoft-HTTPAPI/2.0
Date: Mon, 22 Oct 2018 11:03:40 GMT
HTTP/1.1 403 Forbidden
Content-Length: 288
Content-Type: application/xml; charset=utf-8
Server: Microsoft-HTTPAPI/2.0
Date: Mon, 22 Oct 2018 11:03:40 GMT
<Error xmlns = "http://schemas.microsoft.com/windowsazure" xmlns:i = "http://www.w3.org/2001/XMLSchema-instance">
<Code>ForbiddenError</Code>
<Message>The server failed to authenticate the request.
Verify that the certificate is valid and is associated with this subscription.</Message>
</Error>
Обновление 2:
Я обнаружил, что тот же API используется командой PowerShell Get-AzureVMImage и работает из PowerShell. Powershell просит меня сначала войти в Azure с интерактивными окнами входа по электронной почте и паролю, а запрос использует заголовок Bearer для аутентификации, как мой код.
Если я обнюхаю токен доступа (заголовок Bearer) из сообщения, созданного Powershell, я смогу успешно взаимодействовать с этим API.
Обновление 3: РЕШЕНО, ответьте ниже.
Любопытно, как не работал другой токен на предъявителя. Может быть, учетные данные?





Судя по моему тесту, вам нужно получить токен доступа в интерактивном режиме.
А где взять токен «в интерактивном режиме»?
Согласно связанной документации, при запросе классического REST API отсутствует требуемый заголовок запроса.
x-ms-version - Required. Specifies the version of the operation to use for this request. This header should be set to 2014-02-01 or higher.
Ссылка Список образов ВМ: заголовки запросов
Чтобы разрешить включение заголовка, создайте перегрузку для запросов GET в AzureRestClient.
public async Task<string> GetRequestAsync(string url, Dictionary<string, string> headers) {
var request = new HttpRequestMessage(HttpMethod.Get, url);
if (headers != null)
foreach (var header in headers) {
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
var response = await _client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
и включить требуемый заголовок при вызове apiOld
var headers = new Dictionary<string, string>();
headers["x-ms-version"] = "2014-02-01";
string resultOld = client.GetRequestAsync(apiOld, headers).GetAwaiter().GetResult();
Привет, Nkosi, я добавил заголовок к запросу и проверил запрос в отладчике Fiddler HTTP. Теперь заголовок включен, но он не решает проблему «403 Forbidden».
@ qub1n предоставил ли ответ скрипача какие-либо другие подробности, когда возвращается запрещенный статус?
Вопрос обновлен, подробный ответ включен. Новый Azure APi может использовать токен или сертификат. Так что я предполагаю то же самое со старым.
Я отлично воспроизвел вашу проблему. К сожалению, я не получил рабочий исходный код со старым API, удовлетворяющим ваши потребности.
Хотя я нашел поставщика Microsoft.ClassicCompute вместо обычного используемого Microsoft.Compute, но все еще не смог провести рабочий тест.
Я почти уверен, что вам больше не следует использовать «вручную» старый устаревший API, а следует использовать современные пакеты Microsoft, позволяющие управлять классическими и «обычными» элементами, такими как виртуальные машины или учетные записи хранилища.
Ключевой пакет - Microsoft.Azure.Management.Compute.Fluent
Вы можете найти документацию здесь: https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.management.compute.fluent?view=azure-dotnet
Сообщите мне, если вам все еще понадобится помощь.
Но современный Fluent API не может перечислять и управлять старыми (классическими объектами).
Вы уверены? Вдобавок я пробовал с лазурным cli, и это нормально; может можно проверить исходный код (github.com/Azure/azure-cli)? Пример: лазурный список виртуальных машин
Финнали у меня все заработало:
Первый открытый Powershell:
Get-AzurePublishSettingsFile
и сохраните этот файл.
затем введите Powershell
Import-AzurePublishSettingsFile [mypublishsettingsfile]
Откройте хранилище сертификатов и найдите импортированный сертификат. И используйте этот сертификат одновременно с учетными данными в HttpClient.
Вот это да. Как вы в конечном итоге пришли к такому процессу? Хорошая находка.
Обратный инжиниринг Powershell Команды Azure + Google. Особенно я пробовал все остальное.
Хорошо идет. Рад, что ты это понял.
qub1n и @Nkosi .. Я вижу, что вы нашли хороший способ решения проблемы 403. Я добавил новый ответ, объясняющий причину проблемы 403 и изменения кода для правильного использования делегированных разрешений. С новыми изменениями кода, которые я предложил, вам не нужно будет использовать секреты клиента из консольного приложения (это угроза безопасности) или команду PowerShell для получения настроек публикации. Более того, вы должны иметь возможность поделиться приложением с любым пользователем, и оно будет работать только с их привилегиями, возвращая только информацию, к которой у них есть доступ.
Привет @Rohit, я вижу твой ответ, и он выглядит многообещающим. Проверю сегодня-завтра.
Это связано с тем, что ваше приложение, зарегистрированное в Azure AD, неправильно использует делегированные разрешения «API управления службами Windows Azure». Я говорю это, потому что вижу, что ваш код получает токен напрямую, используя идентификатор приложения (ClientCredential), а не как пользователь.
См. Скриншоты ниже. Window Azure Service Management API явно не предоставляет никаких разрешений для приложений, единственное, что можно использовать, - это делегированные разрешения. Если вы хотите больше узнать о разнице между двумя видами разрешений, прочтите Разрешения в Azure AD. Короче говоря, при использовании делегированных разрешений приложению делегируется разрешение действовать как зарегистрированный пользователь при вызовах API. Значит, должен быть зарегистрированный пользователь.
Я смог воспроизвести ошибку 403, используя ваш код, а затем смог заставить ее работать и вернуть список классических виртуальных машин с некоторыми изменениями. Далее я объясню необходимые изменения.
Перейдите в Azure AD> Регистрация приложений> ваше приложение> Параметры> Необходимые разрешения:
Изменение будет заключаться в том, чтобы получить токен как зарегистрированный пользователь, а не напрямую использовать clientId и секрет приложения. Поскольку ваше приложение является консольным, имеет смысл сделать что-то вроде этого, которое предложит пользователю ввести учетные данные:
var authenticationResult = await authenticationContext.AcquireTokenAsync(resourceUrl, clientId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Auto));
Кроме того, поскольку ваше приложение является консольным, было бы лучше зарегистрировать его как «родное» приложение, а не веб-приложение, как оно у вас есть прямо сейчас. Я говорю это потому, что консольные приложения или настольные клиентские приложения, которые могут работать в пользовательских системах, небезопасны для обработки секретов приложений, поэтому вы не должны регистрировать их как «Веб-приложение / API» и не использовать в них какие-либо секреты, поскольку это представляет угрозу безопасности. .
Итак, всего 2 изменения, и все готово. Как я уже говорил ранее, я пробовал их и вижу, что код работает нормально и получил список классических виртуальных машин.
а. Зарегистрируйте свое приложение в Azure AD как собственное приложение (т. Е. Тип приложения должен быть собственным, а не веб-приложением / API), затем в требуемых разрешениях добавьте «Window Azure Service Management API» и проверьте делегированные разрешения в соответствии с предыдущими снимками экрана в пункте 1.
б. Измените способ получения токена, чтобы можно было использовать делегированные разрешения в соответствии с вошедшим пользователем. Конечно, вошедший в систему пользователь должен иметь разрешения на виртуальные машины, которые вы пытаетесь перечислить, или, если у вас несколько пользователей, список будет отражать те виртуальные машины, к которым в настоящее время вошел пользователь, имеющий доступ.
Вот весь рабочий код после того, как я его изменил.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Net.Http;
using System.Net.Http.Headers;
namespace ListVMsConsoleApp
{
class Program
{
static void Main(string[] args)
{
string tenantId = "xxxxxx";
string clientId = "xxxxxx";
string redirectUri = "https://ListClassicVMsApp";
string apiNew = "https://management.azure.com/subscriptions/xxxxxxxx/providers/Microsoft.Compute/virtualMachines?api-version=2018-06-01";
string apiOld = "https://management.core.windows.net/xxxxxxxx/services/vmimages";
AzureRestClient client = new AzureRestClient(tenantId, clientId, redirectUri);
//OK - I can list the managed VMs.
//string resultNew = client.GetRequestAsync(apiNew).Result;
// 403 forbidden - should work now
string resultOld = client.GetRequestAsync(apiOld).Result;
}
}
public class AzureRestClient
{
private readonly HttpClient _client;
public AzureRestClient(string tenantName, string clientId, string redirectUri)
{
_client = CreateClient(tenantName, clientId, redirectUri).Result;
}
private async Task<string> GetAccessToken(string tenantName, string clientId, string redirectUri)
{
string authString = "https://login.microsoftonline.com/" + tenantName;
string resourceUrl = "https://management.core.windows.net/";
var authenticationContext = new AuthenticationContext(authString, false);
var authenticationResult = await authenticationContext.AcquireTokenAsync(resourceUrl, clientId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Auto));
return authenticationResult.AccessToken;
}
async Task<HttpClient> CreateClient(string tenantName, string clientId, string redirectUri)
{
string token = await GetAccessToken(tenantName, clientId, redirectUri);
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
client.DefaultRequestHeaders.Add("x-ms-version", "2014-02-01");
return client;
}
public async Task<string> GetRequestAsync(string url)
{
return await _client.GetStringAsync(url);
}
}
}
Теперь я это понимаю и с вашей помощью заставляю работать. Большое тебе спасибо.
@ qub1n, пожалуйста, и рад узнать, что это сработало.
Согласно связанной документации, вам кажется, что вам не хватает необходимого заголовка запроса
x-ms-version - Required. Specifies the version of the operation to use for this request. This header should be set to 2014-02-01 or higher.