aws, authz, serverless

PEP und PDP für sichere Autorisierung mit AVP

2025-02-20
This post cover image
aws cloud serverless AuthZ

Questo file e stato tradotto automaticamente dall'IA, potrebbero verificarsi errori

Im ersten Teil, PEP und PDP für sichere Autorisierung mit Cognito, habe ich das Konzept von Policy Enforcement Points (PEPs) und Policy Decision Points (PDPs) eingeführt, indem ich Lambda Authorizer in API Gateway als PEP und einen Lambda-basierten Dienst als PDP verwendet habe. Die Autorisierungsentscheidung basierte auf den Rollen, die Benutzer innehaben, dargestellt durch Cognito-Gruppen, wobei die Berechtigungen in einer DynamoDB-Tabelle abgebildet waren.

Wenn wir ein PEP- und PDP-Setup in einer Multi-Tenant-SaaS-Lösung betreiben, brauchen wir sicherlich etwas Leistungsstärkeres als eine DynamoDB-Tabelle mit Berechtigungen. Daher werden wir in diesem Teil die Verwendung von Amazon Verified Permissions (AVP) als Grundlage für unseren PDP betrachten. Dies wäre eine großartige Wahl, um Autorisierung in SaaS durchzuführen.

Was ist Amazon Verified Permissions?

Amazon Verified Permissions (AVP) ist ein vollständig verwalteter Dienst, der die Implementierung von feingranulierter Zugriffskontrolle in unseren Anwendungen vereinfacht. AVP fungiert als zentraler Policy Decision Point (PDP), an dem Policies ausgewertet und Autorisierungsentscheidungen auf Basis der Attribute der Anfrage, Benutzerrollen und ähnlichem getroffen werden. AVP unterstützt nicht nur rollenbasierte Zugriffskontrolle (RBAC), sondern auch attributbasierte Zugriffskontrolle (ABAC) und andere dynamische Policies, was flexiblere und granularere Autorisierungsentscheidungen ermöglicht.

Mit AVP muss unsere Lambda-Funktion (die als PDP fungiert) Policies nicht mehr manuell parsen und auswerten, und wir müssen Berechtigungszuordnungen nicht in einer DynamoDB-Tabelle speichern. Stattdessen definieren und verwalten wir unsere Policies direkt in AVP, das dann die Autorisierungsentscheidung für uns trifft.

Alternative Open-Source-Lösungen zu AVP

Während AVP eine ausgezeichnete Wahl für Anwendungen ist, insbesondere wenn sie in AWS ausgeführt werden, ist es nicht die einzige Lösung für die Implementierung von feingranulierter Autorisierung. Eine Alternative ist Oso, ein Open-Source-Autorisierungs-Framework, das entwickelt wurde, um flexible, policybasierte Zugriffskontrolle bereitzustellen. Oso ermöglicht es uns, Autorisierungslogik in einer deklarativen Sprache zu definieren und sie in unseren Anwendungen in verschiedenen Umgebungen zu integrieren. Die Wahl zwischen AVP und Oso hängt von unserem spezifischen Anwendungsfall, unserer Infrastruktur usw. ab.

Zwischenspeichern von Autorisierungsentscheidungen

Nachdem wir AVP als zentralen Teil unseres PDP betrachtet haben, sollten wir über Caching nachdenken. Es gibt Vor- und Nachteile beim Caching, zum Beispiel cachet AWS IAM keine Entscheidungen, was sicherstellt, dass Updates von Policies sofort wirksam werden. Mit einem Cache können wir die Anzahl der Aufrufe an AVP reduzieren und damit Kosten senken. Wir können Entscheidungen entweder im Client oder in unseren Backend-Systemen cachen. Ich werde in diesem Beitrag nicht tiefer auf dieses Thema eingehen, als dass wir in dieser Lösung einen externen Cache in unserem Backend-System verwenden werden. Der Cache wird in DynamoDB platziert. Der Grund, den Cache in einer externen Quelle wie DynamoDB statt im Lambda-Funktionsspeicher zu platzieren, ist, dass alle Aufrufe der Lambda-Funktion vom Cache profitieren. Mit einem solchen Cache benötigen wir eine Möglichkeit, den Cache zu leeren, wenn ein Berechtigungssatz für einen Benutzer geändert wird. Ich werde in diesem Beitrag kein Cache-Invalidierungsmechanismus implementieren.

