Nos bastidores do Firewall de Privacidade de Dados

Observação

No momento, os níveis de privacidade não estão disponíveis nos fluxos de dados do Power Platform. A equipe de produtos está trabalhando para habilitar essa funcionalidade.

Se você tiver usado o Power Query por qualquer intervalo de tempo, provavelmente já experimentou isso. Lá está você, consultando, quando de repente recebe um erro que nenhuma quantidade de pesquisa online, ajustes de consulta ou toques no teclado pode remediar. Um erro como:

Formula.Firewall: Query 'Query1' (step 'Source') references other queries or steps, so it may not directly access a data source. Please rebuild this data combination.

Ou talvez:

Formula.Firewall: Query 'Query1' (step 'Source') is accessing data sources that have privacy levels which cannot be used together. Please rebuild this data combination.

Esses erros de Formula.Firewall são o resultado do Firewall de Privacidade de Dados (também conhecido como Firewall), que às vezes pode parecer que existe apenas para frustrar analistas de dados no mundo todo. Porém, acredite ou não, o Firewall serve a uma finalidade importante. Neste artigo, vamos nos aprofundar nos bastidores para entender melhor como funciona. Munido de maior compreensão, você poderá diagnosticar e corrigir melhor os erros de Firewall no futuro.

O que é isso?

A finalidade do Firewall de Privacidade de Dados é simples: ele existe para impedir que Power Query vazem dados sem querer entre fontes.

Por que isso é necessário? Você certamente poderia criar algum M que passaria um valor SQL para um feed OData. Mas isso seria vazamento intencional de dados. O autor da compilação saberia (ou pelo menos deveria) saber que estava fazendo isso. Por que então a necessidade de proteção contra vazamento de dados não intencional?

A resposta? Dobramento.

Dobramento?

Dobrar é um termo que se refere à conversão de expressões em M (como filtros, renomeações, junções e assim por diante) em operações em uma fonte de dados bruta (como SQL, OData e assim por diante). Grande parte do poder do Power Query é devido ao PQ, que pode converter as operações que um usuário executa por meio de sua interface do usuário em linguagens complexas de fonte de dados de back-end ou SQL, sem que o usuário precise conhecer esses idiomas. Os usuários obtêm o benefício de desempenho das operações de fonte de dados nativas, com a facilidade de uso de uma interface do usuário em que todas as fontes de dados podem ser transformadas usando um conjunto comum de comandos.

Como parte do dobramento, o PQ às vezes pode determinar que a maneira mais eficiente de executar uma compilação é pegar dados de uma fonte e passá-los para outra. Por exemplo, se você estiver juntando um pequeno arquivo CSV a uma tabela SQL enorme, provavelmente não quer que o PQ leia o arquivo CSV, leia a tabela SQL inteira e, em seguida, junte-os no computador local. Você provavelmente deseja que o PQ insira os dados CSV em uma instrução SQL e peça ao banco de dados SQL para realizar a junção.

É assim que o vazamento de dados não intencional pode acontecer.

Imagine se você estivesse ingressando em dados SQL que incluíam números de segurança social de funcionários com os resultados de um feed OData externo e, de repente, descobriu que os CPFs do SQL estavam sendo enviados para o serviço OData. Péssimo, não é?

Esse é o tipo de cenário que o Firewall pretende evitar.

Como funciona?

O Firewall existe para impedir que dados de uma fonte sejam enviados sem querer para outra fonte. Bastante simples.

Então, como ele realiza esta missão?

Ele faz isso dividindo suas consultas M em algo chamado “partições” e impondo a seguinte regra:

  • Uma partição pode acessar fontes de dados compatíveis ou referenciar outras partições, mas não ambas.

Simples... mas confuso. O que é uma partição? O que torna duas fontes de dados "compatíveis"? E por que o Firewall deve se importar se uma partição deseja acessar uma fonte de dados e referenciar uma partição?

Vamos detalhar isso e examinar a regra acima por etapas.

O que é uma partição?

