PEP e PDP para Autorização Segura com AVP e ABAC

2025-05-06
This post cover image
#aws
#cloud
#serverless
#AuthZ
Voice provided by Amazon Polly

Este arquivo foi traduzido automaticamente por IA, erros podem ocorrer

No primeiro artigo, PEP e PDP para Autorização Segura com Cognito, introduzi o conceito de Pontos de Aplicação de Política (PEPs) e Pontos de Decisão de Política (PDPs) usando Lambda Authorizer no API Gateway como PEP e um serviço baseado em Lambda como PDP. A decisão de autorização era baseada nas funções que os usuários possuíam, representadas por grupos do Cognito, com as permissões mapeadas em uma tabela DynamoDB.

Na segunda parte, PEP e PDP para Autorização Segura com AVP, substituímos o mapeamento de permissões do DynamoDB pelo Amazon Verified Permissions (AVP) e implementamos um sistema de Controle de Acesso Baseado em Funções (RBAC) usando políticas Cedar. Isso nos deu uma maneira muito mais poderosa de lidar com a autorização, especialmente para aplicações SaaS multi-tenant.

Agora, nesta terceira parte, levaremos nosso sistema de autorização para o próximo nível adicionando Controle de Acesso Baseado em Atributos (ABAC) ao lado do RBAC usando AVP. Essa combinação fornece decisões de autorização ainda mais dinâmicas e conscientes do contexto, que é exatamente o que precisamos para aplicações multi-tenant complexas.

Entendendo o Controle de Acesso Baseado em Atributos (ABAC)

Então, o que exatamente é ABAC? Em sua essência, ABAC é uma estratégia de autorização que toma decisões com base em atributos associados a usuários, recursos, ações e o ambiente. Diferentemente do RBAC, que concede permissões apenas com base em funções, o ABAC avalia vários atributos para tomar decisões de autorização mais refinadas.

Pense desta forma - RBAC é como ter diferentes chaves (funções) que abrem diferentes portas (recursos). O ABAC, por outro lado, é como ter uma fechadura inteligente que considera não apenas quem você é, mas também de onde você está vindo, que horas são, o que você está tentando fazer e até mesmo o que está acontecendo ao seu redor antes de decidir se permite ou não sua entrada.

Como exemplo, alguns atributos comuns usados no ABAC podem ser:

Atributos do usuário: Departamento, localização, nível de liberação, antiguidade
Atributos do recurso: Classificação, proprietário, nível de sensibilidade
Atributos da ação: Hora do dia, status de criptografia, método de acesso
Atributos ambientais: Tipo de dispositivo, localização da rede, nível de segurança

Por exemplo, uma política ABAC pode declarar que Cientistas de dados podem acessar dados sensíveis, mas apenas de redes corporativas, durante o horário comercial e se tiverem concluído o treinamento de segurança nos últimos 6 meses.

Por que Combinar RBAC e ABAC?

Mas se o ABAC é tão poderoso, por que não usá-lo em vez do RBAC? Eu diria que o RBAC é realmente simples de entender e implementar, o que o torna ótimo para necessidades básicas de autorização. No entanto, quando as permissões precisam ser mais granulares ou dependentes do contexto, pode se tornar muito difícil e complexo de gerenciar.

Por outro lado, o ABAC oferece uma flexibilidade incrível, mas pode ser muito complexo de implementar do zero e mais difícil de raciocinar. Ao combinar ambas as abordagens, obtemos o melhor dos dois mundos!

Em nosso exemplo de corrida de unicórnios, podemos usar funções para definir padrões de acesso amplos (Admin, Treinador, Piloto), então usaremos um atributo dataAccess para restringir o acesso a conjuntos de dados específicos dentro dessas funções.

Implementando ABAC no Amazon Verified Permissions

O AVP com sua linguagem de política Cedar é perfeitamente adequado para implementar ABAC. As políticas Cedar podem avaliar condições com base em atributos do principal (usuário), recurso, ação e contexto.

Mapeamento de Token no AVP - Um Conceito Crítico

Uma parte importante da implementação do ABAC é entender como as reivindicações de token são mapeadas para o esquema de política Cedar. O AVP lida com isso de maneira diferente dependendo se você está usando tokens de ID ou tokens de acesso.

Token de ID: As reivindicações são mapeadas para atributos da entidade principal representando o usuário
Token de Acesso: As reivindicações são mapeadas para o objeto context.token na avaliação da política

Essa diferença é super importante ao escrever políticas Cedar porque afeta como você acessa atributos em suas condições de política!

Por exemplo, com um token de ID, uma política ficaria algo assim

permit (principal, action, resource)
when { principal.department == "Engineering" };

