Alice

Autorização com OPA: Open Policy Agent

Como os times de engenharia da Alice trabalharam para lidar com os problemas relativos a Autorização e como a solução que escolhemos nos ajuda a resolvê-los.

Autorização com OPA: Open Policy Agent

Tamanho do texto

A Alice é uma empresa de tecnologia com o propósito de tornar o mundo mais saudável. Mas, como toda empresa de tecnologia, precisa lidar com desafios comuns e decidir como vai endereçá-los. Este artigo explica um pouco dos problemas relativos a Autorização que enfrentamos, passa pelas alternativas que consideramos e mostra como a solução que escolhemos resolve esses problemas (e inevitavelmente acaba criando outros).

O que é Autorização? Qual a diferença de Autenticação?

Antes de entrar nos detalhes do tema, é importante relembrar a diferença entre Autenticação e Autorização, dois conceitos que muitas vezes andam juntos mas são fundamentalmente diferentes.

Em resumo, Autenticação é o processo de Identificação de um usuário, que garante que ele é quem diz ser. Já Autorização é o processo que indica se um usuário identificado (ou não) tem a permissão para realizar determinadas ações dentro do sistema.

Soluções e Frameworks que temos disponíveis para usar podem lidar com um ou ambos aspectos, e é importante saber disso porque pode inviabilizar uma escolha. Você pode, por exemplo, ser obrigado a usar o aspecto de Autenticação da solução para que o aspecto de Autorização funcione e, dependendo do caso (como foi na Alice), isso pode não ser desejado.

Tipos comuns de Autorização

Temos alguns tipos diferentes de Autorização, que cobrem a maior parte das necessidades comuns dos sistemas de mercado. Aqui descrevo os principais:

  • Role-Based Access Control (RBAC): o tipo mais comum de controle de acesso, consiste em atribuir papéis (roles) aos usuários e definir o que cada papel pode fazer. A ideia é que papéis possam ser atribuídos ou revogados a qualquer momento, e o sistema possa mudar seu comportamento imediatamente após os papéis mudarem. Os papéis costumam ser uma informação curta, que pode ser facilmente trafegada dentro de Tokens JWT por conveniência.
    • Exemplo: Joãozinho assume o papel de analista de Operações e ganha a capacidade de editar quaisquer Membros (como chamamos os pacientes da Alice) no sistema.
  • Attribute-Based Access Control (ABAC): controle de acesso baseado em atributos do usuário e / ou do recurso sendo acessado.
    • Exemplo: Joãozinho pode editar um Membro do sistema se seu atributo do usuário isAdmin = true, ou se o membro tem o atributo isPublic = true.
  • Relationship-Based Access Control (ReBAC): caso específico de ABAC onde uma permissão é concedida se há um relacionamento específico entre o usuário e o recurso sendo acessado.
    • Exemplo: Joãozinho pode editar os dados de um Membro se o registro representar ele mesmo ou um dependente dele.

Seu sistema pode ter necessidades de Autorização em qualquer um dos níveis. No caso, as regras que a Alice precisa suportar são RBAC em sua maioria, com alguns casos de ABAC e ReBAC. Isso significa que a solução escolhida precisa também suportar esses modelos de autorização; somente RBAC não é suficiente (ou envolveria muito improviso para fazer).

Nossa abordagem original de Autorização

Sendo uma empresa de saúde, a Alice evidentemente tem muito contato com informações pessoais e de saúde. Portanto, um mecanismo de Autorização é essencial para garantir que dados não sejam acessados ou modificados por pessoas não autorizadas, sejam médicos, enfermeiros ou até o time de atendimento operacional.

Inicialmente optamos por manter a camada de Autorização junto à nossa camada compartilhada de acesso a dados (Data-Layer), que já realiza outras atribuições, como tokenização dos registros, de-identificação e afins. Através de uma DSL fluida, avaliamos quatro parâmetros essenciais para tomar uma decisão de Autorização:

  • Root Service: o serviço do qual se originou a solicitação. Isso é importante pois um mesmo usuário pode ter permissões distintas em sistemas diferentes.
  • Subject: o usuário atual.
  • Resource: o recurso que se deseja acessar.
  • Action: a ação que o usuário deseja realizar no recurso. No contexto de acesso a dados, consideramos as ações CRUD (Create, Read, Update, Delete).