Em seu nível mais básico, uma partição é apenas uma coleção de uma ou mais etapas de consulta. A partição mais granular possível (pelo menos na implementação atual) é uma única etapa. Partições maiores às vezes podem abranger várias consultas. (Mais informações sobre isso mais tarde.)

Caso você não esteja familiarizado com as etapas, pode exibi-las à direita da janela Editor do Power Query depois de selecionar uma consulta, no painel Etapas Aplicadas. As etapas acompanham tudo o que você fez para transformar seus dados em sua forma final.

Partições que fazem referência a outras partições

Quando uma consulta é avaliada com o Firewall ativado, o Firewall divide a consulta e todas as suas dependências em partições (ou seja, grupos de etapas). Sempre que uma partição faz referência a algo em outra partição, o Firewall substitui a referência por uma chamada a uma função especial chamada Value.Firewall. Em outras palavras, o Firewall não permite que partições acessem umas às outras diretamente. Todas as referências são modificadas para percorrer o Firewall. Pense no Firewall como um porteiro. Uma partição que faz referência a outra partição deve obter a permissão do Firewall para fazer isso e o Firewall controla se os dados referenciados serão permitidos ou não na partição.

Isso tudo pode parecer abstrato, então vamos dar uma olhada em um exemplo.

Suponha que você tenha uma consulta chamada Funcionários, que extrai alguns dados de um banco de dados SQL. Suponha que você também tenha outra consulta (Referência de Funcionários), que simplesmente faz referência aos Funcionários.

shared Employees = let
    Source = Sql.Database(…),
    EmployeesTable = …
in
    EmployeesTable;

shared EmployeesReference = let
    Source = Employees
in
    Source;

Essas consultas acabarão divididas em duas partições: uma para a consulta Funcionários e outra para a consulta Referência de Funcionários (que fará referência à partição Funcionários). Quando avaliadas com o Firewall ativado, essas consultas serão reescritas da seguinte maneira:

shared Employees = let
    Source = Sql.Database(…),
    EmployeesTable = …
in
    EmployeesTable;

shared EmployeesReference = let
    Source = Value.Firewall("Section1/Employees")
in
    Source;

Observe que a referência simples à consulta Funcionários foi substituída por uma chamada para Value.Firewall, que é fornecida com o nome completo da consulta Funcionários.

Quando Referência de Funcionários é avaliada, a chamada para Value.Firewall("Section1/Employees") é interceptada pelo Firewall, que agora tem a chance de controlar se (e como) os dados solicitados fluem para a partição EmployeesReference. Ele pode fazer qualquer número de coisas: negar a solicitação, armazenar em buffer os dados solicitados (o que impede que qualquer dobra adicional para sua fonte de dados original ocorra) e assim por diante.

É assim que o Firewall mantém o controle sobre os dados que fluem entre partições.

Partições que acessam diretamente fontes de dados

Digamos que você defina uma consulta Query1 com uma etapa (observe que essa consulta de etapa única corresponde a uma partição do Firewall) e que essa única etapa acesse duas fontes de dados: uma tabela de banco de dados SQL e um arquivo CSV. Como o Firewall lida com isso, já que não há referência de partição e, portanto, nenhuma chamadaValue.Firewall para interceptar? Vamos examinar a regra anterior:

  • Uma partição pode acessar fontes de dados compatíveis ou referenciar outras partições, mas não ambas.

Para que a consulta de fontes de dados que tem partição única e duas fontes de dados tenha permissão para ser executada, suas duas fontes de dados devem ser "compatíveis". Em outras palavras, é preciso que os dados sejam compartilhados bidirecionalmente entre eles. Isso significa que os níveis de privacidade de ambas as fontes precisam ser Públicos, ou ambas serem Organizacionais, pois essas são as duas únicas combinações que permitem o compartilhamento em ambas as direções. Se ambas as fontes estiverem marcadas como Privadas, ou uma estiver marcada como Pública e outra como Organizacional, ou se estiverem marcadas usando alguma outra combinação de níveis de privacidade, o compartilhamento bidirecional não será permitido e, portanto, não será seguro que ambas sejam avaliadas na mesma partição. Isso significaria que o vazamento de dados não seguro poderia ocorrer (devido à dobra) e o Firewall não teria como impedi-lo.

