PEP und PDP für sichere Autorisierung mit AVP und ABAC

2025-05-06
This post cover image
#aws
#cloud
#serverless
#AuthZ
Voice provided by Amazon Polly

Diese Datei wurde automatisch von KI übersetzt, es können Fehler auftreten

Im ersten Teil, PEP und PDP für sichere Autorisierung mit Cognito, habe ich das Konzept der Policy Enforcement Points (PEPs) und Policy Decision Points (PDPs) vorgestellt, wobei Lambda Authorizer in API Gateway als PEP und ein Lambda-basierter Dienst als PDP verwendet wurden. Die Autorisierungsentscheidung basierte auf den Rollen der Benutzer, die durch Cognito-Gruppen repräsentiert wurden, wobei die Berechtigungen in einer DynamoDB-Tabelle abgebildet waren.

Im zweiten Teil, PEP und PDP für sichere Autorisierung mit AVP, haben wir die DynamoDB-Berechtigungszuordnung durch Amazon Verified Permissions (AVP) ersetzt und ein Role-Based Access Control (RBAC) System mit Cedar-Richtlinien implementiert. Dies gab uns eine viel leistungsfähigere Möglichkeit, Autorisierungen zu handhaben, insbesondere für Multi-Tenant-SaaS-Anwendungen.

Nun, in diesem dritten Teil, werden wir unser Autorisierungssystem auf die nächste Stufe heben, indem wir Attribute-Based Access Control (ABAC) neben RBAC mit AVP hinzufügen. Diese Kombination ermöglicht noch dynamischere und kontextbewusste Autorisierungsentscheidungen, genau das, was wir für komplexe Multi-Tenant-Anwendungen benötigen.

Verständnis von Attribute-Based Access Control (ABAC)

Was genau ist ABAC? Im Kern ist ABAC eine Autorisierungsstrategie, die Entscheidungen auf der Grundlage von Attributen trifft, die mit Benutzern, Ressourcen, Aktionen und der Umgebung verbunden sind. Im Gegensatz zu RBAC, das Berechtigungen ausschließlich auf der Grundlage von Rollen gewährt, bewertet ABAC verschiedene Attribute, um feinkörnigere Autorisierungsentscheidungen zu treffen.

Stellen Sie es sich so vor - RBAC ist wie verschiedene Schlüssel (Rollen), die verschiedene Türen (Ressourcen) öffnen. ABAC hingegen ist wie ein intelligentes Schloss, das nicht nur berücksichtigt, wer Sie sind, sondern auch, woher Sie kommen, zu welcher Zeit, was Sie tun möchten und sogar, was um Sie herum geschieht, bevor es entscheidet, ob Sie eingelassen werden.

Als Beispiel könnten einige häufig in ABAC verwendete Attribute sein:

Benutzerattribute: Abteilung, Standort, Sicherheitsstufe, Seniorität
Ressourcenattribute: Klassifizierung, Eigentümer, Sensitivitätsstufe
Aktionsattribute: Tageszeit, Verschlüsselungsstatus, Zugriffsmethode
Umgebungsattribute: Gerätetyp, Netzwerkstandort, Sicherheitsstufe

Ein ABAC-Richtlinie könnte beispielsweise festlegen, dass Datenwissenschaftler auf sensible Daten zugreifen können, aber nur aus Unternehmensnetzwerken, während der Geschäftszeiten und wenn sie in den letzten 6 Monaten eine Sicherheitsschulung absolviert haben.

Warum RBAC und ABAC kombinieren?

Aber wenn ABAC so leistungsfähig ist, warum verwenden wir es nicht einfach anstelle von RBAC? Ich würde sagen, dass RBAC wirklich einfach zu verstehen und zu implementieren ist, was es für grundlegende Autorisierungsbedürfnisse großartig macht. Wenn Berechtigungen jedoch feinkörniger oder kontextabhängiger sein müssen, kann es sehr schwierig und komplex zu verwalten werden.

Andererseits bietet ABAC eine erstaunliche Flexibilität, kann aber von Grund auf sehr komplex zu implementieren sein und ist schwieriger zu verstehen. Durch die Kombination beider Ansätze erhalten wir das Beste aus beiden Welten!

In unserem Einhorn-Rennbeispiel können wir Rollen verwenden, um breite Zugriffsmuster zu definieren (Admin, Trainer, Fahrer), dann werden wir ein Attribut dataAccess verwenden, um den Zugriff auf bestimmte Datensätze innerhalb dieser Rollen einzuschränken.

