serverless, aws, saas, IoT

Aufbau eines serverlosen verbundenen Grills als SaaS - Teil 4 - AuthZ

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

Diese Datei wurde automatisch von KI ubersetzt, es konnen Fehler auftreten

Wir haben bereits auf Datenisolierung und Zeilenebensicherheit in einer Multi-Tenant-SaaS-Umgebung eingegangen. Nun ist es an der Zeit, uns damit zu befassen, wie Autorisierung gehandhabt wird – der Prozess, der den Zugriff auf Daten und Funktionalitäten steuert, basierend darauf, wer der Benutzer ist und was er tun darf.

In diesem Beitrag werden wir die Architektur eines zentralen Policy Decision Point (PDP) mit verteilten Policy Enforcement Points (PEPs) untersuchen, um eine konsistente und sichere Autorisierung über Ihre SaaS-Plattform hinweg zu sichern. Wir werden auch die wichtigsten Unterschiede zwischen Authentifizierung (AuthN) und Autorisierung (AuthZ) erklären.

Falls Sie es noch nicht getan haben, hier sind Teil eins, Teil zwei und Teil drei

Authentifizierung (AuthN) vs. Autorisierung (AuthZ)

Bevor wir mit dem Aufbau der Architektur beginnen, müssen wir ein gemeinsames Verständnis etablieren. Es ist entscheidend, zwischen Authentifizierung und Autorisierung zu unterscheiden – zwei Begriffe, die oft vermischt werden und die ich schon oft erklären musste, aber sehr unterschiedliche Zwecke erfüllen.

Authentifizierung (AuthN)

Bei der Authentifizierung geht es darum, die Identität zu überprüfen. Sie beantwortet die Frage: Wer bist du? Wenn sich ein Benutzer bei unserer SaaS-Anwendung anmeldet, stellt die Authentifizierung sicher, dass er der ist, für den er sich ausgibt. Dies könnte etwas so Einfaches wie ein Benutzername und Passwort oder eine komplexere Multi-Faktor-Authentifizierung (MFA) sein.

Wir verwenden in dieser Serie Amazon Cognito User Pools für unsere Authentifizierung.

Autorisierung (AuthZ)

Sobald die Identität eines Benutzers authentifiziert ist, kommt die Autorisierung ins Spiel. Dieser Prozess beantwortet die Frage: Was darfst du tun? Die Autorisierung bestimmt, auf welche Ressourcen, Daten und Aktionen ein Benutzer basierend auf seinen Rollen und Berechtigungen zugreifen darf.

In einer Multi-Tenant-Umgebung ist es wichtig, die Autorisierung richtig zu implementieren. Wir müssen sicherstellen, dass Benutzer nur auf die Daten und Funktionalitäten zugreifen können, die für ihren Tenant und seine Rolle relevant sind. Hier kommt ein robustes, skalierbares Autorisierungssystem ins Spiel.

Zentralisierter PDP und verteilte PEPs: Das Rückgrat der Autorisierung

Nachdem wir den Unterschied zwischen AuthN und AuthZ geklärt haben, konzentrieren wir uns auf den Aufbau eines Autorisierungssystems mit einem zentralen PDP und verteilten PEPs.

Zentralisierter PDP

Ein Policy Decision Point (PDP) ist der Ort, an dem alle Ihre Autorisierungsentscheidungen getroffen werden. Es ist das Gehirn des Autorisierungssystems, das jede Anfrage gegen vordefinierte Richtlinien oder Rollen auswertet und entscheidet, ob die Anfrage zugelassen oder abgelehnt werden soll. Es gibt einige klare Vorteile eines zentralen PDP:

Konsistenz: Jede Anfrage wird gegen denselben Satz von Richtlinien ausgewertet, was konsistente Entscheidungen über unsere Systeme hinweg sichert.
Verwaltung und Compliance: Da alle Richtlinien an einem Ort verwaltet werden, werden Updates und Audits vereinfacht. Das Protokollieren von Entscheidungen zu Audit-Zwecken ist für einige regulatorische Compliance wichtig.