AVP und die Cedar-Policy-Sprache verstehen

Amazon Verified Permissions (AVP) verwendet Cedar, eine speziell entwickelte Policy-as-Code-Sprache für feingranulierte Autorisierung. Cedar ermöglicht es uns, Zugriffskontroll-Policies zu definieren und durchzusetzen, die festlegen, wer welche Aktionen auf welchen Ressourcen ausführen kann.

Cedar-Policies sind deklarativ, was bedeutet, dass sie die Berechtigungen explizit festlegen, ohne prozedurale Logik zu erfordern. Sie sind einfach zu lesen und unterstützen leistungsstarke Bedingungen und attributbasierte Zugriffskontrolle (ABAC).

Einfaches Cedar-Policy-Beispiel

Beginnen wir mit einer einfachen RBAC-Policy (Role-Based Access Control), die Benutzern in der Gruppe "Admin" ermöglicht, bestimmte Aktionen auf allen Ressourcen durchzuführen

permit (
    principal in UserGroup::"Admin",
    action in [
        Action::"create",
        Action::"read",
        Action::"update",
        Action::"delete"
    ],
    resource
);

Diese Policy bedeutet, dass jedes principal (Benutzer), das zur UserGroup Admin gehört, berechtigt ist, die aufgeführten Aktionen auf allen Ressourcen auszuführen, da die Ressource nicht eingeschränkt ist.

Ein zweites Beispiel, bei dem wir uns einer ABAC-Methode nähern, wäre.

permit (
    principal in UserGroup::"JimmysFriends",
    action == Action::"viewPhoto",
    resource
)
when {
    resource.tags.contains("Shared")
};

Dies bedeutet, dass Benutzer in JimmysFriends Fotos anzeigen können, wenn das Foto das Tag Shared hat.

In einem Multi-Tenant-SaaS-System möchten wir möglicherweise den Zugriff auf Daten einschränken, die einem bestimmten Tenant gehören. Cedar ermöglicht attributbasierte Bedingungen dafür.

permit (
    principal,
    action == Action::"view",
    resource
)
when {
    principal.tenant == resource.tenant
};

Dies stellt sicher, dass der Benutzer nur Ressourcen anzeigen kann, die zum gleichen Tenant gehören.

Bei der Implementierung für unseren AVP-basierten PDP werden wir RBAC und eine Policy ähnlich dem ersten Beispiel verwenden.

AVP für skalierbare und flexible Zugriffskontrolle verwenden

Durch die Integration von (AVP) als Teil unseres zentralen PDP haben wir unsere Policy-Verwaltung vereinfacht und die Skalierbarkeit und Flexibilität des Autorisierungssystems verbessert. Die Unterstützung von AVP für dynamische Policies und feingranulierte Zugriffskontrolle sind leistungsstarke Werkzeuge für jede SaaS-Anwendung.

Implementierung

Mit der Einführung von AVP konvertieren wir unseren PDP, um AVP für Role Based Access (RBAC) anstelle einer DynamoDB-Berechtigungszuordnung zu verwenden. Wir werden einfach einen zweiten PDP in unsere Lösung deployen und dann umstellen, damit unser PEP den neuen PDP basierend auf AVP verwendet. Dieses gesamte Setup basiert auf der in Teil eins eingeführten Lösung, PEP und PDP für sichere Autorisierung mit Cognito,, und als Voraussetzung sollte diese Lösung deployt sein. Die gesamte Lösung finden Sie auf Serverless Handbook PEP und PDP.

Architekturüberblick

Nur als Erinnerung, der gesamte Code und die gesamte Architektur finden Sie auf Serverless Handbook PEP und PDP

Betrachten wir den Überblick über die Architektur und den Aufrufablauf, sehen wir, dass es keine größeren Änderungen gibt, sondern dass wir statt einer Berechtigungszuordnung von DynamoDB AVP aufrufen und den Dienst seine Policy-Engine zur Allow/Deny-Entscheidung verwenden lassen.

Bild zeigt den Architekturüberblick

Um den Ablauf während eines API-Zugriffs besser zu verstehen.

Bild zeigt den Aufrufablauf

Setup und Deploy AVP-basierter PDP

Zuerst müssen wir alle Ressourcen deployen, die von AVP benötigt werden. Dazu gehört, einen Policy Store mit unserem Policy-Schema zu erstellen. Die Policies für unsere drei verschiedenen Rollen mit einer Cedar-basierten Policy, um zu bestimmen, was die Rolle berechtigt ist zu tun, und wir müssen unsere Identity Source einrichten.

