aws, authz, serverless

PEP e PDP para Autorização Segura com AVP

2025-02-20
This post cover image
aws cloud serverless AuthZ

Este arquivo foi traduzido automaticamente por IA, erros podem ocorrer

Na primeira parte, 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 o Lambda Authorizer no API Gateway como PEP e um serviço baseado em Lambda como PDP. A decisão de autorização foi baseada nos papéis dos usuários, representados por grupos do Cognito, com as permissões mapeadas em uma tabela do DynamoDB.

Ao executar uma configuração de PEP e PDP em uma solução SaaS multilocatário, certamente precisamos de algo mais poderoso do que uma tabela do DynamoDB com permissões. Portanto, nesta parte, vamos examinar o uso do Amazon Verified Permissions (AVP) como base em nosso PDP. Isso seria uma excelente escolha para fazer autorização em SaaS.

O que é o Amazon Verified Permissions?

O Amazon Verified Permissions (AVP) é um serviço totalmente gerenciado que simplifica a implementação de controle de acesso granular em nossos aplicativos. O AVP atua como um Ponto de Decisão de Política (PDP) central, onde avalia políticas e toma decisões de autorização com base nos atributos da solicitação, papéis do usuário e similares. Ele não apenas suporta controle de acesso baseado em papéis (RBAC), mas o AVP também suporta controle de acesso baseado em atributos (ABAC) e outras políticas dinâmicas, permitindo decisões de autorização mais flexíveis e granulares.

Com o AVP, nossa função Lambda (atuando como o PDP) não precisa mais analisar e avaliar políticas manualmente, e não precisamos armazenar o mapeamento de permissões em uma tabela do DynamoDB. Em vez disso, definimos e gerenciamos nossas políticas diretamente no AVP, que então toma a decisão de autorização para nós.

Alternativas de soluções de código aberto para AVP

Embora o AVP seja uma excelente escolha para aplicativos, especialmente se eles rodarem na AWS, não é a única solução para implementar autorização granular. Uma alternativa é o Oso, um framework de autorização de código aberto projetado para fornecer controle de acesso baseado em políticas de forma flexível. O Oso permite definir a lógica de autorização em uma linguagem declarativa e se integra com nossos aplicativos em vários ambientes. A escolha entre AVP e Oso depende do nosso caso de uso específico, infraestrutura, etc.

Cache de decisões de autorização

Agora que examinamos o uso do AVP como parte central do nosso PDP, devemos pensar em cache. Existem prós e contras contra o cache, por exemplo, o AWS IAM nunca armazena em cache nenhuma decisão, o que garante que as atualizações nas políticas sejam refletidas imediatamente. Com um cache, podemos reduzir o número de chamadas ao AVP e, assim, reduzir o custo. Também podemos armazenar em cache as decisões no cliente ou em nossos sistemas backend. Não vou entrar em detalhes neste tópico mais do que isso, nesta solução usaremos um cache externo em nosso sistema backend. O cache será colocado no DynamoDB. A razão para armazenar em cache em uma fonte externa, como o DynamoDB, em vez de na memória da função Lambda, é para que todas as invocações da função Lambda se beneficiem do cache. Com um cache como este, precisaríamos de uma maneira de limpar o cache se um conjunto de permissões de um usuário for alterado. Não implementarei nenhum mecanismo de invalidação de cache nesta publicação.

Entendendo AVP e a Linguagem de Política Cedar

O Amazon Verified Permissions (AVP) usa o Cedar, uma linguagem de política-as-código projetada especificamente para autorização granular. O Cedar permite definir e impor políticas de controle de acesso que determinam quem pode executar quais ações em quais recursos.

As políticas Cedar são declarativas, ou seja, elas explicitamente afirmam as permissões sem exigir lógica procedural. Elas são projetadas para serem fáceis de ler enquanto suportam condições poderosas e controle de acesso baseado em atributos (ABAC).

Exemplo Básico de Política Cedar

Vamos começar com uma política RBAC (Role-Based Access Control) simples que permite que usuários no grupo "Admin" executem certas ações em todos os recursos

permit (
    principal in UserGroup::"Admin",
    action in [
        Action::"create",
        Action::"read",
        Action::"update",
        Action::"delete"
    ],
    resource
);

Esta política significa que qualquer principal (usuário) que pertencer ao UserGroup Admin tem permissão para executar as ações listadas em todos os recursos, uma vez que o recurso não está restrito.