Ein Nachteil wäre jedoch die erhöhte Latenz. Jedes Autorisierungsanfragen über einen einzigen PDP zu leiten, kann die Dinge verlangsamen, besonders in einem verteilten System.

Mit Amazon Verified Permissions (AVP) ist ein zentraler PDP erforderlich.

Verteilte PEPs

Policy Enforcement Points (PEPs) sind die Stellen, an denen die vom PDP getroffenen Autorisierungsentscheidungen durchgesetzt werden. Diese Punkte sind über unser System verteilt. Unser Lambda-basierter Authorizer ist ein Beispiel für einen PEP. Die Vorderfront unserer Mikrodienste kann ebenfalls als PEP dienen und unseren PDP aufrufen.

Wir haben einige Vorteile, wenn wir unsere PEPs verteilt betreiben.

Reduzierte Latenz: Indem wir PEPs nahe an den Stellen platzieren, an denen Entscheidungen durchgesetzt werden müssen, können wir die Latenz reduzieren, mit einer Caching-Strategie kann sie noch weiter reduziert werden.
Skalierbarkeit: Wenn Ihr System wächst, stellen verteilte PEPs sicher, dass kein einzelner Punkt zum Engpass wird, insbesondere mit aktiviertem Caching.

Architekturüberblick

In unserer neuen Autorisierungsarchitektur werden wir einen neuen Autorisierungsdienst einführen, der unser zentraler PDP sein wird. Die Logik für die Autorisierung wird von unserem vorherigen Lambda-Authorizer zum PDP verschoben, stattdessen wird unser Lambda-Authorizer nun den PDP aufrufen, der die Zugriffsentscheidung zurückgibt.

Bild zeigt Überblick über die Tenant-Erstellung

Autorisierungsdienst (PDP) erstellen

Das Erste, was wir tun müssen, ist, unseren Autorisierungsdienst (PDP) zu erstellen, wie gezeigt, wird dies ein API Gateway und eine Lambda-Funktion sein. Ein interessanter Ansatz, den wir verwenden könnten, ist, das API Gateway zu entfernen und nur Lambda-Funktions-URLs zu verwenden. Für eine einfachere Erweiterung später habe ich mich jedoch entschieden, ein API Gateway zu verwenden.


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
  

Unsere API wird AWS IAM für Machine-2-Machine-Autorisierung verwenden, genau wie unsere Admin-API, die wir im vorherigen Teil erstellt haben.

Als Nächstes verschieben wir unsere AuthZ-Logik von unserem Lambda-Authorizer zu unserer neuen PDP-Implementierung. Wir werden sowohl die Autorisierung für Benutzer- als auch für Tenant-Zugriffe behandeln und zwei neue Ressourcen tenant und tenantUser einführen.


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

		# Ressource prüfen und eine AuthZ-Entscheidung treffen
        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)}")

    # Standardantwort generieren, die den Zugriff verweigert
    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

Als Nächstes müssen wir natürlich den Lambda-Authorizer aktualisieren, um unseren PDP für die Autorisierung aufzurufen.


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)}")

Zusammenfassung

Mit diesen Änderungen haben wir unsere Autorisierung zu einem zentralen PDP mit einem verteilten PEP-Setup verlegt. Dies schafft eine gute Grundlage für uns, um unsere AuthZ weiterzuentwickeln. In späteren Beiträgen werden wir RBAC (Role Based Access Control) und ABAC (Attribute Based Access Control) einführen und auch zu Amazon Verified Permissions wechseln.

Holen Sie sich den Code

Das vollständige Setup mit allem Code ist auf der Serverless Handbook verfügbar.

Abschließende Worte

In diesem Beitrag haben wir uns die Architektur eines zentralen Autorisierungssystems mit einem PDP und verteilten PEPs angesehen. Wir haben auch die Unterschiede zwischen Authentifizierung (AuthN) und Autorisierung (AuthZ) hervorgehoben.

In den kommenden Beiträgen werden wir uns mit der Geräte-Onboarding und Daten befassen, bleiben Sie gespannt!

Schauen Sie sich mein Serverless Handbook an, um den Code für die in dieser Beitragsreihe erstellte Lösung zu erhalten.

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!

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