PEP y PDP para Autorización Segura con AVP y ABAC

Este archivo ha sido traducido automáticamente 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íticas (PEPs) y Puntos de Decisión de Políticas (PDPs) usando Lambda Authorizer en API Gateway como PEP y un servicio basado en Lambda como PDP. La decisión de autorización se basaba en los roles que los usuarios tenían, representados por grupos de Cognito, con los permisos mapeados en una tabla de DynamoDB.
En la segunda parte, PEP y PDP para Autorización Segura con AVP, reemplazamos el mapeo de permisos de DynamoDB con Amazon Verified Permissions (AVP) e implementamos un sistema de Control de Acceso Basado en Roles (RBAC) usando políticas Cedar. Esto nos dio una forma mucho más poderosa de manejar la autorización, especialmente para aplicaciones SaaS multi-inquilino.
Ahora, en esta tercera parte, llevaremos nuestro sistema de autorización al siguiente nivel agregando Control de Acceso Basado en Atributos (ABAC) junto con RBAC usando AVP. Esta combinación proporciona decisiones de autorización aún más dinámicas y conscientes del contexto, que es exactamente lo que necesitamos para aplicaciones multi-inquilino complejas.
Entendiendo el Control de Acceso Basado en Atributos (ABAC)
Entonces, ¿qué es exactamente ABAC? En su esencia, ABAC es una estrategia de autorización que toma decisiones basadas en atributos asociados con usuarios, recursos, acciones y el entorno. A diferencia de RBAC, que otorga permisos únicamente basados en roles, ABAC evalúa varios atributos para tomar decisiones de autorización más detalladas.
Piénsalo de esta manera: RBAC es como tener diferentes llaves (roles) que abren diferentes puertas (recursos). ABAC, por otro lado, es como tener una cerradura inteligente que considera no solo quién eres, sino también de dónde vienes, qué hora es, qué estás tratando de hacer e incluso lo que está sucediendo a tu alrededor antes de decidir si dejarte entrar.
Como ejemplo, algunos atributos comunes utilizados en ABAC podrían ser:
Atributos de usuario: Departamento, ubicación, nivel de autorización, antigüedad
Atributos de recurso: Clasificación, propietario, nivel de sensibilidad
Atributos de acción: Hora del día, estado de encriptación, método de acceso
Atributos ambientales: Tipo de dispositivo, ubicación de red, nivel de seguridad
Por ejemplo, una política ABAC podría establecer que Los científicos de datos pueden acceder a datos sensibles, pero solo desde redes corporativas, durante el horario laboral y si han completado la capacitación de seguridad en los últimos 6 meses.
¿Por qué Combinar RBAC y ABAC?
Pero si ABAC es tan poderoso, ¿por qué no lo usamos en lugar de RBAC? Diría que RBAC es realmente simple de entender e implementar, lo que lo hace excelente para necesidades básicas de autorización. Sin embargo, cuando los permisos necesitan ser más granulares o dependientes del contexto, puede volverse muy difícil y complejo de manejar.
Por otro lado, ABAC ofrece una flexibilidad increíble, pero puede ser muy complejo de implementar desde cero y más difícil de razonar. Al combinar ambos enfoques, obtenemos lo mejor de ambos mundos!
En nuestro ejemplo de carreras de unicornios, podemos usar roles para definir patrones de acceso amplios (Admin, Entrenador, Jinete), luego usaremos un atributo dataAccess
para restringir el acceso a conjuntos de datos específicos dentro de esos roles.
Implementando ABAC en Amazon Verified Permissions
AVP con su lenguaje de políticas Cedar es perfectamente adecuado para implementar ABAC. Las políticas Cedar pueden evaluar condiciones basadas en atributos del principal (usuario), recurso, acción y contexto.
Mapeo de Tokens en AVP - Un Concepto Crítico
Una parte importante de la implementación de ABAC es entender cómo los reclamos de tokens se mapean a tu esquema de políticas Cedar. AVP maneja esto de manera diferente dependiendo de si estás usando tokens de ID o tokens de acceso.
Token de ID: Los reclamos se mapean a atributos de la entidad principal
que representa al usuario
Token de Acceso: Los reclamos se mapean al objeto context.token
en la evaluación de políticas
Esta diferencia es súper importante al escribir políticas Cedar porque afecta cómo accedes a los atributos en las condiciones de tu política!
Por ejemplo, con un token de ID, una política se vería algo así
permit (principal, action, resource)
when { principal.department == "Engineering" };
Pero con un token de acceso, necesitamos usar el objeto de contexto y no el principal
permit (principal, action, resource)
when { context.token["department"] == "Engineering" };
En nuestra implementación, usaremos el enfoque de token de acceso ya que creo que proporciona una separación más limpia entre identidad (quién es el usuario) y permisos de acceso y atributos (qué pueden hacer y qué propiedades tienen).
Por Qué Usar Tokens de Acceso para Autorización
Mientras que los tokens de ID están principalmente destinados a la autenticación (demostrar identidad), los tokens de acceso están diseñados específicamente para la autorización (determinar permisos). Veo varias ventajas al usar tokens de acceso:
Separación de preocupaciones: Los detalles de autenticación permanecen en los tokens de ID, mientras que los detalles de autorización viven en los tokens de acceso
Tamaño de token reducido: Los tokens de ID no estarán inflados con atributos de autorización
Vida útil independiente: La expiración del token de acceso puede ser más corta que la expiración del token de ID
Revocación de token más fácil: Puedes revocar el acceso sin afectar la autenticación
En nuestro PDP basado en AVP, usaremos tokens de acceso y mapearemos reclamos personalizados al contexto para mejores decisiones de autorización.
Implementando AVP con RBAC y ABAC
Ahora que entendemos los conceptos, ¡pongámonos manos a la obra e implementemos nuestro sistema de autorización mejorado!
Arquitectura
Nuestra arquitectura permanece similar pero con algunos cambios. Donde agregamos una nueva función Lambda para poder enriquecer nuestro token de acceso con reclamos personalizados.
Esto nos dará un flujo de llamadas actualizado como este.
Los cambios que necesitamos hacer serían.
En primer lugar, necesitamos agregar dataAccess
como un reclamo personalizado en nuestro token de acceso, para hacer eso debemos configurar una función Lambda de Pre-generación de Token, que será invocada por Cognito.
Segundo, necesitamos actualizar nuestro esquema y agregar el atributo dataAccess
en cada acción que queramos que la política pueda evaluar de acuerdo con ABAC.
Por último, necesitamos actualizar nuestras políticas Cedar para que sean conscientes del contexto y evalúen tanto el rol (RBAC) como los atributos (ABAC)
Agregando Atributos Personalizados a los Tokens de Acceso
Para implementar ABAC, necesitamos una forma de agregar atributos personalizados a nuestros tokens de acceso. Usaremos el disparador de Lambda de Pre-Generación de Token de Cognito para este propósito, y necesitamos usar la versión 2 o 3 del evento. Con la versión 1 solo podemos modificar el Token de ID.
A continuación se muestra una implementación que agrega el atributo dataAccess
de los atributos de usuario al token de acceso:
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 función verifica si el usuario tiene un atributo custom:dataAccess
. Si se encuentra, divide los valores separados por comas en una matriz y los agrega al token de acceso. De esta manera, nuestras políticas AVP pueden verificar si el usuario tiene acceso a categorías de datos específicas.
Para que Cognito llame a nuestra función, necesitamos configurar los Disparadores Lambda del Grupo de Usuarios.
PreTokenGenerationFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: Lambda/PreTokenGeneration
Handler: index.handler
Runtime: python3.12
Architectures:
- x86_64
MemorySize: 128
Description: Una función Lambda que agrega atributos personalizados al token de acceso 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 con Atributos de Contexto
Nuestro esquema Cedar define la estructura de principales, recursos, acciones y sus relaciones. Para ABAC, necesitamos actualizar el esquema para incluir atributos de contexto:
{
"${AVPNameSpace}": {
"actions": {
"get /rider": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {
"dataAccess": {
"type": "String",
"required": true
}
}
},
"principalTypes": ["CognitoUser"],
"resourceTypes": ["Application"]
}
}
// Otras acciones...
}
}
}
Este esquema nos permite usar el atributo dataAccess
en nuestras políticas de autorización.
Políticas que Combinan RBAC y ABAC
Nuestras políticas Cedar ahora combinan lógica RBAC (basada en roles) y ABAC (basada en atributos):
permit(
principal in ${AVPNameSpace}::CognitoUserGroup::"${UserPoolId}|Admin",
action in [
${AVPNameSpace}::Action::"get /rider",
${AVPNameSpace}::Action::"get /riders",
${AVPNameSpace}::Action::"get /trainer",
// más acciones...
],
resource
)
when {
// Condición ABAC usando atributos
context.dataAccess == "" ||
(context.token has "custom:dataAccess" &&
context.token["custom:dataAccess"].contains(context.dataAccess))
};
Esta política permite a los usuarios del grupo Admin acceder a los puntos finales (parte RBAC), pero solo si una de estas condiciones es verdadera (parte ABAC):
- No se necesita una verificación específica de acceso a datos (context.dataAccess vacío), O
- El usuario tiene el atributo de acceso a datos requerido en su token
Desglosemos un poco la condición para una mejor comprensión.
- Si
context.dataAccess
está vacío, no aplicamos restricciones de acceso a datos - De lo contrario, verificamos si el token del usuario tiene el reclamo
custom:dataAccess
que contiene el valor requerido
Lógica de Autorización Consciente del Contexto
Nuestra función Lambda PDP ahora maneja la autorización basada en contexto. Extrae atributos relevantes de las solicitudes y los incluye en la llamada de autorización AVP. La siguiente función ha sido simplificada y se han eliminado algunas partes, consulta el código fuente para una versión 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"]
# Llamar a AVP con contexto para la decisión de autorización
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
)
# Almacenar en caché el resultado con el hash del contexto
store_auth_cache(user_principal, action_id, context_hash, auth_response)
# Generar la respuesta
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"Error al validar permisos: {e}")
response_body = generate_access(parsed_token["sub"], "Deny", action, resource)
return {
"statusCode": 200,
"body": json.dumps(response_body),
"headers": {"Content-Type": "application/json"},
}
Con esta implementación, nuestro PDP ahora evalúa tanto reglas basadas en roles (RBAC) como condiciones basadas en atributos (ABAC).
Almacenamiento en Caché Consciente del Contexto
Uno de los desafíos de agregar ABAC es que las decisiones de autorización ahora dependen no solo de quién eres y qué estás tratando de hacer, sino también del contexto. Esto significa que nuestra estrategia de almacenamiento en caché necesita ser actualizada.
Incluiremos información de contexto en nuestra clave de caché creando un hash de los 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á en milisegundos
return None
return item.get("Effect")["S"]
except Exception as e:
print(f"Error al obtener la caché de autenticación: {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"Error al almacenar la caché de autenticación: {e}")
Al incluir un hash del contexto en nuestra clave de caché, nos aseguramos de que las decisiones de autorización se almacenen y recuperen correctamente en caché basándose tanto en el rol del usuario como en los atributos de contexto relevantes.
Modificando el PEP para Pasar Contexto
Nuestro PEP (Lambda Authorizer) también necesita una pequeña actualización para pasar información de contexto al PDP, esta es una implementación muy básica, y en un sistema de producción seguramente tendrías algo más sofisticado, como mirar las etiquetas de recursos en los recursos de 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 de la función handler
resource_tags = get_resource_tags_for_path(path)
data = {
"jwt_token": token,
"resource": path,
"action": method,
"resource_tags": resource_tags,
}
Esta función determina a qué categoría de datos pertenece un recurso basándose en su ruta y pasa esa información al PDP para una autorización consciente del contexto.
Probando la Implementación de ABAC
¡Ahora que tenemos nuestra implementación en su lugar, es hora de probarla! Aquí es cómo podemos probar nuestra solución combinada de RBAC + ABAC.
Primero necesitamos crear o actualizar un usuario en diferentes grupos de Cognito (Admin, Entrenador, Jinete). También necesitamos asignar diferentes atributos dataAccess
a estos usuarios (por ejemplo, "Unicornio", "Carreras").
Luego navegamos a la página web desplegada con la distribución de CloudFront e inspeccionamos los tokens JWT, las cookies.
Si copiamos el token de acceso y lo decodificamos, yo uso jwt.io, podemos ver que mi usuario tiene el reclamo custom:dataAccess
que nuestro PEP y PDP usarán más tarde para los permisos.
Ahora podemos usar una herramienta como Bruno o Postman para incluir el token de acceso en las llamadas a la API. Por supuesto, dependiendo de cómo hayas configurado el acceso, deberías obtener ya sea un Allow o Deny del PDP resultando en diferentes resultados de la API. A continuación se muestran algunas vistas de ejemplo donde uso Bruno para llamar a la API con y sin acceso.
Conclusión
Al combinar RBAC y ABAC con Amazon Verified Permissions, hemos creado un sistema de autorización poderoso y flexible que puede manejar requisitos complejos de control de acceso. Este enfoque proporciona la simplicidad del acceso basado en roles mientras permite el control detallado de decisiones basadas en atributos.
Este enfoque de implementación se puede usar en aplicaciones SaaS multi-inquilino donde la segregación de datos, basada en el inquilino, y el control de acceso consciente del contexto son críticos. Con AVP manejando la evaluación de políticas y nuestra arquitectura sin servidor para la aplicación, tenemos una solución escalable y mantenible para incluso las necesidades de autorización más exigentes.
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 leer el resto de mis Blogs
Como dice Werner! ¡Ahora a 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!