O que acontece se você tentar acessar fontes de dados incompatíveis na mesma partição?

Formula.Firewall: Query 'Query1' (step 'Source') is accessing data sources that have privacy levels which cannot be used together. Please rebuild this data combination.

Espero que agora você entenda melhor uma das mensagens de erro listadas no início deste artigo.

Observe que esse requisito de compatibilidade só se aplica dentro de uma determinada partição. Se uma partição estiver fazendo referência a outras partições, as fontes de dados das partições referenciadas não precisarão ser compatíveis entre si. Isso ocorre porque o Firewall pode armazenar os dados em buffer, o que impedirá qualquer dobra adicional na fonte de dados original. Os dados serão carregados na memória e tratados como se fossem provenientes do nada.

Por que não fazer as duas coisas?

Digamos que você defina uma consulta com uma etapa (que corresponderá novamente a uma partição) que acessa duas outras consultas (ou seja, duas outras partições). E se você quiser, na mesma etapa, também acessar diretamente um banco de dados SQL? Por que uma partição não pode fazer referência a outras partições e acessar fontes de dados compatíveis diretamente?

Como você viu anteriormente, quando uma partição faz referência a outra partição, o Firewall atua como o porteiro para todos os dados que fluem para a partição. Para fazer isso, ele deve ser capaz de controlar em quais dados são permitidos. Se houver fontes de dados sendo acessadas dentro da partição e dados fluindo de outras partições, a partição perde sua capacidade de ser o gatekeeper, uma vez que os dados que fluem podem ser vazados para uma das fontes de dados acessadas internamente sem que ela saiba. Assim, o Firewall impede que uma partição que acessa outras partições tenha permissão para acessar diretamente quaisquer fontes de dados.

Então, o que acontece se uma partição tentar referenciar outras partições e também acessar diretamente fontes de dados?

Formula.Firewall: Query 'Query1' (step 'Source') references other queries or steps, so it may not directly access a data source. Please rebuild this data combination.

Agora, vamos entender a outra mensagem de erro listada no início deste artigo.

Partições detalhadas

Como você provavelmente pode adivinhar pelas informações acima, como as consultas são particionadas acaba sendo incrivelmente importante. Se você tiver algumas etapas que estão fazendo referência a outras consultas e outras etapas que acessam fontes de dados, agora você espera reconhecer que desenhar os limites de partição em determinados locais causará erros de Firewall, ao mesmo tempo que desenhá-los em outros locais permitirá que sua consulta seja executada muito bem.

Então, como exatamente as consultas são particionadas?

Esta seção é provavelmente a mais importante para entender por que você está vendo erros de Firewall e entender como resolvê-los (quando possível).

Aqui está um resumo de alto nível da lógica de particionamento.

  • Particionamento inicial
    • Cria uma partição para cada etapa em cada consulta
  • Fase Estática
    • Essa fase não depende dos resultados da avaliação. Em vez disso, ela se baseia em como as consultas são estruturadas.
      • Corte de parâmetro
        • Corta partições por parâmetro, ou seja, qualquer uma que:
          • Não faz referência a nenhuma outra partição
          • Não contém invocações de função
          • Não é cíclica (ou seja, não se refere a si mesma)
        • Observe que "remover" uma partição inclui em qualquer outra partição que a referencie.
        • Cortar partições de parâmetro permite que as referências de parâmetro usadas nas chamadas de função de fonte de dados (por exemplo, Web.Contents(myUrl)) funcionem, em vez de gerar erros "a partição não pode fazer referência a fontes de dados e outras etapas".
      • Agrupamento (estático)
        • As partições são mescladas em ordem de dependência de baixo para cima. Nas partições mescladas resultantes, o seguinte será separado:
          • Partições em consultas diferentes
          • Partições que não fazem referência a outras partições (e, portanto, têm permissão para acessar uma fonte de dados)
          • Partições que fazem referência a outras partições (e, portanto, estão proibidas de acessar uma fonte de dados)
  • Fase Dinâmica
    • Essa fase depende dos resultados da avaliação, incluindo informações sobre fontes de dados acessadas por várias partições.
    • Filtragem
      • Corta partições que atendem a todos os seguintes requisitos:
        • Não acessa nenhuma fonte de dados
        • Não faz referência a nenhuma partição que acesse fontes de dados
        • Não é cíclica
    • Agrupamento (dinâmico)
      • Agora que partições desnecessárias foram cortadas, tente criar partições de origem tão grandes quanto puder. Para isso, as partições são mescladas usando as mesmas regras descritas na fase de agrupamento estático acima.