Um segundo exemplo, onde nos inclinamos para um método ABAC, seria.

permit (
    principal in UserGroup::"JimmysFriends",
    action == Action::"viewPhoto",
    resource
)
when {
    resource.tags.contains("Shared")
};

Isso significa que usuários em JimmysFriends podem visualizar fotos quando a foto tem a tag Shared.

Em um sistema SaaS multilocatário, podemos querer restringir o acesso a dados pertencentes a um locatário específico. O Cedar permite condições baseadas em atributos para isso.

permit (
    principal,
    action == Action::"view",
    resource
)
when {
    principal.tenant == resource.tenant
};

Isso garante que o usuário só possa visualizar recursos que pertencem ao mesmo locatário.

Na implementação para o nosso PDP baseado em AVP, usaremos RBAC e políticas semelhantes ao primeiro exemplo.

Usando AVP para controle de acesso escalável e flexível

Ao integrar o (AVP) como parte do nosso PDP central, simplificamos o gerenciamento de políticas e melhoramos a escalabilidade e flexibilidade do sistema de autorização. O suporte do AVP a políticas dinâmicas e controle de acesso granular são ferramentas poderosas para qualquer aplicativo SaaS.

Implementação

Com a introdução do AVP, vamos converter nosso PDP para usar AVP para Acesso Baseado em Papel (RBAC) em vez de um mapeamento de permissões do DynamoDB. Vamos simplesmente implantar um segundo PDP em nossa solução e trocar para que nosso PEP use o novo PDP baseado em AVP. Toda essa configuração é baseada na solução introduzida na primeira parte, PEP e PDP para Autorização Segura com Cognito,, e como pré-requisito, essa solução deve ser implantada. Você encontra toda a solução em Serverless Handbook PEP e PDP.

Visão Geral da Arquitetura

Só para lembrar, todo o código e toda a arquitetura podem ser encontrados em Serverless Handbook PEP e PDP

Olhando para a visão geral da arquitetura e do fluxo de chamadas, podemos ver que não há mudanças importantes, mas em vez de buscar um mapeamento de permissões do DynamoDB, chamaremos o AVP e deixaremos o serviço usar seu motor de políticas para permitir/negar a decisão.

Imagem mostrando a visão geral da arquitetura

Para entender melhor o fluxo durante um acesso à API.

Imagem mostrando o fluxo de chamadas

Configuração e implantação do PDP baseado em AVP

Primeiro, precisamos implantar todos os recursos necessários pelo AVP, o que precisamos fazer é criar um Policy Store com nosso esquema de política. As Políticas para nossos três papéis diferentes com uma política baseada em Cedar para determinar o que o Papel tem permissão para fazer, e precisamos configurar nossa Fonte de Identidade.

Este modelo pode ser bastante longo, pois as políticas e esquemas Cedar tendem a ser bastante extensos. Portanto, partes deste modelo foram omitidas, visite Serverless Handbook PEP e PDP para o modelo completo.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: PDP Service
Parameters:
  ApplicationName:
    Type: String
    Description: Name of owning application
  UserManagementStackName:
    Type: String
    Description: The name of the stack that contains the user management part, e.g the Cognito UserPool
  AVPNameSpace:
    Type: String
    Description: The name space for Amazon Verified Permissions
    AllowedPattern: "[a-z]+"
  UserPoolId:
    Type: String
    Description: The ID of the Cognito User Pool

