PEP e PDP para Autorização Segura com AVP

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.

Para entender melhor o fluxo durante um acesso à API.

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.

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

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

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


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>

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

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

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!