Mas com um token de acesso, precisamos usar o objeto de contexto e não o principal

permit (principal, action, resource)
when { context.token["department"] == "Engineering" };

Em nossa implementação, usaremos a abordagem de token de acesso, pois acho que fornece uma separação mais limpa entre identidade (quem é o usuário) e permissões de acesso e atributos (o que eles podem fazer e que propriedades possuem).

Por que Tokens de Acesso para Autorização

Enquanto os tokens de ID são principalmente destinados à autenticação (provando identidade), os tokens de acesso são projetados especificamente para autorização (determinando permissões). Vejo várias vantagens em usar tokens de acesso:

Separação de preocupações: Detalhes de autenticação permanecem nos tokens de ID, enquanto detalhes de autorização vivem nos tokens de acesso
Tamanho reduzido do token: Os tokens de ID não ficarão inflados com atributos de autorização
Vida útil independente: A expiração do token de acesso pode ser mais curta que a expiração do token de ID
Revogação mais fácil do token: Você pode revogar o acesso sem afetar a autenticação

Em nosso PDP baseado em AVP, usaremos tokens de acesso e mapearemos reivindicações personalizadas para o contexto para melhores decisões de autorização.

Implementando AVP com RBAC e ABAC

Agora que entendemos os conceitos, vamos colocar as mãos na massa e implementar nosso sistema de autorização aprimorado!

Arquitetura

Nossa arquitetura permanece semelhante, mas com algumas mudanças. Onde adicionamos uma nova função Lambda para poder enriquecer nosso token de acesso com reivindicações personalizadas.

Diagrama de arquitetura mostrando PEP e PDP com ABAC

Isso nos dará um fluxo de chamada atualizado como este.

Fluxo de chamada PEP e PDP com ABAC

As mudanças que precisamos fazer seriam.

Primeiro de tudo, precisamos adicionar dataAccess como uma reivindicação personalizada em nosso token de acesso, para fazer isso devemos configurar uma função Lambda de Pré-geração de Token, que será invocada pelo Cognito.

Segundo, precisamos atualizar nosso esquema e adicionar o atributo dataAccess em cada ação que gostaríamos que a política pudesse avaliar de acordo com o ABAC.

Por último, precisamos atualizar nossas políticas Cedar para serem conscientes do contexto e avaliar tanto a função (RBAC) quanto os atributos (ABAC)

Adicionando Atributos Personalizados aos Tokens de Acesso

Para implementar ABAC, precisamos de uma maneira de adicionar atributos personalizados aos nossos tokens de acesso. Usaremos o gatilho Pre-Token Generation Lambda do Cognito para esse propósito, e precisamos usar a versão 2 ou 3 do evento. Com a versão 1, podemos apenas modificar o Token de ID.

Abaixo está uma implementação que adiciona o atributo dataAccess dos atributos do usuário ao token de acesso:

def handler(event, context):
    user_attributes = event["request"]["userAttributes"]
    claims_to_add_to_access_token = {}

    if "custom:dataAccess" in user_attributes:
        data_access_values = [
            value.strip() for value in user_attributes["custom:dataAccess"].split(",")
        ]
        claims_to_add_to_access_token["custom:dataAccess"] = data_access_values

    response = {
        "claimsAndScopeOverrideDetails": {
            "accessTokenGeneration": {
                "claimsToAddOrOverride": claims_to_add_to_access_token,
            }
        }
    }

    event["response"] = response
    return event

Esta função verifica se o usuário tem um atributo custom:dataAccess. Se encontrado, divide os valores separados por vírgula em uma matriz e os adiciona ao token de acesso. Dessa forma, nossas políticas AVP podem verificar se o usuário tem acesso a categorias específicas de dados.

Para que o Cognito chame nossa função, precisamos configurar os Gatilhos Lambda do Pool de Usuários.

  PreTokenGenerationFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/PreTokenGeneration
      Handler: index.handler
      Runtime: python3.12
      Architectures:
        - x86_64
      MemorySize: 128
      Description: Uma função Lambda que adiciona atributos personalizados ao token de acesso JWT
      Policies:
        - AWSLambdaBasicExecutionRole

  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UsernameConfiguration:
        CaseSensitive: false
      AutoVerifiedAttributes:
        - email
      UserPoolName: !Sub ${ApplicationName}-user-pool
      Schema:
        - Name: email
          AttributeDataType: String
          Mutable: false
          Required: true
        - Name: name
          AttributeDataType: String
          Mutable: true
          Required: true
        - Name: dataAccess
          AttributeDataType: String
          Mutable: true
          Required: false
      LambdaConfig:
        PreTokenGenerationConfig:
          LambdaArn: !GetAtt PreTokenGenerationFunction.Arn
          LambdaVersion: V2_0