Resources:
  PolicyStore:
    Type: AWS::VerifiedPermissions::PolicyStore
    Properties:
      Description: !Sub Policy Store for ${ApplicationName}
      ValidationSettings:
        Mode: "OFF"
      Schema:
        CedarJson: !Sub |
          {
            "${AVPNameSpace}": {
                "entityTypes": {
                    "CognitoUser": {
                        "shape": {
                            "type": "Record",
                            "attributes": {}
                        },
                        "memberOfTypes": [
                            "CognitoUserGroup"
                        ]
                    },
                    "CognitoUserGroup": {
                        "shape": {
                            "attributes": {},
                            "type": "Record"
                        }
                    },
                    "Application": {
                        "shape": {
                            "attributes": {},
                            "type": "Record"
                        }
                    }
                },
                "actions": {
                    "get /rider": {
                        "appliesTo": {
                            "context": {
                                "type": "Record",
                                "attributes": {}
                            },
                            "principalTypes": [
                                "CognitoUser"
                            ],
                            "resourceTypes": [
                                "Application"
                            ]
                        }
                    },
                    "get /riders": {
                        "appliesTo": {
                            "context": {
                                "type": "Record",
                                "attributes": {}
                            },
                            "principalTypes": [
                                "CognitoUser"
                            ],
                            "resourceTypes": [
                                "Application"
                            ]
                        }
                    },
                    "get /trainer": {
                        "appliesTo": {
                            "context": {
                                "type": "Record",
                                "attributes": {}
                            },
                            "principalTypes": [
                                "CognitoUser"
                            ],
                            "resourceTypes": [
                                "Application"
                            ]
                        }
                    },
                    "get /trainers": {
                        "appliesTo": {
                            "context": {
                                "type": "Record",
                                "attributes": {}
                            },
                            "principalTypes": [
                                "CognitoUser"
                            ],
                            "resourceTypes": [
                                "Application"
                            ]
                        }
                    },
                    "get /unicorn": {
                        "appliesTo": {
                            "context": {
                                "type": "Record",
                                "attributes": {}
                            },
                            "principalTypes": [
                                "CognitoUser"
                            ],
                            "resourceTypes": [
                                "Application"
                            ]
                        }
                    },
                    "get /unicorns": {
                        "appliesTo": {
                            "context": {
                                "type": "Record",
                                "attributes": {}
                            },
                            "principalTypes": [
                                "CognitoUser"
                            ],
                            "resourceTypes": [
                                "Application"
                            ]
                        }
                    }
                }
            }
          }

  CognitoIdentitySource:
    Type: AWS::VerifiedPermissions::IdentitySource
    Properties:
      Configuration: 
        CognitoUserPoolConfiguration: 
          ClientIds: 
            - Fn::ImportValue: !Sub ${UserManagementStackName}:app-audience
          GroupConfiguration: 
            GroupEntityType: !Sub ${AVPNameSpace}::CognitoUserGroup
          UserPoolArn: 
            Fn::ImportValue: !Sub ${UserManagementStackName}:user-pool-arn
      PolicyStoreId: !Ref PolicyStore
      PrincipalEntityType: !Sub ${AVPNameSpace}::CognitoUser

  AdminUserPolicy:
    Type: AWS::VerifiedPermissions::Policy
    Properties:
      Definition:
        Static:
          Description: Policy for Admin User group in Cognito
          Statement: !Sub |
            permit(
              principal in ${AVPNameSpace}::CognitoUserGroup::"${UserPoolId}|Admin", 
              action in [ ${AVPNameSpace}::Action::"get /rider", ${AVPNameSpace}::Action::"get /riders", ${AVPNameSpace}::Action::"get /trainer", ${AVPNameSpace}::Action::"get /trainers", ${AVPNameSpace}::Action::"get /unicorn", ${AVPNameSpace}::Action::"get /unicorns" ], 
              resource
            );
      PolicyStoreId: !Ref PolicyStore

  LambdaPDPFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/AuthZ
      Handler: authz.handler
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref AVPCacheTable
        - Version: "2012-10-17"
          Statement:
            Effect: Allow
            Action:
              - "verifiedpermissions:EvaluatePolicy"
              - "verifiedpermissions:GetPolicy"
              - "verifiedpermissions:IsAuthorizedWithToken"
            Resource: "*"
      Environment:
        Variables:
          JWKS_URL:
            Fn::ImportValue: !Sub ${UserManagementStackName}:jwks-url
          AUDIENCE:
            Fn::ImportValue: !Sub ${UserManagementStackName}:app-audience
          POLICY_STORE_ID:
            !Ref PolicyStore
          NAMESPACE:
            !Ref AVPNameSpace
          TOKEN_TYPE: "accessToken"

Lógica de autorização

O PDP Lambda obterá o jwt-token, action e resource do PEP, nosso Lambda Authorizer no API Gateway. A função obterá o principal, o sujeito, do JWT-Token e verificará se há uma decisão em cache. Se não houver, chamará o AVP com todas as informações para obter uma decisão de autorização.

import json
import os
import base64
import boto3
import time
from datetime import datetime, timedelta, timezone