Implementierung von ABAC in Amazon Verified Permissions

AVP mit seiner Cedar-Richtliniensprache eignet sich perfekt für die Implementierung von ABAC. Cedar-Richtlinien können Bedingungen basierend auf Attributen des Principals (Benutzer), der Ressource, der Aktion und des Kontexts auswerten.

Token-Zuordnung in AVP - Ein kritisches Konzept

Ein wichtiger Teil der Implementierung von ABAC ist das Verständnis, wie Token-Claims auf Ihr Cedar-Richtlinienschema abgebildet werden. AVP handhabt dies unterschiedlich, je nachdem, ob Sie ID-Token oder Access-Token verwenden.

ID-Token: Claims werden auf Attribute der principal-Entität abgebildet, die den Benutzer repräsentiert
Access-Token: Claims werden auf das context.token-Objekt in der Richtlinienauswertung abgebildet

Dieser Unterschied ist sehr wichtig beim Schreiben von Cedar-Richtlinien, da er beeinflusst, wie Sie auf Attribute in Ihren Richtlinienbedingungen zugreifen!

Zum Beispiel würde eine Richtlinie mit einem ID-Token so aussehen

permit (principal, action, resource)
when { principal.department == "Engineering" };

Aber mit einem Access-Token müssen wir das Kontextobjekt und nicht den Principal verwenden

permit (principal, action, resource)
when { context.token["department"] == "Engineering" };

In unserer Implementierung werden wir den Access-Token-Ansatz verwenden, da ich denke, dass er eine sauberere Trennung zwischen Identität (wer der Benutzer ist) und Zugriffsberechtigungen und Attributen (was sie tun können und welche Eigenschaften sie haben) bietet.

Warum Access-Token für Autorisierung

Während ID-Token hauptsächlich für die Authentifizierung (Identitätsnachweis) gedacht sind, sind Access-Token speziell für die Autorisierung (Bestimmung von Berechtigungen) konzipiert. Ich sehe mehrere Vorteile bei der Verwendung von Access-Token:

Trennung der Belange: Authentifizierungsdetails bleiben in ID-Token, während Autorisierungsdetails in Access-Token leben
Reduzierte Token-Größe: ID-Token werden nicht mit Autorisierungsattributen aufgebläht
Unabhängige Lebensdauern: Die Gültigkeitsdauer von Access-Token kann kürzer sein als die von ID-Token
Einfachere Token-Widerrufung: Sie können den Zugriff widerrufen, ohne die Authentifizierung zu beeinträchtigen

In unserem AVP-basierten PDP werden wir Access-Token verwenden und benutzerdefinierte Claims auf den Kontext abbilden, um bessere Autorisierungsentscheidungen zu treffen.

Implementierung von AVP mit RBAC und ABAC

Nun, da wir die Konzepte verstehen, lasst uns die Hände schmutzig machen und unser erweitertes Autorisierungssystem implementieren!

Architektur

Unsere Architektur bleibt ähnlich, aber mit einigen Änderungen. Wir fügen eine neue Lambda-Funktion hinzu, um unseren Access-Token mit benutzerdefinierten Claims anzureichern.

Architekturdiagramm, das PEP und PDP mit ABAC zeigt

Dies gibt uns einen aktualisierten Aufruffluss wie diesen.

Aufruffluss PEP und PDP mit ABAC

Die Änderungen, die wir vornehmen müssen, wären.

Zunächst müssen wir dataAccess als benutzerdefinierten Claim in unserem Access-Token hinzufügen, dazu müssen wir eine Pre-Token-Generation-Lambda-Funktion konfigurieren, die von Cognito aufgerufen wird.

Zweitens müssen wir unser Schema aktualisieren und das dataAccess-Attribut zu jeder Aktion hinzufügen, die die Richtlinie gemäß ABAC auswerten soll.

Zuletzt müssen wir unsere Cedar-Richtlinien kontextbewusst machen und sowohl Rollen (RBAC) als auch Attribute (ABAC) auswerten

Hinzufügen benutzerdefinierter Attribute zu Access-Token

Um ABAC zu implementieren, benötigen wir eine Möglichkeit, benutzerdefinierte Attribute zu unseren Access-Token hinzuzufügen. Wir werden Cognitos Pre-Token-Generation-Lambda-Trigger für diesen Zweck verwenden und müssen Version 2 oder 3 des Ereignisses verwenden. Mit Version 1 können wir nur das ID-Token modifizieren.

