Neste post, iremos consumir a API do Azure Resource Graph por meio de uma aplicação Blazor. A chamada a API irá retornar as regiões do Azure em que a subscription selecionada possui recursos de nuvem, junto com suas respectivas quantidades. Para que isso seja possível, iremos implementar a autenticação do aplicativo web com a ajuda do Azure AD. Nosso web app possuirá duas partes: server e client.

Sobre o Azure AD, eu já escrevi alguns artigos sobre essa ferramenta. Recomendo a leitura!

Vamos criar o registro de aplicativo para a parte do server no nosso Azure AD.

Para que o server faça a chamada ao Azure Resource Graph em nome do usuário logado (no client), precisaremos criar um secret para esse registro de aplicativo.

Como mencionado anteriormente, o server irá fazer a chamada a uma API protegida do Azure, por isso será necessário adicionar o user_impersonation scope por meio do menu "API permissions".

Além disso, vamos consentir (admin consent) essa permissão para os todos os usuários da nossa tenant.

Para finalizar, devemos criar um scope para que outros registros de aplicativos possam pedir autorização para consumir a nossa aplicação "app-server-api".

Vamos agora criar o registro de aplicativo para o client. Como iremos utilizar o blazor como client, devemos prover uma redirect URI.

Com o registro criado, podemos ir ao menu "API permissions" e adicionar a permissão (scope) criada na etapa anterior, a "Access.Server".

Dessa forma, quando o usuário acessar o client pela primeira vez, a página de login do Azure AD irá perguntar se o usuário permite que o client acesse o Access.Server scope em seu nome.

Com os dados prontos, já podemos criar o projeto blazor. Vale a pena dar uma lida na documentação oficial para entender melhor sobre os parâmetros do comando abaixo.

dotnet new blazorwasm -au SingleOrg \
--api-client-id "{SERVER API APP CLIENT ID}" \
--app-id-uri "{SERVER API APP ID URI GUID}" \
--client-id "{CLIENT APP CLIENT ID}" \ 
--default-scope "{DEFAULT SCOPE}" \
--domain "{TENANT DOMAIN}" \ 
-ho -o {PROJECT NAME} \
--tenant-id "{TENANT ID}"

Toda a parte de configuração da autenticação foi finalizada, vamos preparar agora a chamada à API do Azure Resource Graph. Iremos inicialmente criar os modelos retornados pela API dentro do projeto Shared.

using System.Text.Json.Serialization;

namespace ResourceGraphApp.Shared;

public class Content
{
    [JsonPropertyName("location")]
    public string Location { get; set; } = default!;

    [JsonPropertyName("qtd")]
    public int Qtd { get; set; }
}

public class CloudRegion
{
    [JsonPropertyName("totalRecords")]
    public int TotalRecords { get; set; }

    [JsonPropertyName("count")]
    public int Count { get; set; }

    [JsonPropertyName("data")]
    public List<Content> Data { get; set; } = default!;

    [JsonPropertyName("facets ")]
    public List<object> Facets { get; set; } = default!;

    [JsonPropertyName("resultTruncated")]
    public string ResultTruncated { get; set; } = default!;
}

No projeto server, vamos criar a controladora CloudResourcesController com o seguinte conteúdo:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;
using ResourceGraphApp.Shared;

namespace ResourceGraphApp.Server.Controllers;

[Authorize]
[ApiController]
[Route("[controller]")]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class CloudResourcesController : ControllerBase
{
    private readonly IDownstreamWebApi _downstreamWebApi;
    
    public CloudResourcesController(IDownstreamWebApi downstreamWebApi)
    {
        _downstreamWebApi = downstreamWebApi;
    }

    [HttpGet]
    [AuthorizeForScopes(ScopeKeySection = "ResourceGraphAPI:Scopes")]
    public async Task<IActionResult> GetAsync()
    {
       
        var value = await _downstreamWebApi.PostForUserAsync<CloudRegion, object>(
            "ResourceGraphAPI",
            "/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01",
            new
            {
                subscriptions = new List<string> { "3a1cf685-3342-4c07-a8f7-07bb6008e146" },
                query = "Resources | summarize qtd=count() by location"
            });

        return Ok(value);
    }
}