Esquema de Política Cedar com Atributos de Contexto

Nosso esquema Cedar define a estrutura de principais, recursos, ações e seus relacionamentos. Para ABAC, precisamos atualizar o esquema para incluir atributos de contexto:

{
  "${AVPNameSpace}": {
    "actions": {
      "get /rider": {
        "appliesTo": {
          "context": {
            "type": "Record",
            "attributes": {
              "dataAccess": {
                "type": "String",
                "required": true
              }
            }
          },
          "principalTypes": ["CognitoUser"],
          "resourceTypes": ["Application"]
        }
      }
      // Outras ações...
    }
  }
}

Este esquema nos permite usar o atributo dataAccess em nossas políticas de autorização.

Políticas que Combinam RBAC e ABAC

Nossas políticas Cedar agora combinam lógica RBAC (baseada em função) e ABAC (baseada em atributo):

permit(
  principal in ${AVPNameSpace}::CognitoUserGroup::"${UserPoolId}|Admin",
  action in [
    ${AVPNameSpace}::Action::"get /rider",
    ${AVPNameSpace}::Action::"get /riders",
    ${AVPNameSpace}::Action::"get /trainer",
    // mais ações...
  ],
  resource
)
when {
  // Condição ABAC usando atributos
  context.dataAccess == "" ||
  (context.token has "custom:dataAccess" &&
   context.token["custom:dataAccess"].contains(context.dataAccess))
};

Esta política permite que usuários no grupo Admin acessem endpoints (parte RBAC), mas apenas se uma destas condições for verdadeira (parte ABAC):

  • Nenhuma verificação específica de acesso a dados é necessária (context.dataAccess vazio), OU
  • O usuário tem o atributo de acesso a dados necessário em seu token

Vamos decompor a condição um pouco para melhor entendimento.

  • Se context.dataAccess estiver vazio, não aplicamos restrições de acesso a dados
  • Caso contrário, verificamos se o token do usuário tem a reivindicação custom:dataAccess que contém o valor necessário

Lógica de Autorização Consciente do Contexto

Nossa função Lambda PDP agora lida com autorização baseada em contexto. Ela extrai atributos relevantes das solicitações e os inclui na chamada de autorização AVP. A função abaixo foi simplificada e algumas partes foram removidas, verifique o código-fonte para uma versão completa.

def validate_permission(event):
    jwt_token = event["jwt_token"]
    resource = event["resource"]
    action = event["action"]
    resource_tags = {}

    if "resource_tags" in event:
        resource_tags = event["resource_tags"]

    parsed_token = decode_token(jwt_token)

    try:
        action_id = f"{action.lower()} {resource.lower()}"
        user_principal = parsed_token["sub"]

        # Chamar AVP com contexto para a decisão de autorização
        context = {
            "dataAccess": resource_tags.get("Data", "")
        }

        auth_response = avp_client.is_authorized_with_token(
            accessToken=jwt_token,
            policyStoreId=policy_store_id,
            action={"actionType": action_type, "actionId": action_id},
            resource={"entityType": resource_type, "entityId": resource_id},
            context=context
        )

        # Armazenar o resultado com o hash do contexto
        store_auth_cache(user_principal, action_id, context_hash, auth_response)

        # Gerar a resposta
        response_body = generate_access(
            user_principal,
            "Allow" if auth_response["decision"].upper() == "ALLOW" else "Deny",
            action,
            resource
        )

        return {
            "statusCode": 200,
            "body": json.dumps(response_body),
            "headers": {"Content-Type": "application/json"},
        }

    except Exception as e:
        print(f"Erro ao validar permissões: {e}")

    response_body = generate_access(parsed_token["sub"], "Deny", action, resource)

    return {
        "statusCode": 200,
        "body": json.dumps(response_body),
        "headers": {"Content-Type": "application/json"},
    }

Com esta implementação, nosso PDP agora avalia tanto regras baseadas em função (RBAC) quanto condições baseadas em atributos (ABAC).

Cache Consciente do Contexto

Um dos desafios ao adicionar ABAC é que as decisões de autorização agora dependem não apenas de quem você é e o que está tentando fazer, mas também do contexto. Isso significa que nossa estratégia de cache precisa ser atualizada.

Incluiremos informações de contexto em nossa chave de cache criando um hash dos atributos de contexto:

def generate_context_hash(context_dict):
    sorted_items = sorted(context_dict.items())
    context_str = json.dumps(sorted_items)
    return hashlib.md5(context_str.encode()).hexdigest()