O que isso tudo significa?

Vamos ver um exemplo para ilustrar como funciona a lógica complexa apresentada acima.

Veja abaixo um cenário de exemplo. É uma mesclagem bastante simples de um arquivo de texto (Contatos) com um banco de dados SQL (Funcionários), em que o SQL Server é um parâmetro (DbServer).

As três consultas

Aqui está o código M para as três consultas usadas neste exemplo.

shared DbServer = "MySqlServer" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true];
shared Contacts = let

    Source = Csv.Document(File.Contents("C:\contacts.txt"),[Delimiter="   ", Columns=15, Encoding=1252, QuoteStyle=QuoteStyle.None]),

    #"Promoted Headers" = Table.PromoteHeaders(Source, [PromoteAllScalars=true]),

    #"Changed Type" = Table.TransformColumnTypes(#"Promoted Headers",{{"ContactID", Int64.Type}, {"NameStyle", type logical}, {"Title", type text}, {"FirstName", type text}, {"MiddleName", type text}, {"LastName", type text}, {"Suffix", type text}, {"EmailAddress", type text}, {"EmailPromotion", Int64.Type}, {"Phone", type text}, {"PasswordHash", type text}, {"PasswordSalt", type text}, {"AdditionalContactInfo", type text}, {"rowguid", type text}, {"ModifiedDate", type datetime}})

in

    #"Changed Type";
shared Employees = let

    Source = Sql.Databases(DbServer),

    AdventureWorks = Source{[Name="AdventureWorks"]}[Data],

    HumanResources_Employee = AdventureWorks{[Schema="HumanResources",Item="Employee"]}[Data],

    #"Removed Columns" = Table.RemoveColumns(HumanResources_Employee,{"HumanResources.Employee(EmployeeID)", "HumanResources.Employee(ManagerID)", "HumanResources.EmployeeAddress", "HumanResources.EmployeeDepartmentHistory", "HumanResources.EmployeePayHistory", "HumanResources.JobCandidate", "Person.Contact", "Purchasing.PurchaseOrderHeader", "Sales.SalesPerson"}),

    #"Merged Queries" = Table.NestedJoin(#"Removed Columns",{"ContactID"},Contacts,{"ContactID"},"Contacts",JoinKind.LeftOuter),

    #"Expanded Contacts" = Table.ExpandTableColumn(#"Merged Queries", "Contacts", {"EmailAddress"}, {"EmailAddress"})

in

    #"Expanded Contacts";

Aqui está uma exibição de nível superior, mostrando as dependências.

Diálogo de dependências da consulta.

Vamos particionar

Vamos ampliar um pouco e incluir etapas na imagem e começar a percorrer a lógica de particionamento. Aqui está um diagrama das três consultas, mostrando as partições de firewall iniciais em verde. Observe que cada etapa começa em sua própria partição.

Partições iniciais de firewall.

Em seguida, cortamos partições de parâmetro. Assim, o DbServer é incluído implicitamente na partição de origem.

Partições cortadas de firewall.

Agora, executamos o agrupamento estático. Isso mantém a separação entre partições em consultas separadas (observe, por exemplo, que as duas últimas etapas de Funcionários não são agrupadas com as etapas de Contatos) e entre partições que fazem referência a outras partições (como as duas últimas etapas de Funcionários) e aquelas que não fazem (como as três primeiras etapas de Funcionários).

