serverless, aws, saas, IoT

Construyendo un BBQ conectado serverless como SaaS - Parte 4 - AuthZ

2024-09-20
This post cover image
aws cloud serverless bbq iot saas

Este archivo ha sido traducido automaticamente por IA, pueden ocurrir errores

Ya hemos abordado la aislación de datos y la seguridad a nivel de fila en un entorno SaaS multitenante. Ahora es el momento de ver cómo manejar la autorización, el proceso de controlar el acceso a los datos y la funcionalidad según quién es el usuario y lo que está autorizado para hacer.

En esta publicación, exploraremos la arquitectura de un Punto de Decisión de Política (PDP) centralizado con Puntos de Aplicación de Política (PEPs) distribuidos para garantizar una autorización consistente y segura en toda su plataforma SaaS. También desglosaremos las diferencias clave entre Autenticación (AuthN) y Autorización (AuthZ).

Si aún no la ha revisado, aquí están la primera parte, la segunda parte y la tercera parte

Autenticación (AuthN) vs. Autorización (AuthZ)

Antes de entrar en la construcción de la arquitectura, necesitamos establecer un entendimiento común. Es crucial distinguir entre Autenticación y Autorización, dos términos que a menudo se confunden, y que he tenido que explicar en muchas ocasiones, pero que cumplen propósitos muy diferentes.

Autenticación (AuthN)

La autenticación se trata de verificar la identidad. Responde a la pregunta, ¿Quién eres? Cuando un usuario inicia sesión en nuestra aplicación SaaS, la autenticación asegura que sean quien dicen ser. Esto podría implicar algo tan simple como un nombre de usuario y contraseña o una autenticación multifactor (MFA) más compleja.

Estamos utilizando Amazon Cognito User Pools para nuestra autenticación en esta serie.

Autorización (AuthZ)

Una vez que la identidad del usuario está autenticada, entra en juego la autorización. Este proceso responde a la pregunta, ¿Qué puedes hacer? La autorización determina qué recursos, datos y acciones está permitido acceder a un usuario según sus roles y permisos.

En un entorno multitenante, hacer bien la autorización es importante. Necesitamos asegurarnos de que los usuarios solo puedan acceder a los datos y la funcionalidad relevante para su inquilino y rol. Aquí es donde entra en juego un sistema de autorización robusto y escalable.

PDP centralizado y PEPs distribuidos: La columna vertebral de la autorización

Ahora que hemos aclarado la diferencia entre AuthN y AuthZ, centrémonos en construir un sistema de autorización utilizando un PDP centralizado y PEPs distribuidos.

PDP centralizado

Un Punto de Decisión de Política (PDP) es donde se toman todas las decisiones de autorización. Es el cerebro del sistema de autorización, evaluando cada solicitud contra políticas o roles predefinidos y determinando si permitir o denegar la solicitud. Hay algunos beneficios claros con un PDP centralizado:

Consistencia: Cada solicitud se evalúa contra el mismo conjunto de políticas, asegurando una toma de decisiones consistente en todos nuestros sistemas.
Gestión y cumplimiento: Con todas las políticas gestionadas en un solo lugar, las actualizaciones y las auditorías se facilitan. Registrar decisiones para fines de auditoría es importante para cumplir con algunas normativas.

Sin embargo, un inconveniente sería el aumento de la latencia. Enrutar cada solicitud de autorización a través de un solo PDP puede ralentizar las cosas, especialmente en un sistema distribuido.

Usando Amazon Verified Permissions (AVP) se requiere un PDP centralizado.

PEPs distribuidos

Los Puntos de Aplicación de Política (PEPs) son donde se hacen cumplir las decisiones de autorización tomadas por el PDP. Estos puntos están distribuidos por todo nuestro sistema. Nuestro Autorizador basado en Lambda es un ejemplo de PEP. La puerta principal de nuestros microservicios también puede actuar como PEP y llamar a nuestro PDP.

Tenemos algunos beneficios al ejecutar nuestros PEPs de manera distribuida.

Reducción de la latencia: Al colocar los PEPs cerca de donde se deben hacer cumplir las decisiones, podemos reducir la latencia, y con una estrategia de caché esto se puede reducir aún más.
Escalabilidad: A medida que su sistema crece, los PEPs distribuidos aseguran que ningún punto único se convierta en un cuello de botella, especialmente con el caché habilitado.

Visión general de la arquitectura

En nuestra nueva arquitectura de autorización introduciremos un nuevo servicio de autorización, que será nuestro PDP central. La lógica de autorización se moverá de nuestro Autorizador Lambda anterior al PDP, en su lugar nuestro Autorizador Lambda ahora llamará al PDP que devolverá la decisión de acceso.

Imagen mostrando la visión general de la creación de inquilinos

Crear el servicio de autorización (PDP)

Lo primero que debemos hacer es crear nuestro Servicio de Autorización (PDP), como se muestra, este será un API Gateway y una función Lambda. Un enfoque interesante que podríamos usar es eliminar el API Gateway y solo usar URLs de funciones Lambda. Sin embargo, para una mayor extensibilidad posterior decidí usar un API Gateway.


AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Connected BBQ Application Tenant Service
Parameters:
  ApplicationName:
    Type: String
    Description: Name of owning application
    Default: bbq-iot
  UserPoolStackName:
    Type: String
    Description: The name of the Stack with the Cognito User Pool

Globals:
  Function:
    Timeout: 30
    MemorySize: 2048
    Runtime: python3.12

