Compartilhar via


Nos bastidores do Firewall de Privacidade de Dados

Observação

Atualmente, os níveis de privacidade estão indisponíveis nos fluxos de dados do Power Platform, mas a equipe do produto está trabalhando para habilitar essa funcionalidade.

Se você usou o Power Query por qualquer período de tempo, provavelmente o experimentou. Lá está você, executando consultas, quando de repente recebe um erro que nenhuma quantidade de pesquisa online, ajuste de consulta ou batendo no teclado pode resolver. 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 Formula.Firewall erros são resultado do Firewall de Privacidade de Dados do Power Query (também conhecido como Firewall), que às vezes pode parecer que ele existe apenas para frustrar analistas de dados em todo o mundo. Acredite ou não, no entanto, o Firewall serve a uma finalidade importante. Neste artigo, nos aprofundamos nos bastidores para entender melhor como ele funciona. Com 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 o Power Query vaze dados sem querer entre fontes.

Por que isso é necessário? Quero dizer, você certamente poderia criar um código M que transferisse um valor SQL para um feed OData. Mas isso seria vazamento intencional de dados. O autor do mashup saberia (ou pelo menos deveria) que eles estavam fazendo isso. Por que, em seguida, a necessidade de proteção contra vazamento de dados não intencional?

A resposta? Dobragem.

Dobradura?

Folding é 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 sobre uma fonte de dados não processados (como SQL, OData e assim por diante). Uma grande parte do poder do Power Query vem do fato de que o Power Query 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 da otimização, o Power Query às vezes pode determinar que a maneira mais eficiente de executar um determinado mashup é obter dados de uma fonte e transferi-los para outra. Por exemplo, se você estiver juntando um pequeno arquivo CSV a uma tabela SQL enorme, provavelmente não deseja que o Power Query leia o arquivo CSV, leia a tabela SQL inteira e, em seguida, junte-as em seu computador local. Você provavelmente deseja que o Power Query integre os dados CSV em uma instrução SQL e peça ao banco de dados SQL para executar a junção.

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

Imagine se você estivesse combinando dados SQL que incluíam Números de Segurança Social de funcionários aos resultados de um feed OData externo e de repente descobriu que os Números de Segurança Social do SQL estavam sendo enviados para o serviço OData. Más notícias, certo?

Esse é o tipo de cenário que o Firewall se destina a evitar.

Como funciona?

O Firewall existe para impedir que os dados de uma fonte sejam enviados involuntariamente para outra fonte. Simples o bastante.

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

Ele faz isso dividindo suas consultas M em algo chamado partições e aplicando 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 analisar isso e dar uma olhada na regra anterior passo a passo.

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. As maiores partições às vezes podem abranger várias consultas. (Mais informações sobre isso posteriormente.)

Se você não estiver familiarizado com as etapas, poderá exibi-las à direita da janela do Editor do Power Query depois de selecionar uma consulta, no painel Etapas Aplicadas . As etapas acompanham tudo o que você faz 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 para 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 passar pelo Firewall. Pense no Firewall como um gatekeeper. 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 são permitidos ou não na partição.

Isso tudo pode parecer bastante abstrato, então vamos examinar 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 (EmployeesReference), 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 acabam divididas em duas partições: uma para a consulta Employees e outra para a consulta EmployeesReference (que faz referência à partição Employees). Quando avaliadas com o Firewall ativado, essas consultas sã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 é substituída por uma chamada para Value.Firewall, à qual é fornecido o nome completo da consulta Funcionários.

Quando EmployeesReference é avaliado, o Firewall intercepta a chamada para Value.Firewall("Section1/Employees"), que agora tem a chance de controlar se (e como) os dados solicitados fluem para a partição EmployeesReference. Ele pode fazer várias 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 acessa 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 chamada para o Value.Firewall interceptar? Vamos examinar a regra declarada anteriormente:

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