Também partimos do princípio de “Least Privilege”, o que significa que qualquer acesso precisa necessariamente ser liberado via a escrita de Policies, ou regras de autorização, na DSL supracitada. Caso contrário, o acesso por padrão é negado. Essas regras são deployadas junto ao Data-Layer, e toda alteração das mesmas exige uma nova versão em produção para ser efetiva e impactar os usuários finais.

Problemas

A abordagem que inicialmente escolhemos nos permite grande liberdade na tomada de decisões de Autorização, mas vem com os seguintes contrapontos:

  • Acoplamento com o Data-Layer: não podemos alterar policies sem redeployar o serviço; regras de certos domínios acessam diretamente dados de outros domínios sem passar pelo serviço correspondente, quebrando o encapsulamento. Gera acoplamento da própria camada de dados com os modelos servidos por ela.
  • Testes: não é trivial escrever testes das policies e é comum haver erros em tempo de desenvolvimento descobrindo que esquecemos de incluir uma policy necessária.
  • Isolamento (“siloing”): a alteração de policies é altamente técnica e só pode ser feita por desenvolvedores back-end. Não há UI ou facilitador para isso.
  • Escopo: limitado somente à camada de acesso a dados; isso significa que, com esse mecanismo, não temos capacidade de tomar decisões de exibição de itens visuais no front-end de acordo com o perfil do usuário, por exemplo.

Com esses aspectos em mente, olhamos para o mercado procurando por uma solução de Autorização que suportasse as nossas necessidades, ao mesmo tempo que mitigasse a maior parte possível desses problemas.

Alternativas consideradas

Um ponto importante na hora de elencar a solução: não tínhamos a intenção de mudar o mecanismo de Autenticação utilizado atualmente, o que significa que soluções que fazem a “venda casada” com Autorização (por exemplo, keycloak e aserto) não resolveriam nosso problema.

Assim sendo, encontramos duas alternativas mais em linha com o que buscávamos:

Após uma análise minuciosa, preferimos seguir com o OPA, em vez do Oso Cloud, devido a alguns fatores:

  • o Oso é menos flexível e a migração das nossas policies atuais seria mais trabalhosa.
  • o Oso é um SaaS e tem um custo associado; com nosso volume de requests esse custo pode ser bem significativo.
  • o Oso tem uma UI para edição de policies, mas para escrever ABAC / ReBAC é preciso escrever as regras na linguagem Polar.

Por outro lado, o OPA tem várias características que o tornam uma boa opção para a gente. É open-source, utilizado por algumas grandes empresas, flexível e orientado à nuvem. É escrito em Golang, altamente performático e capaz de responder a um grande volume de requests de Autorização na casa dos milissegundos – característica essencial para nosso caso, pois teríamos que externalizar as chamadas de autorização, já pagando o custo de latência de rede.

Outra característica que tornava o OPA mais atrativo era que as regras são escritas na linguagem Rego , que apesar de não ser tão simples é flexível o bastante para implementar RBAC, ABAC, ReBAC e outros tipos de controle de acesso. As regras e dados de autorização são alimentados no serviço e ficam disponíveis em memória, e podem ser substituídos em tempo de execução.

Além disso, o usuário do OPA é quem define o formato do input (os dados de autorização relativos a um dado request) e o formato do output (quais permissões são calculadas e retornadas), então ele pode ser usado em mais de um contexto.

Além de tudo isso, há um forte ecossistema em torno do OPA, com várias ferramentas construídas para facilitar sua adoção em escala, e até mesmo uma versão Enterprise.

