PEP y PDP para Autorización Segura con AVP

Este archivo ha sido traducido automaticamente por IA, pueden ocurrir errores
En la primera parte, PEP y PDP para Autorización Segura con Cognito, introduje el concepto de Puntos de Aplicación de Política (PEPs) y Puntos de Decisión de Política (PDPs) utilizando Lambda Authorizer en API Gateway como PEP y un servicio basado en Lambda como PDP. La decisión de autorización se basó en los roles que tenían los usuarios, representados por grupos de Cognito, con los permisos mapeados en una tabla de DynamoDB.
Cuando se ejecuta una configuración de PEP y PDP en una solución SaaS multitenante, definitivamente necesitamos algo más potente que una tabla de DynamoDB con permisos. Por lo tanto, en esta parte veremos el uso de Permisos Verificados de Amazon (AVP) como base en nuestro PDP. Lo cual sería una excelente opción para hacer autorización en SaaS.
¿Qué es Permisos Verificados de Amazon?
Permisos Verificados de Amazon (AVP) es un servicio completamente administrado que simplifica la implementación de control de acceso granular en nuestras aplicaciones. AVP actúa como un Punto de Decisión de Política (PDP) central, donde evalúa políticas y toma decisiones de autorización basadas en los atributos de la solicitud, roles de usuario y similares. No solo admite el control de acceso basado en roles (RBAC), sino que AVP también admite el control de acceso basado en atributos (ABAC) y otras políticas dinámicas, lo que permite decisiones de autorización más flexibles y granulares.
Con AVP, nuestra función Lambda (que actúa como el PDP) ya no tiene que analizar y evaluar políticas manualmente, y no necesitamos almacenar el mapeo de permisos en una tabla de DynamoDB. En su lugar, definimos y gestionamos nuestras políticas directamente en AVP, que luego toma la decisión de autorización por nosotros.
Soluciones de código abierto alternativas a AVP
Aunque AVP es una excelente opción para aplicaciones, especialmente si se ejecutan en AWS, no es la única solución para implementar autorización granular. Una alternativa es Oso, un marco de autorización de código abierto diseñado para proporcionar un control de acceso basado en políticas flexible. Oso nos permite definir la lógica de autorización en un lenguaje declarativo y se integra con nuestras aplicaciones en varios entornos. Elegir entre AVP y Oso depende de nuestro caso de uso específico, infraestructura, etc.
Caché de decisiones de autorización
Ahora que hemos visto el uso de AVP como una parte central de nuestro PDP, deberíamos pensar en el caché. Hay pros y contras en el caché, por ejemplo, AWS IAM nunca almacena en caché ninguna decisión, lo que asegura que las actualizaciones de las políticas se reflejen de inmediato. Con un caché podemos reducir la cantidad de llamadas a AVP y con eso reducir costos. También podemos almacenar en caché las decisiones en el cliente o en nuestros sistemas de backend. No entraré en profundidad en este tema más allá de que en esta solución utilizaremos un caché externo en nuestro sistema de backend. El caché se almacenará en DynamoDB. La razón para almacenar en caché en una fuente externa, como DynamoDB, en lugar de en la memoria de la función Lambda, es que todas las invocaciones de la función Lambda se beneficien del caché. Con un caché como este necesitaríamos una forma de borrar el caché si se cambia el conjunto de permisos de un usuario. No implementaré ningún mecanismo de invalidación de caché en esta publicación.
Comprendiendo AVP y el lenguaje de políticas Cedar
Permisos Verificados de Amazon (AVP) utiliza Cedar, un lenguaje de políticas como código diseñado específicamente para autorización granular. Cedar nos permite definir y hacer cumplir políticas de control de acceso que dictan quién puede realizar qué acciones sobre qué recursos.
Las políticas de Cedar son declarativas, lo que significa que establecen explícitamente los permisos sin requerir lógica procedimental. Están diseñadas para ser fáciles de leer mientras admiten condiciones poderosas y control de acceso basado en atributos (ABAC).
Ejemplo básico de política Cedar
Comencemos con una política RBAC (Control de Acceso Basado en Roles) simple que permite a los usuarios del grupo "Admin" realizar ciertas acciones en todos los recursos
permit (
principal in UserGroup::"Admin",
action in [
Action::"create",
Action::"read",
Action::"update",
Action::"delete"
],
resource
);Esta política significa que cualquier principal (usuario) que pertenezca al UserGroup Admin está autorizado para realizar las acciones enumeradas en todos los recursos, ya que el recurso no está restringido.
Un segundo ejemplo, donde nos inclinamos hacia un método ABAC, sería.
permit (
principal in UserGroup::"JimmysFriends",
action == Action::"viewPhoto",
resource
)
when {
resource.tags.contains("Shared")
};Esto significa que los usuarios en JimmysFriends pueden ver fotos cuando la foto tiene la etiqueta Shared.
En un sistema SaaS multitenante, podríamos querer restringir el acceso a datos que pertenecen a un inquilino específico. Cedar permite condiciones basadas en atributos para esto.
permit (
principal,
action == Action::"view",
resource
)
when {
principal.tenant == resource.tenant
};Esto asegura que el usuario solo pueda ver recursos que pertenezcan al mismo inquilino.
En la implementación para nuestro PDP basado en AVP utilizaremos RBAC y políticas similares al primer ejemplo.
Uso de AVP para un control de acceso escalable y flexible
Al integrar (AVP) como parte de nuestro PDP central, hemos simplificado la gestión de políticas y mejorado la escalabilidad y flexibilidad del sistema de autorización. El soporte de AVP para políticas dinámicas y control de acceso granular son herramientas poderosas para cualquier aplicación SaaS.
Implementación
Con la introducción de AVP, convertiremos nuestro PDP para que utilice AVP para el Control de Acceso Basado en Roles (RBAC) en lugar de un mapeo de permisos de DynamoDB. Simplemente desplegaremos un segundo PDP en nuestra solución y cambiaremos para que nuestro PEP utilice el nuevo PDP basado en AVP en su lugar. Toda esta configuración se basa en la solución presentada en la primera parte, PEP y PDP para Autorización Segura con Cognito,, y como prerequisito, esa solución debe estar desplegada. Encuentras toda la solución en Serverless Handbook PEP y PDP.
Visión general de la arquitectura
Solo como recordatorio, todo el código y la arquitectura pueden encontrarse en Serverless Handbook PEP y PDP
Mirando la visión general de la arquitectura y el flujo de llamadas, podemos ver que no hay cambios importantes, pero en lugar de obtener un mapeo de permisos de DynamoDB, llamaremos a AVP y dejaremos que el servicio utilice su motor de políticas para permitir/negar la decisión.