Alguns pontos sobre essa controladora:

  • Possui o atributo Authorize que apenas permite requisições autenticadas à nossa controladora;
  • Possui o atributo RequiredScope que permite que apenas requisições com o Access.Server scope (via appsettings.json) consentido pelo usuário;
  • Utiliza a interface IDownstreamWebApi para fazer um POST a API do azure Resource Graph em nome do usuário logado. Toda a parte de obtenção de um novo token de acesso é feita de forma automática! Veja a documentação oficial;
  • Fazemos a chamada a API, passando como parâmetro, a query em KUSTO: "Resources | summarize qtd=count() by location".

Para configurar as requisições as downstream APIs em nome do usuário, precisaremos incluir no program.cs as extensões mostradas abaixo:

  • EnableTokenAcquisitionToCallDownstreamApi;
  • AddDownstreamWebApi;
  • AddInMemoryTokenCaches:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamWebApi("ResourceGraphAPI", builder.Configuration.GetSection("ResourceGraphAPI"))
    .AddInMemoryTokenCaches();

Para finalizar a parte do server, vamos apenas incluir uma nova seção de configuração ao appsettings.json, a ResourceGraphAPI. Basicamente estamos dizendo a URL da API e qual os scopes que serão requeridos para os access tokens que serão gerados dentro da nossa API.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<YOUR-DOMAIN>",
    "TenantId": "<YOUR-TENANT-ID>",
    "ClientId": "<YOUR-CLIENT-ID>",
    "Scopes": "Access.Server",
    "CallbackPath": "/signin-oidc",
    "ClientSecret": ""<YOUR-CLIENT-SECRET>""
  },
  "ResourceGraphAPI": {
    "BaseUrl": "https://management.azure.com/",
    "Scopes": "https://management.azure.com/user_impersonation"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Com isso fechamos o desenvolvimento do server.

Já na parte do client, vamos inicialmente criar a página CloudRegions.razor com o seguinte conteúdo:

@page "/cloudregions"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using ResourceGraphApp.Shared
@attribute [Authorize]
@inject HttpClient Http

<PageTitle>Cloud Regions</PageTitle>

<h1>Cloud Regions</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (_cloudRegion == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Region</th>
                <th>Qtd of Resources</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var cloudRegion in _cloudRegion.Data)
            {
                <tr>
                    <td>@cloudRegion.Location</td>
                    <td>@cloudRegion.Qtd</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private CloudRegion? _cloudRegion;

    protected override async Task OnInitializedAsync()
    {
        _cloudRegion = await Http.GetFromJsonAsync<CloudRegion>("CloudResources");
    }
}

Alguns detalhes sobre essa página:

  • Ela é protegida pelo atributo Authorize;
  • Ela é exibida quando chamamos a rota "/cloudregions";
  • Faz uma requisição à controladora criada anteriormente;
  • Exibe os dados retornados pela chamada HTTP em uma tabela;

Vamos modificar o MainLayout.razor para que essa nova rota fique disponível no menu do web app.

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">ResourceGraphApp</a>
        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
            <span class="navbar-toggler-icon"></span>
        </button>
    </div>
</div>

<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="cloudregions">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Cloud Regions
            </NavLink>
        </div>
    </nav>
</div>

@code {
    private bool collapseNavMenu = true;

    private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Feito essas modificações, podemos executar o projeto server com o comando donet run:

dotnet run

Com o browser aberto, vamos clicar em "Log in" e fazer o login no registro de aplicativo (client) criado anteriormente.

Caso você possua o MFA configurado para sua tenant, ou para usuários específicos, é aqui que você utilizará essa ferramenta.

Como estamos acessando o web app pela primeira vez, vamos consentir que a aplicação client acesse a aplicação server em nosso nome.

Navegando à rota "/cloudregions", temos o seguinte resultado!

Você já pode baixar o projeto por esse link, e não esquece de me seguir no LinkedIn!

Até a próxima, abraços!

💡
Podemos te ajudar com uma revisão 100% gratuita do seu ambiente cloud.
Share this post