Unten ist eine Implementierung, die das dataAccess-Attribut aus Benutzerattributen zum Access-Token hinzufügt:

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

Diese Funktion prüft, ob der Benutzer ein custom:dataAccess-Attribut hat. Wenn es gefunden wird, teilt es die durch Kommas getrennten Werte in ein Array auf und fügt sie dem Access-Token hinzu. Auf diese Weise können unsere AVP-Richtlinien überprüfen, ob der Benutzer Zugriff auf bestimmte Datenkategorien hat.

Damit Cognito unsere Funktion aufruft, müssen wir die Lambda-Trigger des Benutzerpools konfigurieren.

  PreTokenGenerationFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/PreTokenGeneration
      Handler: index.handler
      Runtime: python3.12
      Architectures:
        - x86_64
      MemorySize: 128
      Description: Eine Lambda-Funktion, die benutzerdefinierte Attribute zum JWT-Access-Token hinzufügt
      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

Cedar-Richtlinienschema mit Kontextattributen

Unser Cedar-Schema definiert die Struktur von Principals, Ressourcen, Aktionen und ihren Beziehungen. Für ABAC müssen wir das Schema aktualisieren, um Kontextattribute einzuschließen:

{
  "${AVPNameSpace}": {
    "actions": {
      "get /rider": {
        "appliesTo": {
          "context": {
            "type": "Record",
            "attributes": {
              "dataAccess": {
                "type": "String",
                "required": true
              }
            }
          },
          "principalTypes": ["CognitoUser"],
          "resourceTypes": ["Application"]
        }
      }
      // Andere Aktionen...
    }
  }
}

Dieses Schema ermöglicht es uns, das dataAccess-Attribut in unseren Autorisierungsrichtlinien zu verwenden.

Richtlinien, die RBAC und ABAC kombinieren

Unsere Cedar-Richtlinien kombinieren nun RBAC (rollenbasiert) und ABAC (attributbasiert) Logik:

permit(
  principal in ${AVPNameSpace}::CognitoUserGroup::"${UserPoolId}|Admin",
  action in [
    ${AVPNameSpace}::Action::"get /rider",
    ${AVPNameSpace}::Action::"get /riders",
    ${AVPNameSpace}::Action::"get /trainer",
    // mehr Aktionen...
  ],
  resource
)
when {
  // ABAC-Bedingung unter Verwendung von Attributen
  context.dataAccess == "" ||
  (context.token has "custom:dataAccess" &&
   context.token["custom:dataAccess"].contains(context.dataAccess))
};

Diese Richtlinie erlaubt Benutzern in der Admin-Gruppe den Zugriff auf Endpunkte (RBAC-Teil), aber nur, wenn eine dieser Bedingungen wahr ist (ABAC-Teil):

  • Keine spezifische Datenzugriffsprüfung erforderlich (leeres context.dataAccess), ODER
  • Der Benutzer hat das erforderliche Datenzugriffsattribut in seinem Token

Lassen Sie uns die Bedingung ein wenig aufschlüsseln, um sie besser zu verstehen.

  • Wenn context.dataAccess leer ist, erzwingen wir keine Datenzugriffsbeschränkungen
  • Andernfalls prüfen wir, ob das Token des Benutzers den custom:dataAccess-Claim enthält, der den erforderlichen Wert enthält

Kontextbewusste Autorisierungslogik

Unsere PDP-Lambda-Funktion handhabt nun die kontextbasierte Autorisierung. Sie extrahiert relevante Attribute aus Anfragen und schließt sie in den AVP-Autorisierungsaufruf ein. Die folgende Funktion wurde vereinfacht und einige Teile wurden entfernt, überprüfen Sie den Quellcode für eine vollständige Version.

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"]

        # Aufruf von AVP mit Kontext für die Autorisierungsentscheidung
        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
        )

        # Zwischenspeichern des Ergebnisses mit dem Kontext-Hash
        store_auth_cache(user_principal, action_id, context_hash, auth_response)

        # Generieren der Antwort
        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"Fehler bei der Validierung von Berechtigungen: {e}")

    response_body = generate_access(parsed_token["sub"], "Deny", action, resource)

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

Mit dieser Implementierung evaluiert unser PDP nun sowohl rollenbasierte Regeln (RBAC) als auch attributbasierte Bedingungen (ABAC).

Kontextbewusstes Caching