def get_auth_cache(principal, action, context_hash):
    try:
        response = dynamodb_client.get_item(
            TableName=table_name, 
            Key={
                "PK": {"S": principal}, 
                "SK": {"S": f"{action}#{context_hash}"}
            }
        )

        item = response.get("Item")
        if not item:
            return None

        ttl = int(item.get("TTL")["N"])
        if ttl and ttl < int(time.time() * 1000):  # TTL está em milissegundos
            return None

        return item.get("Effect")["S"]

    except Exception as e:
        print(f"Erro ao obter cache de autenticação: {e}")
        return None

def store_auth_cache(principal, action, context_hash, auth_response):
    try:
        ttl = int((datetime.now(timezone.utc) + timedelta(hours=12)).timestamp() * 1000)

        effect = "Allow" if auth_response.get("decision", "").upper() == "ALLOW" else "Deny"

        dynamodb_client.put_item(
            TableName=table_name,
            Item={
                "PK": {"S": principal},
                "SK": {"S": f"{action}#{context_hash}"},
                "TTL": {"N": str(ttl)},
                "Effect": {"S": effect},
            },
        )

    except Exception as e:
        print(f"Erro ao armazenar cache de autenticação: {e}")

Ao incluir um hash do contexto em nossa chave de cache, garantimos que as decisões de autorização sejam corretamente armazenadas em cache e recuperadas com base tanto na função do usuário quanto nos atributos de contexto relevantes.

Modificando o PEP para Passar Contexto

Nosso PEP (Lambda Authorizer) também precisa de uma pequena atualização para passar informações de contexto para o PDP, esta é uma implementação muito básica, e em um sistema de produção você certamente teria algo mais sofisticado, como olhar para as Tags de recursos nos recursos da AWS.

def get_resource_tags_for_path(path):
    if (
        path.startswith("/unicorn")
        or path.startswith("/rider")
        or path.startswith("/trainer")
    ):
        return {"Data": "Unicorn"}
    elif path.startswith("/race"):
        return {"Data": "Races"}
    else:
        return {}

# Dentro da função handler
resource_tags = get_resource_tags_for_path(path)

data = {
    "jwt_token": token,
    "resource": path,
    "action": method,
    "resource_tags": resource_tags,
}

Esta função determina a qual categoria de dados um recurso pertence com base em seu caminho e passa essa informação para o PDP para autorização consciente do contexto.

Testando a Implementação ABAC

Agora que temos nossa implementação no lugar, é hora de testá-la! Aqui está como podemos testar nossa solução combinada RBAC + ABAC.

Primeiro, precisamos criar ou atualizar um usuário em diferentes grupos do Cognito (Admin, Treinador, Piloto). Também precisamos atribuir diferentes atributos dataAccess a esses usuários (por exemplo, "Unicorn", "Races").

Em seguida, navegue até a página da web implantada com a distribuição CloudFront e inspecione os tokens JWT, cookies. Imagem mostrando os cookies

Se copiarmos o token de acesso e decodificá-lo, eu uso jwt.io, podemos ver que meu usuário tem a reivindicação custom:dataAccess que nosso PEP e PDP usarão mais tarde para permissões.

Agora podemos usar uma ferramenta como Bruno ou Postman para incluir o token de acesso nas chamadas para a API. Claro, dependendo de como você configurou o acesso, você deve obter um Allow ou Deny do PDP resultando em diferentes resultados da API. Abaixo estão alguns exemplos de visualizações onde uso o Bruno para chamar a API com e sem acesso.

Imagem mostrando permissão concedida

Imagem mostrando permissão negada

Conclusão

Ao combinar RBAC e ABAC com Amazon Verified Permissions, criamos um sistema de autorização poderoso e flexível que pode lidar com requisitos complexos de controle de acesso. Esta abordagem fornece a simplicidade do acesso baseado em função enquanto permite o controle refinado de decisões baseadas em atributos.

Esta abordagem de implementação pode ser usada em aplicações SaaS multi-tenant onde a segregação de dados, baseada no tenant, e o controle de acesso consciente do contexto são críticos. Com o AVP lidando com a avaliação de políticas e nossa arquitetura serverless para aplicação, temos uma solução escalável e sustentável para até mesmo as necessidades de autorização mais exigentes.

Código-Fonte

Toda a configuração, com instruções detalhadas de implantação e todo o código pode ser encontrado no Serverless Handbook PEP e PDP

Palavras Finais

Não se esqueça de me seguir no LinkedIn e X para mais conteúdo, e ler o resto dos meus Blogs

Como Werner diz! Agora Vá Construir!

Post Quiz

Test what you just learned by doing this five question quiz.
Scan the QR code below or click the link above.

Powered by kvist.ai your AI generated quiz solution!