Como o OPA endereça nossos problemas

  • Acoplamento com o Data-Layer: o deploy de novas policies é feito diretamente no OPA e não precisa nem redeployar o serviço, pois ele tem suporte a hot-reload (apontando para um bucket do S3, por exemplo). O OPA pode também ter uma base de dados de Autorização, que torna mais simples a implementação de ReBAC em vez de ficar buscando esses dados em real-time no momento do request.
  • Testes: toda policy pode e deve ser testada unitariamente; os testes são rápidos e podem ser incluídos na sua pipe de CI. Em termos de garantir que os dados esperados são efetivamente enviados para o OPA, essa responsabilidade fica com os testes do seu sistema.
  • Isolamento (“siloing”): infelizmente o OPA não resolve este ponto muito bem, pois é necessário escrever código e não há UI que facilite o processo. Mas um ponto positivo é que quem cuida das policies não precisa ser o mesmo time dono dos dados, pode ser um time focado nisso.
  • Escopo: dependendo de como forem escritas as regras, pode-se usar um mesmo servidor do OPA para atender a requests de Autorização de várias fontes. Na Alice, conseguimos escrever de uma forma que a mesma regra calcula permissões para o front-end (quando tenho dados somente do usuário acessando) e para o back-end (quando tenho as demais informações de recurso e ação, por exemplo).

Conclusão

Apesar de ser um problema bastante comum no desenvolvimento de sistemas, pode ser desafiador encontrar uma solução de Autorização que case bem com as suas necessidades. É importante estudar com calma as alternativas, realizar Provas de Conceito em escopos menores e ter um bom plano de migração quando já há bastante coisa construída no esquema atual.

Esse é o tipo de problema que faz muito sentido ser endereçado pelo time de Plataforma, pois causa um grande impacto ao longo de todas as camadas de desenvolvimento, e precisa de grande apoio das lideranças para efetivamente sair do papel.

——————————————————————————————————-

Alice is a technology company with the mission of making the world healthier. But like any tech company, it faces common challenges and must decide how to address them. This article explains some of the authorization-related problems we encountered, explores the alternatives we considered, and shows how the solution we chose solves these problems (while inevitably creating others as well ).

What is Authorization? How is it Different from Authentication?

Before diving into the details, it’s important to recall the difference between Authentication and Authorization—two concepts that often go together but are fundamentally different.

In short, Authentication is the process of identifying a user, ensuring that they are who they claim to be. Authorization, on the other hand, is the process of determining whether an identified (or even unidentified) user has permission to perform certain actions within the system.

The solutions and frameworks available may handle one or both aspects, which is important to consider when making a choice. You might, for example, be forced to use a solution’s Authentication component just to make its Authorization component work. Depending on the case (as was true for Alice), this may not be desirable.

Common Types of Authorization

There are several types of Authorization, covering most of the common needs of modern systems. Here are the main ones:

  • Role-Based Access Control (RBAC): The most common type of access control, in which roles are assigned to users, defining what each role can do. Roles can be assigned or revoked at any time, and the system adjusts behavior accordingly.
    • Example: Bob is assigned the role of Operations Analyst and gains the ability to edit any Member (as we call the patients here in Alice) in the system.
  • Attribute-Based Access Control (ABAC): Access control based on user attributes and/or the attributes of the resource being accessed.
    • Example: Bob can edit a Member in the system if his user attribute isAdmin = true, or if the Member has the attribute isPublic = true.
  • Relationship-Based Access Control (ReBAC): A specific case of ABAC where permission is granted based on a specific relationship between the user and the accessed resource.
    • Example: Bob can edit a Member’s data if the record represents himself or one of his dependents.

A system may require Authorization at any of these levels. In Alice’s case, most rules follow RBAC, with some cases requiring ABAC and ReBAC. This means the chosen solution must support these Authorization models—RBAC alone wouldn’t be sufficient (or would require excessive workarounds).

Our Initial Approach to Authorization

As a healthcare company, Alice deals extensively with personal and medical information, making a robust Authorization mechanism essential to ensure that data is accessed or modified only by authorized individuals—whether doctors, nurses, or operational support teams.