Diese Vorlage kann ziemlich lang sein, da die Cedar-Policies und das Schema tendenziell recht umfangreich sind. Daher wurden Teile dieser Vorlage weggelassen, besuchen Sie daher Serverless Handbook PEP und PDP für die vollständige Vorlage.

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"

Autorisierungslogik

Der PDP Lambda erhält das jwt-token, die action und die resource vom PEP, unserem Lambda Authorizer in der API Gateway. Die Funktion erhält das principal, das Subjekt, aus dem JWT-Token und prüft, ob es eine gecachte Entscheidung gibt. Wenn es keine gibt, ruft sie AVP mit allen Informationen auf, um eine Autorisierungsentscheidung zu erhalten.

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 ist in Millisekunden
            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

PEP aktualisieren, um AVP-basierten PDP zu verwenden

Um unseren PEP zu aktualisieren, damit er den neuen AVP-basierten PDP anstelle des DynamoDB-basierten verwendet, navigieren Sie zum Ordner API und ändern Sie die Datei samconfig.yaml, aktualisieren Sie den Wert von PDP auf PDPStackName=pep-pdp-cognito-pdp-auth-service-avp und deployen Sie dann den API-Teil der Lösung neu. Dies wird nun den verwendeten PDP austauschen.

Testen

Um das Setup von AVP zu testen, können wir zum AVP-Teil der Konsole navigieren.

Unter Policy Stores sollten Sie den Policy Store sehen, der für unseren PDP erstellt wurde.

Bild zeigt die Policy Store-Liste

Wenn Sie auf die ID des Policy Store klicken und Policies im Menü auswählen, sehen wir die Liste der drei erstellten Policies.

Bild zeigt die Policy-Liste

Wenn Sie die Riders-Policy auswählen, können wir jetzt die erstellte Policy inspizieren.

Bild zeigt die Policy-Liste

Wenn Sie Schema im Menü auswählen, können wir das erstellte Schema in einer visuellen Form inspizieren.

Bild zeigt das Policy-Schema

Bild zeigt das Policy-Schema

Nun können wir zum Test Bench navigieren, um unsere Policies zu testen, füllen Sie die Informationen wie im Bild unten gezeigt aus. Die Gruppe muss mit der Cognito User Pool-ID präfixiert sein und dem Muster <COGNITO_USER_POOL_ID>|<GROUP_NAME> folgen

Bild zeigt die Testbank

Wenn wir die Aktion /get trainers auswählen und auf Run Authorization request klicken, sollten wir eine Ablehnung zurückbekommen, da die Trainer-Rolle keinen Zugriff auf diese Action hat

Bild zeigt die Testbank

Wenn wir zur Aktion /get trainer wechseln, sollten wir stattdessen eine Zulassung zurückbekommen.

Bild zeigt die Testbank

Zusammenfassung und Fazit

Die Implementierung von PEP und PDP in unserem Autorisierungsfluss bietet eine hoch skalierbare, flexible und sichere Möglichkeit, den Zugriff auf Ressourcen zu kontrollieren. Durch die Nutzung von AWS Lambda und API Gateway können wir ein serverloses Autorisierungssystem erstellen, das Authentifizierungs- und Autorisierungsprobleme trennt, mit der Nachfrage skaliert und die Policy-Verwaltung vereinfacht.

Mit der Ergänzung von Role-Based Access Control und Amazon Verified Permissions (AVP), kombiniert mit Caching für verbesserte Leistung, können wir eine Autorisierungslösung erstellen, die sowohl den aktuellen als auch den zukünftigen Anforderungen entspricht. Die Verwendung von AVP in unseren SaaS-Lösungen bietet uns eine sehr leistungsstarke Möglichkeit, Multi-Tenancy zu handhaben.

Viel Spaß beim Codieren und bleiben Sie sicher!

Quellcode

Das gesamte Setup, mit detaillierten Deployungsanweisungen und dem gesamten Code, finden Sie auf Serverless Handbook PEP und PDP

Abschließende Worte

Vergessen Sie nicht, mir auf LinkedIn und X zu folgen, um weitere Inhalte zu erhalten, und lesen Sie meine übrigen Blogs

Wie Werner sagt! Jetzt loslegen!