Partições de firewall pós-agrupamento estático.

Agora, entramos na fase dinâmica. Nesta fase, as partições estáticas acima são avaliadas. As partições que não acessam nenhuma fonte de dados são cortadas. As partições são agrupadas para criar partições de origem tão grandes quanto for possível. No entanto, neste cenário de exemplo, todas as partições restantes acessam fontes de dados e não há nenhum agrupamento adicional que possa ser feito. Portanto, as partições de nosso exemplo não serão alteradas durante essa fase.

Vamos supor

Porém, por uma questão de ilustração, vamos examinar o que aconteceria se a consulta Contatos, em vez de vir de um arquivo de texto, fosse codificada no M (talvez por meio da caixa de diálogo Inserir dados).

Nesse caso, a consulta Contatos não acessaria nenhuma fonte de dados. Assim, ela seria cortada durante a primeira parte da fase dinâmica.

Partição de firewall após o corte de fase dinâmica.

Com a partição Contatos removida, as duas últimas etapas dos Funcionários não referenciam mais nenhuma partição, exceto aquela que contém as três primeiras etapas dos Funcionários. Assim, as duas partições seriam agrupadas.

A partição resultante ficaria assim.

Partições finais de firewall.

Exemplo: passando dados de uma fonte de dados para outra

Certo, chega dessa explicação abstrata. Vamos examinar um cenário comum em que é provável que você encontre um erro de Firewall e as etapas para resolvê-lo.

Imagine que você deseja pesquisar um nome de empresa do serviço Northwind OData e, em seguida, usar o nome da empresa para executar uma pesquisa do Bing.

Primeiro, você cria uma consulta da Empresa para recuperar o nome da empresa.

let
    Source = OData.Feed("https://services.odata.org/V4/Northwind/Northwind.svc/", null, [Implementation="2.0"]),
    Customers_table = Source{[Name="Customers",Signature="table"]}[Data],
    CHOPS = Customers_table{[CustomerID="CHOPS"]}[CompanyName]
in
    CHOPS

Em seguida, você cria uma consulta de Pesquisa que faz referência à Empresa e a passa para o Bing.

let
    Source = Text.FromBinary(Web.Contents("https://www.bing.com/search?q=" & Company))
in
    Source

Neste momento, não há problemas. Avaliar a Pesquisa produz um erro de Firewall.

Formula.Firewall: Query 'Search' (step 'Source') references other queries or steps, so it may not directly access a data source. Please rebuild this data combination.

Isso ocorre porque a etapa de origem da Pesquisa está fazendo referência a uma fonte de dados (bing.com) e também fazendo referência a outra consulta/partição (Empresa). Está violando a regra mencionada acima ("uma partição pode acessar fontes de dados compatíveis ou fazer referência a outras partições, mas não a ambas").

O que fazer? Uma opção é desabilitar completamente o Firewall (por meio da opção Privacidade rotulada Ignorar os Níveis de Privacidade e potencialmente melhorar o desempenho). Mas e se você quiser deixar o Firewall habilitado?

Para resolver o erro sem desabilitar o Firewall, você pode combinar Empresa e Pesquisa em uma única consulta, desta forma:

let
    Source = OData.Feed("https://services.odata.org/V4/Northwind/Northwind.svc/", null, [Implementation="2.0"]),
    Customers_table = Source{[Name="Customers",Signature="table"]}[Data],
    CHOPS = Customers_table{[CustomerID="CHOPS"]}[CompanyName],
    Search = Text.FromBinary(Web.Contents("https://www.bing.com/search?q=" & CHOPS))
in
    Search

Tudo está acontecendo dentro de uma única partição. Supondo que os níveis de privacidade das duas fontes de dados sejam compatíveis, o Firewall agora deverá estar feliz e você não receberá mais um erro.

É isso

Embora haja muito mais que poderia ser dito sobre este tópico, este artigo introdutório já é longo o suficiente. Espero que ele tenha dado a você uma melhor compreensão do Firewall e ajude a entender e corrigir erros de Firewall quando encontrá-los no futuro.