policy_store_id = os.getenv("POLICY_STORE_ID")
namespace = os.getenv("NAMESPACE")
token_type = os.getenv("TOKEN_TYPE")
resource_type = f"{namespace}::Application"
resource_id = namespace
action_type = f"{namespace}::Action"
table_name = os.environ["PERMISSION_CACHE_TABLE"]

avp_client = boto3.client("verifiedpermissions")
dynamodb_client = boto3.client("dynamodb")


def decode_token(bearer_token):
    return json.loads(base64.b64decode(bearer_token.split(".")[1]).decode("utf-8"))


def generate_access(principal, effect, action, resource):
    auth_response = {
        "statusCode": 200 if effect == "Allow" else 403,
        "principalId": principal,
        "effect": effect,
        "action": action,
        "resource": resource,
    }
    return auth_response


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

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

        ttl = int(item.get("TTL")["N"])
        if ttl and ttl < int(time.time() * 1000):  # TTL is in milliseconds
            return None

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

    except Exception as e:
        print(f"Error getting auth cache: {e}")
        return None


def store_auth_cache(principal, action, 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": action},
                "TTL": {"N": str(ttl)},
                "Effect": {"S": effect},
            },
        )

    except Exception as e:
        print(f"Error storing auth cache: {e}")


def validate_permission(event):
    jwt_token = event["jwt_token"]
    resource = event["resource"]
    action = event["action"]
    parsed_token = decode_token(jwt_token)

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

        cached_auth = get_auth_cache(user_principal, action_id)

        if cached_auth is None:
            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},
            )

            store_auth_cache(user_principal, action_id, auth_response)

            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"},
            }

        else:
            response_body = generate_access(
                user_principal, cached_auth, action, resource
            )

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

    except Exception as e:
        print(f"Error validating permissions: {e}")

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

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


def handler(event, context):
    print(f"Event: {json.dumps(event)}")
    permissions = validate_permission(event)
    return permissions

Atualizar PEP para usar PDP baseado em AVP

Para atualizar nosso PEP para usar o novo PDP baseado em AVP em vez do baseado em DynamoDB, navegue até a pasta API e modifique o arquivo samconfig.yaml, atualize o valor de PDP para PDPStackName=pep-pdp-cognito-pdp-auth-service-avp e depois reimplantar a parte da API da solução. Isso agora trocará o PDP que está sendo usado.

Testes

Para testar a configuração do AVP, podemos navegar até a parte do AVP no console.

Embaixo de Policy Stores, você deve ver o policy store que foi criado para nosso PDP.

Imagem mostrando a lista de policy stores

Clicando no ID do policy store e selecionando Policies no menu, vemos a lista das três políticas criadas.

Imagem mostrando a lista de políticas

Selecionando a política Riders, podemos inspecionar a política de criação.

Imagem mostrando a lista de políticas

Selecionando Schema no menu, podemos inspecionar o esquema criado de forma visual.

Imagem mostrando o esquema de política

Imagem mostrando o esquema de política

Agora, podemos navegar até o Test Bench para testar nossas políticas, preencher as informações conforme mostrado na imagem abaixo. O grupo deve ser prefixado com o ID do Cognito User Pool e seguir o padrão <COGNITO_USER_POOL_ID>|<GROUP_NAME>

Imagem mostrando o banco de testes

Se selecionarmos a ação /get trainers e clicarmos em Run Authorization request, devemos receber uma negação, pois o papel Trainer não tem acesso à essa Ação

Imagem mostrando o banco de testes

Trocando para a ação /get trainer, devemos receber uma autorização.

Imagem mostrando o banco de testes

Resumo e conclusão

Implementar PEP e PDP em nosso fluxo de autorização oferece uma maneira altamente escalável, flexível e segura de controlar o acesso aos recursos. Ao aproveitar o AWS Lambda e o API Gateway, podemos construir um sistema de autorização sem servidor que separa as preocupações de autenticação e autorização, escala com a demanda e simplifica o gerenciamento de políticas.

Com a adição de controle de acesso baseado em papéis e Amazon Verified Permissions (AVP), combinado com cache para maior desempenho, podemos criar uma solução de autorização que atenda às necessidades atuais e futuras. Usar AVP em nossas soluções SaaS nos dá uma maneira muito poderosa de lidar com multilocatários.

Boa codificação e mantenha-se seguro!

Código-fonte

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

Palavras Finais

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

Como Werner diz! Agora vá construir!