Para comprender mejor el flujo durante un acceso a la API.

Configuración e implementación del PDP basado en AVP
Primero necesitamos desplegar todos los recursos necesarios para AVP, lo que debemos hacer es crear un Policy Store con nuestro esquema de políticas. Las Políticas para nuestros tres roles diferentes con una política basada en Cedar para determinar qué puede hacer el Rol, y necesitamos configurar nuestra Fuente de Identidad.
Esta plantilla puede ser bastante larga ya que las políticas y esquemas de Cedar tienden a ser bastante grandes. Por lo tanto, partes de esta plantilla se han omitido, por lo tanto, visita Serverless Handbook PEP y PDP para obtener la plantilla completa.
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 autorización
El PDP Lambda obtendrá el jwt-token, la acción y el recurso del PEP, nuestro Lambda Authorizer en API Gateway. La función obtendrá el principal, el sujeto, del JWT-Token y verá si hay una decisión en caché. Si no la hay, llamará a AVP con toda la información para obtener una decisión de autorización.
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
Actualizar PEP para usar PDP basado en AVP
Para actualizar nuestro PEP para que use el nuevo PDP basado en AVP en lugar del basado en DynamoDB, navega a la carpeta API y modifica el archivo samconfig.yaml, actualiza el valor de PDP a PDPStackName=pep-pdp-cognito-pdp-auth-service-avp luego vuelve a implementar la parte de la API de la solución. Esto ahora cambiará el PDP que se utiliza.
Pruebas
Para probar la configuración de AVP podemos navegar a la parte de AVP en la consola.
Bajo Policy Stores deberías ver la tienda de políticas que se creó para nuestro PDP.

Haciendo clic en el ID de la tienda de políticas y seleccionando Políticas en el menú veremos la lista de las tres políticas creadas.

Seleccionando la política Riders ahora podemos inspeccionar la política creada.

Seleccionando Esquema en el menú podemos inspeccionar el esquema creado en forma visual.


Ahora, podemos navegar al Banco de Pruebas para probar nuestras políticas, llena la información como se muestra en la imagen a continuación. El grupo debe estar prefijado con el ID del Grupo de Usuarios de Cognito y seguir el patrón <ID_DEL_GRUPO_DE_USUARIOS_DE_COGNITO>|<NOMBRE_DEL_GRUPO>

Si seleccionamos la acción /get trainers y hacemos clic en Ejecutar solicitud de autorización deberíamos obtener un denegar, ya que el rol Trainer no tiene acceso a esa Acción

Cambiando a la acción /get trainer debería darnos un permitir.

Resumen y conclusión
Implementar PEP y PDP en nuestro flujo de autorización ofrece una forma altamente escalable, flexible y segura de controlar el acceso a los recursos. Al aprovechar AWS Lambda y API Gateway, podemos construir un sistema de autorización sin servidor que separa las preocupaciones de autenticación y autorización, se escala con la demanda y simplifica la gestión de políticas.
Con la adición del Control de Acceso Basado en Roles y Permisos Verificados de Amazon (AVP), combinado con el almacenamiento en caché para un rendimiento mejorado, podemos crear una solución de autorización que se adapte tanto a las necesidades actuales como futuras. Usar AVP en nuestras soluciones SaaS nos brinda una forma muy poderosa de manejar la multitenancia.
¡Feliz codificación y mantente seguro!
Código fuente
Toda la configuración, con instrucciones detalladas de implementación y todo el código se puede encontrar en Serverless Handbook PEP y PDP
Palabras finales
¡No olvides seguirme en LinkedIn y X para más contenido, y lee el resto de mis Blogs
Como dice Werner! ¡Ahora ve a construir!