Eine der Herausforderungen bei der Hinzufügung von ABAC ist, dass Autorisierungsentscheidungen nun nicht nur davon abhängen, wer Sie sind und was Sie tun möchten, sondern auch vom Kontext. Dies bedeutet, dass unsere Caching-Strategie aktualisiert werden muss.

Wir werden Kontextinformationen in unserem Cache-Schlüssel einschließen, indem wir einen Hash der Kontextattribute erstellen:

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

        return item.get("Effect")["S"]

    except Exception as e:
        print(f"Fehler beim Abrufen des Auth-Cache: {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"Fehler beim Speichern des Auth-Cache: {e}")

Indem wir einen Hash des Kontexts in unserem Cache-Schlüssel einschließen, stellen wir sicher, dass Autorisierungsentscheidungen korrekt zwischengespeichert und basierend sowohl auf der Rolle des Benutzers als auch auf den relevanten Kontextattributen abgerufen werden.

Ändern des PEP zum Übergeben von Kontext

Unser PEP (Lambda Authorizer) benötigt auch eine kleine Aktualisierung, um Kontextinformationen an das PDP zu übergeben. Dies ist eine sehr grundlegende Implementierung, und in einem Produktionssystem hätten Sie sicherlich etwas Sophistizierteres, wie das Betrachten von Ressourcen-Tags auf AWS-Ressourcen.

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 {}

# Innerhalb der Handler-Funktion
resource_tags = get_resource_tags_for_path(path)

data = {
    "jwt_token": token,
    "resource": path,
    "action": method,
    "resource_tags": resource_tags,
}

Diese Funktion bestimmt, zu welcher Datenkategorie eine Ressource gehört, basierend auf ihrem Pfad, und übergibt diese Informationen an das PDP für eine kontextbewusste Autorisierung.

Testen der ABAC-Implementierung

Nun, da wir unsere Implementierung eingerichtet haben, ist es Zeit, sie zu testen! Hier ist, wie wir unsere kombinierte RBAC + ABAC-Lösung testen können.

Zuerst müssen wir einen Benutzer in verschiedenen Cognito-Gruppen erstellen oder aktualisieren (Admin, Trainer, Fahrer). Wir müssen auch verschiedene dataAccess-Attribute diesen Benutzern zuweisen (z.B. "Unicorn", "Races").

Navigieren Sie dann zur Webseite, die mit der CloudFront-Distribution bereitgestellt wurde, und inspizieren Sie die JWT-Token und Cookies. Bild, das die Cookies zeigt

Wenn wir den Access-Token kopieren und dekodieren, verwende ich jwt.io, können wir sehen, dass mein Benutzer den Claim custom:dataAccess hat, den unser PEP und PDP später für Berechtigungen verwenden werden.

Nun können wir ein Tool wie Bruno oder Postman verwenden, um den Access-Token in die Aufrufe der API einzuschließen. Natürlich sollten Sie, abhängig davon, wie Sie den Zugriff konfiguriert haben, entweder ein Allow oder Deny vom PDP erhalten, was zu unterschiedlichen Ergebnissen von der API führt. Unten sind einige Beispielansichten, wo ich Bruno verwende, um die API mit und ohne Zugriff aufzurufen.

Bild, das Allow zeigt

Bild, das Denied zeigt

Fazit

Durch die Kombination von RBAC und ABAC mit Amazon Verified Permissions haben wir ein leistungsfähiges, flexibles Autorisierungssystem geschaffen, das komplexe Zugriffskontrollanforderungen bewältigen kann. Dieser Ansatz bietet die Einfachheit des rollenbasierten Zugriffs und ermöglicht gleichzeitig die feinkörnige Kontrolle von attributbasierten Entscheidungen.

Dieser Implementierungsansatz kann in Multi-Tenant-SaaS-Anwendungen verwendet werden, bei denen Datentrennung basierend auf dem Tenant und kontextbewusste Zugriffskontrolle kritisch sind. Mit AVP, das die Richtlinienauswertung übernimmt, und unserer serverlosen Architektur für die Durchsetzung haben wir eine skalierbare, wartbare Lösung für selbst die anspruchsvollsten Autorisierungsanforderungen.

Quellcode

Die gesamte Einrichtung, mit detaillierten Bereitstellungsanweisungen und dem gesamten Code, finden Sie auf Serverless Handbook PEP und PDP

Abschließende Worte

Vergessen Sie nicht, mir auf LinkedIn und X für mehr Inhalte zu folgen und den Rest meiner Blogs zu lesen.

Wie Werner sagt! Jetzt bauen Sie los!

Post Quiz

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!