Para que a consulta de uma única partição, mas com duas fontes de dados, seja autorizada a executar, as duas fontes de dados devem ser "compatíveis". Em outras palavras, é necessário que os dados possam ser compartilhados de forma bidirecional entre elas. Isso significa que os níveis de privacidade de ambas as fontes precisam ser públicos ou ambos ser 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 uma estiver marcada como Organizacional ou estiver marcada usando alguma outra combinação de níveis de privacidade, o compartilhamento bidirecional não será permitido. Não é seguro que ambos sejam avaliados 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.

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 impede qualquer dobra adicional na fonte de dados original. Os dados são carregados na memória e tratados como se viessem do nada.

Por que não fazer as duas coisas?

Digamos que você defina uma consulta com uma etapa (que corresponde novamente a uma partição) que acessa duas outras consultas (ou seja, duas outras partições). E se você quisesse, 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 diretamente fontes de dados compatíveis?

Como você viu anteriormente, quando uma partição faz referência a outra partição, o Firewall atua como o gatekeeper para todos os dados que fluem para a partição. Para fazer isso, ele deve ser capaz de controlar quais dados são permitidos. Se houver fontes de dados sendo acessadas dentro da partição e dados fluindo de outras partições, ela perderá sua capacidade de ser o gatekeeper, pois os dados que fluem podem ser vazados para uma das fontes de dados acessadas internamente sem que ele 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 você espera entender melhor a outra mensagem de erro listada no início deste artigo.

Partições detalhadas

Como você provavelmente pode adivinhar das informações anteriores, a forma como as consultas são particionadas é 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 causa erros de Firewall, enquanto desenhá-las em outros locais permite 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 (sempre que 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, ele depende de como as consultas são estruturadas.
      • Corte de parâmetro
        • Remove partições semelhantes a parâmetros, 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íclico (ou seja, não se refere a si mesmo)
        • Observe que "remover" uma partição efetivamente inclui-a em qualquer outra partição que a referencie.
        • Cortar partições de parâmetro permite que referências de parâmetro usadas em chamadas de função de fonte de dados (por exemplo) Web.Contents(myUrl)funcionem, em vez de gerar erros de "partição não pode referenciar fontes de dados e outras etapas".
      • Agrupamento (estático)
        • As partições são mescladas na ordem de dependência de baixo para cima. Nas partições mescladas resultantes, as seguintes são separadas:
          • 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, sã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.
    • Aparagem
      • Corta partições que atendem a todos os seguintes requisitos:
        • Não acessa nenhuma fonte de dados
        • Não faz referência a partições que acessam fontes de dados
        • Não é cíclico
    • Agrupamento (dinâmico)
      • Agora que as partições desnecessárias são cortadas, tente criar partições de origem tão grandes quanto possível. Essa criação é feita mesclando as partições usando as mesmas regras descritas na fase de agrupamento estático anterior.

O que tudo isso significa?

Vamos percorrer um exemplo para ilustrar como a lógica complexa estabelecida anteriormente funciona.

Aqui está 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

Este é 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 visão geral, mostrando as dependências.

Caixa de diálogo Dependências de 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.

Diagrama mostrando as partições de firewall iniciais.

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

Diagrama mostrando as partições de firewall cortadas.

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

Diagrama mostrando as partições de firewall após o 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. Em seguida, as partições são agrupadas para criar partições de origem tão grandes quanto 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. As partições em nosso exemplo, portanto, não são alteradas durante essa fase.

Vamos fingir

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

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

Diagrama mostrando a partição de firewall após o corte dinâmico da fase.

Com a partição Contatos removida, as duas últimas etapas dos Funcionários não mais fariam referência a 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 teria esta aparência.

Diagrama mostrando as partições de firewall finais.

Exemplo: passando dados de uma fonte de dados para outra

Ok, chega de 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 no 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 ponto, você tem problemas. Avaliar Search 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.

Esse erro 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). Ele está violando a regra mencionada anteriormente ("uma partição pode acessar fontes de dados compatíveis ou referenciar outras partições, mas não 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 Pesquisar 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

Agora 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 deve ser feliz e você não receberá mais um erro.

É um encerramento.

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