Initially, we decided to implement Authorization within our shared Data-Layer, which already handled tasks like tokenizing records, de-identifying data, and other privacy and security measures. Using a fluid Domain Specific Language (code homebrewed for this purpose), we evaluated four key parameters to make Authorization decisions:

  • Root service: The service where the request originated (important because a user might have different permissions in different systems).
  • Subject: The current user.
  • Resource: The resource being accessed.
  • Action: The action the user wants to perform on the resource (CRUD: Create, Read, Update, Delete).

We also followed the Least Privilege principle, meaning access must be explicitly granted via written Policies in the DSL—otherwise, access is denied by default. These rules were deployed alongside the Data-Layer, requiring a new production release for any changes to take effect.

Problems

While this approach gave us flexibility in Authorization decisions, it also introduced some drawbacks:

  • Tight coupling with the Data-Layer: Policies could not be changed without redeploying the service; some domain rules accessed data from other domains directly, breaking encapsulation.
  • Testing challenges: Writing tests for policies wasn’t trivial, leading to frequent runtime errors when missing a necessary policy.
  • Expertise: Policy modifications were highly technical and could only be done by backend developers—there was no UI or other tooling to facilitate this.
  • Scope limitations: The mechanism only covered data access, meaning we couldn’t use it for UI-based authorization (e.g., controlling which UI elements should be displayed based on a user’s role).

With these challenges in mind, we looked for an Authorization solution that met our needs while mitigating as many of these issues as possible.

Considered Alternatives

A key requirement in our search: we had no intention of changing our existing Authentication mechanism. This meant solutions that bundled Authentication and Authorization together (such as Keycloak or Aserto) wouldn’t work for us.

Instead, we found two alternatives that better aligned with our needs:

After a thorough analysis, we decided to go with OPA instead of Oso Cloud for several reasons:

  • Oso is less flexible, and migrating our existing policies would be more complex.
  • Oso is a SaaS solution with associated costs, which could be significant given our request volume.
  • Oso provides a UI for policy editing, but writing ABAC/ReBAC rules still requires using the Polar language.

On the other hand, OPA has several characteristics that make it a great option for us. It is open-source, used by some large companies, flexible, and cloud-oriented. It is written in Golang, highly performant, and capable of handling a high volume of authorization requests in milliseconds—an essential feature for our case since we needed to externalize authorization calls while already accounting for network latency costs.

Policies are written in the Rego language, which, although not the simplest, is flexible enough to implement RBAC, ABAC, ReBAC, and other access control models. Authorization rules and data are fed into the service, stored in memory, and can be replaced at runtime.

OPA users define the input format (the authorization data related to a given request) and the output format (which permissions are calculated and returned), allowing it to be used in multiple contexts.

Beyond all this, OPA has a strong ecosystem, with various tools built to facilitate large-scale adoption and even an Enterprise version.

How OPA Solves Our Problems

  • Decoupling from the Data-Layer: Policies are deployed directly to OPA, eliminating the need to redeploy the service. OPA also supports an Authorization database, making it easier to implement ReBAC without real-time data lookups.
  • Improved Testing: Policies can and should be unit tested, with fast execution that integrates into CI pipelines. Ensuring the right data reaches OPA remains the responsibility of system tests.
  • Less Expertise: While OPA still requires writing code (lacking a UI for policy management), policy ownership and authoring can be assigned to a dedicated team rather than the teams managing the data itself.
  • Expanded Scope: A single OPA instance can handle Authorization requests from multiple sources. At Alice, we structured our policies so the same rule could determine permissions for both the frontend (based on user attributes) and the backend (considering additional resource and action information).

Conclusion

Despite being a common challenge in system development, finding an Authorization solution that fits your needs can be difficult. It’s crucial to carefully study alternatives, run Proofs of Concept in limited scopes, and have a solid migration plan when transitioning from an existing system.

This is the kind of problem that makes sense to be handled by a Platform Team, as it impacts all layers of development and requires strong leadership support to be successfully implemented.

Tenha um plano de saúde empresarial que você pode contar

Tenha um plano de saúde empresarial que você pode contar

Peça um orçamento

empresas estão simulando

Escolha aqui seu plano ideal