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

Questo file è stato tradotto automaticamente dall'IA, potrebbero verificarsi errori
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 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 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 forma 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 quais propriedades eles têm).
Por que Tokens de Acesso para Autorização
Enquanto os tokens de ID são principalmente para 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 de token reduzido: Os tokens de ID não serã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 de token mais fácil: 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.
Isso nos dará um fluxo de chamada atualizado como este.
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"]
# Chama 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
)
# Armazena o resultado com o hash do contexto
store_auth_cache(user_principal, action_id, context_hash, auth_response)
# Gera 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 de adicionar ABAC é que as decisões de autorização agora dependem não apenas de quem você é e o que você 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 autorizaçã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 autorizaçã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, "Unicórnio", "Corridas").
Em seguida, navegue até a página da web implantada com a distribuição CloudFront e inspecione os tokens JWT, 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.
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!
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!