Resources:
  LambdaPDPFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/AuthZ
      Handler: authz.handler
      Environment:
        Variables:
          JWKS_URL:
            Fn::ImportValue: !Sub ${UserPoolStackName}:jwks-url
          AUDIENCE:
            Fn::ImportValue: !Sub ${UserPoolStackName}:app-audience
      Events:
        AuthZ:
          Type: Api
          Properties:
            Path: /authz
            Method: post
            RestApiId: !Ref PDPApi
            Auth:
              AuthorizationType: AWS_IAM

  PDPApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: !Sub ${ApplicationName}-pdp-api
      StageName: prod
      EndpointConfiguration: REGIONAL
      Cors:
        AllowMethods: "'GET,PUT,POST,DELETE,OPTIONS'"
        AllowHeaders: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"
        AllowOrigin: "'*'"
      Auth:
        DefaultAuthorizer: AWS_IAM

Outputs:
  PDPApiID:
    Value: !Ref PDPApi
    Description: The ID of the PDP API
    Export:
      Name: !Sub ${AWS::StackName}:pdp-api-id
  

Nuestro API usará AWS IAM para la autorización máquina-a-máquina, al igual que nuestro API de administración que creamos en nuestra parte anterior.

A continuación, movemos nuestra lógica de AuthZ de nuestro Autorizador Lambda a nuestra nueva implementación de PDP. Manejaremos tanto la autorización para el acceso de usuarios como de inquilinos e introduciremos dos nuevos recursos tenant y tenantUser.


import os
import json
import jwt
from jwt import PyJWKClient


def handler(event, context):
    data = json.loads(event["body"])
    jwt_token = data["jwt_token"]

    try:
        jwks_url = os.environ["JWKS_URL"]

        jwks_client = PyJWKClient(jwks_url)
        signing_key = jwks_client.get_signing_key_from_jwt(jwt_token)

        decoded_token = jwt.decode(
            jwt_token,
            signing_key.key,
            algorithms=["RS256"],
            audience=os.environ["AUDIENCE"],
        )

		# Verificar el recurso y tomar una decisión de AuthZ
        if data["resource"] == "tenant":
            tenant_id = data["tenant_id"]
            token_tenant_id = decoded_token.get("tenant")

            if token_tenant_id == tenant_id:
                response_body = generate_access(
                    "Allow", data["action"], data["resource"]
                )
                return {
                    "statusCode": 200,
                    "body": json.dumps(response_body),
                    "headers": {"Content-Type": "application/json"},
                }
        elif data["resource"] == "tenantUser":
            user_id = data["user_id"]
            token_user_id = decoded_token.get("cognito:username")

            if token_user_id == user_id:
                response_body = generate_access(
                    "Allow", data["action"], data["resource"]
                )
                return {
                    "statusCode": 200,
                    "body": json.dumps(response_body),
                    "headers": {"Content-Type": "application/json"},
                }

    except Exception as e:
        print(f"Authorization error: {str(e)}")

    # Generar una respuesta predeterminada que deniega el acceso
    response_body = generate_access("Deny", data["action"], data["resource"])

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


def generate_access(effect, action, resource):
    auth_response = {
        "effect": effect,
        "action": action,
        "resource": resource,
    }
    return auth_response

A continuación, por supuesto, necesitamos actualizar el Autorizador Lambda para que llame a nuestro PDP para la autorización.


api_endpoint = os.environ.get("PDP_AUTHZ_API_ENDPOINT")

def handler(event, context):
    token = event["headers"].get("authorization", "")

    if not token:
        raise Exception("Unauthorized")

    token = token.replace("Bearer ", "")

    try:
        
		......

        path_tenant_id = event["pathParameters"]["tenantId"]

        session = boto3.Session()
        credentials = session.get_credentials().get_frozen_credentials()

        region = os.environ["AWS_REGION"]
        auth = AWS4Auth(
            credentials.access_key,
            credentials.secret_key,
            region,
            "execute-api",
            session_token=credentials.token,
        )

        data = {
            "jwt_token": token,
            "resource": "tenant",
            "action": "read",
            "resource_path": event["path"],
            "tenant_id": event["pathParameters"]["tenantId"],
        }
        headers = {"Content-type": "application/json"}
        response = requests.post(
            api_endpoint + "/authz", data=json.dumps(data), headers=headers, auth=auth
        )

		......

    except Exception as e:
        print(f"Authorization error: {str(e)}")

Resumen

Con esos cambios, hemos movido nuestra autorización a un PDP centralizado con una configuración de PEP distribuida. Esto crea una buena base para seguir evolucionando nuestro AuthZ. En publicaciones futuras introduciremos un RBAC (Control de Acceso Basado en Roles) y ABAC (Control de Acceso Basado en Atributos), también pasaremos a usar Amazon Verified permissions.

Obtenga el código

La configuración completa con todo el código está disponible en Serverless Handbook

Palabras finales

En esta publicación, hemos visto la arquitectura de un sistema de autorización centralizado usando un PDP y PEPs distribuidos. También hemos destacado las diferencias entre Autenticación (AuthN) y Autorización (AuthZ).

En las próximas publicaciones comenzaremos a ver el onboarding de dispositivos y datos, ¡manténgase atento!

Consulte mi Manual serverless para obtener el código de la solución construida en esta serie de publicaciones.

¡No olvide seguirme en LinkedIn y X para más contenido, y lea el resto de mis Blogs

Como dice Werner ¡Ahora vaya a construir!

If this saved you an afternoon, you can buy me a coffee.