serverless, aws, saas, IoT

Aufbau eines Serverless Connected BBQ als SaaS - Teil 3 - Mandanten

2024-08-23
This post cover image
aws cloud serverless bbq iot saas

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

Die Zeit ist gekommen für Teil 3 der Serie zum Erstellen eines Serverless Connected BBQ als SaaS. In diesem dritten Beitrag werden wir uns mit der Mandantenerstellung, Authentifizierung und Autorisierung befassen. Wir werden einen neuen Mandantenservice erstellen und ihn mit dem Benutzerservice aus Teil zwei verbinden.

Falls Sie es noch nicht überprüft haben, hier sind Teil eins und Teil zwei

Mandanten in einer SaaS

In einer Software as a Service (SaaS)-Lösung ist ein Mandant die Organisation oder Einzelperson, die den Dienst abonniert und nutzt.

Einer der entscheidendsten Teile einer Multi-Mandanten-SaaS-Lösung ist die Isolation von Mandantendaten, um sicherzustellen, dass die Daten jedes Mandanten getrennt und sicher von anderen innerhalb derselben Umgebung bleiben. Dies ist besonders wichtig, um den Datenschutz zu wahren, Vorschriften einzuhalten und vor Datenpannen zu schützen.

Ansätze für die Isolation von Mandantendaten in AWS

Um Mandantendaten zu isolieren, gibt es mehrere verschiedene Ansätze, die wir verwenden können.

Dedizierte Datenbank / Datenspeicher pro Mandant

Jeder Mandant hat eine separate Datenbankinstanz oder einen Datenspeicher, wie S3. Dies bietet die stärkste Isolierung, da die Daten jedes Mandanten auf der Datenbank- / Datenspeicherebene vollständig getrennt sind. Bei diesem Ansatz können wir auch separate KMS-Schlüssel für jeden Mandanten verwenden, um sicherzustellen, dass die Daten selbst auf der Verschlüsselungsebene gesichert sind.

Einige der Herausforderungen bei diesem Ansatz wären die höheren Kosten aufgrund mehrerer Datenbankinstanzen. Die Skalierung kann komplex werden, wenn die Anzahl der Mandanten wächst.

Tabelle pro Mandant

Jeder Mandant hat eine separate Tabelle in der Datenbank. Bei einem Datenspeicher wie S3 kann jeder Mandant ein einziges Präfix haben, unter dem die Daten gespeichert werden. Um separate KMS-Schlüssel zu verwenden, müssen die Daten auf der Client-Seite und nicht auf der Speicherebene verschlüsselt werden, was noch mehr Isolierung bieten kann.

Dieser Ansatz bietet einen guten Kompromiss zwischen Kosten und Isolierung und ist einfacher, die Datenisolierung auf Tabellenniveau durchzusetzen.

Einige der Herausforderungen bei diesem Ansatz sind, dass er immer noch die sorgfältige Verwaltung von Tabellennamen und Schemaevolution erfordert. Die Leistung könnte beeinträchtigt werden.

Zeilenebene-Sicherheit (RLS)

Die Daten aller Mandanten werden in einer einzigen Tabelle gespeichert, aber mit zugriffssteuerungen auf Zeilenebene, um sicherzustellen, dass jeder Mandant nur auf seine eigenen Daten zugreifen kann. Diese Zugriffssteuerung kann auf der Ebene des Datenspeichers mit integrierter RLS oder auf der Client-Ebene mit Autorisierungsprüfungen mit Diensten wie Amazon Verified Permissions erfolgen. Bei einem Datenspeicher wie S3 kann jeder Mandant weiterhin ein einziges Präfix haben, unter dem die Daten gespeichert werden.

Einige der Herausforderungen bei diesem Ansatz sind, dass es komplexer ist, die Einhaltung korrekt durchzusetzen; Fehler in der Logik könnten Daten freilegen. Die Leistung kann durch komplexe Abfragesfilterung beeinträchtigt werden.

Welchen Ansatz sollte ich verwenden?

Der Ansatz, den Sie verwenden, hängt von Ihrem Anwendungsfall, dem erforderlichen Konformitätsniveau und den Kundenanforderungen ab.

In dieser Serie über BBQ-Daten werde ich den Ansatz der Zeilenebene-Sicherheit verwenden.

Architekturüberblick

In dieser Architektur bauen wir weiter auf dem ereignisgesteuerten Setup auf, das in Teil 2 eingeführt wurde. Wenn sich ein Benutzer anmeldet und erstellt wird, wird ein Ereignis vom Benutzerservice an einen EventBridge-Ereignisbus gesendet, der nun den Mandantenservice aufruft. Der Mandantenservice verfügt über zwei primäre DynamoDB-Tabellen, eine, die die Mandantendaten, ID, Name usw. speichert, und eine, die die Zugriffe der Benutzer auf einen Mandanten zuordnet. Der Service beginnt mit der Erstellung einer Mandanten-ID, speichert sie und ordnet den Benutzer dann diesem neuen Mandanten zu.

Bild zeigt Überblick zur Mandantenerstellung

Unser Mandantenservice kann über zwei separate APIs aufgerufen werden. Eine für unsere Webanwendung, die die Mandantendaten abruft und aktualisiert, und eine Admin-API, die nicht nur von SaaS-Admins, sondern auch für die Kommunikation zwischen Diensten verwendet wird. In diesem Beitrag werden wir die erste Runde der API-Autorisierung einführen, IAM für Machine-2-Machine und Oauth sowie JWT-Tokenvalidierung für die Webanwendungs-API. Wir werden später in diesem Beitrag beide APIs und die Autorisierung genauer untersuchen.

Bild zeigt Überblick zum Mandantenservice

Mandantenerstellung

Zunächst erstellen wir die benötigten Ressourcen für die Mandantenerstellung. Hier erstellen wir zwei neue DynamoDB-Tabellen. In einer Tabelle speichern wir die Mandanten und Informationen dazu wie Mandantennamen usw. In der zweiten Tabelle speichern wir eine Zuordnung zwischen Mandanten und Benutzern für den Zugriff. Wir müssen auch abfragen können, auf welche Mandanten ein Benutzer Zugriff hat, dafür richten wir auch einen Index für die Tabelle ein.

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
  CommonStackName:
    Type: String
    Description: The name of the common stack that contains the EventBridge Bus and more

  TenantTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub ${ApplicationName}-tenants
      AttributeDefinitions:
        - AttributeName: tenantid
          AttributeType: S
      KeySchema:
        - AttributeName: tenantid
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  TenantUserTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub ${ApplicationName}-tenant-users
      AttributeDefinitions:
        - AttributeName: tenantid
          AttributeType: S
        - AttributeName: userid
          AttributeType: S
      KeySchema:
        - AttributeName: tenantid
          KeyType: HASH
        - AttributeName: userid
          KeyType: RANGE
      GlobalSecondaryIndexes: 
        - IndexName: user-index
          KeySchema:
            - AttributeName: userid
              KeyType: HASH
            - AttributeName: tenantid
              KeyType: RANGE
          Projection:
            ProjectionType: ALL
      BillingMode: PAY_PER_REQUEST

Wenn ein Benutzer erstellt wird, müssen wir den Mandanten erstellen, dem dieser Benutzer gehört. Der Benutzerservice sendet ein Ereignis an den Anwendungsereignisbus, wenn ein Benutzer erstellt wird, also bauen wir hier weiter auf einem ereignisgesteuerten Saga-Muster auf und erstellen den Mandanten.

Die Verwendung von StepFunctions macht es einfach, in DynamoDB zu schreiben und ein Ereignis für den nächsten Teil zu veröffentlichen.

Bild zeigt Step Functions zur Mandantenerstellung

Nachdem der Mandant erstellt und gespeichert wurde, müssen wir den ersten Admin-Benutzer dazu zuordnen, im Wesentlichen den Benutzer, der den Mandanten besitzt. Diese StepFunction wird durch das Ereignis ausgelöst, dass der Mandant erstellt wurde.

Bild zeigt Step Functions zur Zuordnung von Mandantenbenutzern

Der letzte Schritt in dieser StepFunction veröffentlicht ebenfalls auf EventBridge für die zukünftige Verwendung.

Lassen Sie uns diese zu StepFunctions in unsere Vorlage hinzufügen.


  TenantCreateExpress:
    Type: AWS::Serverless::StateMachine
    Properties:
      DefinitionUri: create-tenant-statemachine/statemachine.asl.yaml
      Tracing:
        Enabled: true
      Logging:
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt TenantCreateStateMachineLogGroup.Arn
        IncludeExecutionData: true
        Level: ALL
      DefinitionSubstitutions:
        EventBridgeBusName:
          Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
        TenantTable: !Ref TenantTable
        ApplicationName: !Ref ApplicationName
      Policies:
        - Statement:
            - Effect: Allow
              Action:
                - logs:*
              Resource: "*"
        - EventBridgePutEventsPolicy:
            EventBusName:
              Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
        - DynamoDBCrudPolicy:
            TableName: !Ref TenantTable
      Events:
        CreateTenantEvent:
          Type: EventBridgeRule
          Properties:
            EventBusName:
              Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
            Pattern:
              source:
                - !Sub ${ApplicationName}.user
              detail-type:
                - created
      Type: EXPRESS


  TenantAddFirstAdminExpress:
    Type: AWS::Serverless::StateMachine
    Properties:
      DefinitionUri: add-tenant-first-admin-statemachine/statemachine.asl.yaml
      Tracing:
        Enabled: true
      Logging:
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt TenantAddFirstAdminStateMachineLogGroup.Arn
        IncludeExecutionData: true
        Level: ALL
      DefinitionSubstitutions:
        EventBridgeBusName:
          Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
        TenantUserTable: !Ref TenantUserTable
        ApplicationName: !Ref ApplicationName
      Policies:
        - Statement:
            - Effect: Allow
              Action:
                - logs:*
              Resource: "*"
        - EventBridgePutEventsPolicy:
            EventBusName:
              Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
        - DynamoDBCrudPolicy:
            TableName: !Ref TenantUserTable
      Events:
        CreateTenantEvent:
          Type: EventBridgeRule
          Properties:
            EventBusName:
              Fn::ImportValue: !Sub ${CommonStackName}:eventbridge-bus-name
            Pattern:
              source:
                - !Sub ${ApplicationName}.tenant
              detail-type:
                - created
      Type: EXPRESS
Comment: Tenant service - Create Tenant On User Created
StartAt: Debug
States:
  Debug:
    Type: Pass
    Next: Genetate Tenant ID
  Genetate Tenant ID:
    Type: Pass
    Parameters:
      tenantid.$: States.UUID()
    ResultPath: $.TenantID
    Next: Create Tenant
  Create Tenant:
    Type: Task
    Resource: arn:aws:states:::dynamodb:putItem
    Parameters:
      TableName: ${TenantTable}
      Item:
        tenantid:
          S.$: $.TenantID.tenantid
    ResultPath: null
    Next: Prepare Event
  Prepare Event:
    Type: Pass
    Parameters:
      tenantId.$: $.TenantID.tenantid
      email.$: $.detail.email
      userName.$: $.detail.userName
      name.$: $.detail.name
    ResultPath: $.TenantData
    Next: Post Event
  Post Event:
    Type: Task
    Resource: arn:aws:states:::events:putEvents
    Parameters:
      Entries:
        - Source: ${ApplicationName}.tenant
          DetailType: created
          Detail.$: $.TenantData
          EventBusName: ${EventBridgeBusName}
    End: true
Comment: Tenant Service - Create Tenant Admin User Mapping
StartAt: Debug
States:
  Debug:
    Type: Pass
    Next: Create Tenant User Mapping
  Create Tenant User Mapping:
    Type: Task
    Resource: arn:aws:states:::dynamodb:putItem
    Parameters:
      TableName: ${TenantUserTable}
      Item:
        tenantid:
          S.$: $.detail.tenantId
        userid:
          S.$: $.detail.userName
        name:
          S.$: $.detail.name
        email:
          S.$: $.detail.email
        role: admin
    ResultPath: null
    Next: Prepare Event
  Prepare Event:
    Type: Pass
    Parameters:
      tenantId.$: $.detail.tenantId
      email.$: $.detail.email
      userName.$: $.detail.userName
      name.$: $.detail.name
      role: admin
    ResultPath: $.TenantUser
    Next: Post Event
  Post Event:
    Type: Task
    Resource: arn:aws:states:::events:putEvents
    Parameters:
      Entries:
        - Source: ${ApplicationName}.tenant
          DetailType: adminUserAdded
          Detail.$: $.TenantUser
          EventBusName: ${EventBridgeBusName}
    End: true

APIs

Der Mandantenservice verfügt über zwei separate APIs. Zunächst einmal eine API, die von der Webanwendung verwendet wird, um Informationen über den Mandanten zu laden, anzuzeigen und zu aktualisieren. Diese API wird in kommenden Teilen zentral sein, wenn wir erweitern, wie Benutzer Zugang zu Mandanten erhalten. Die zweite API ist eine Management-Admin-API, die für spezielle Dienst- und Systemintegration verwendet werden kann.

Für die Anwendungs-API werden wir die von Cognito ausgestellten Token verwenden, um die Benutzer zu autorisieren, und für die Management-API werden wir Machine-2-Machine-Autorisierung mit AWS Iam verwenden, zumindest für jetzt.

Beide APIs werden durch Lambda-Funktionen unterstützt, um die Geschäftslogik zu implementieren.

Um zu beginnen, erstellen wir die Anwendungs-API. Benutzer werden die API mit dem Token aufrufen, den sie von Cognito erhalten haben. API Gateway hat drei Lambda-Integrationen, die die korrekten Daten abrufen werden, und wir haben einen Lambda-Autorisierungsdienst, um den Token zu validieren.

Bild zeigt die Mandantenanwendungs-API

Lassen Sie uns beginnen, indem wir die API-Definition zur Vorlage hinzufügen, wir werden AWS::Serverless::Api-Ressourcen verwenden, und diese MÜSSEN in derselben Vorlage wie die Lambda-Funktionen definiert werden, damit wir die Integration einfach erstellen können.


  TenantAPi:
    Type: AWS::Serverless::Api
    Properties:
      Name: !Sub ${ApplicationName}-tenant-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:
        AddDefaultAuthorizerToCorsPreflight: false
        Authorizers:
          LambdaRequestAuthorizer:
            FunctionArn: !GetAtt LambdaApiAuthorizer.Arn
            FunctionPayloadType: REQUEST
            Identity: 
              Headers:
                - Authorization
          LambdaUserRequestAuthorizer:
            FunctionArn: !GetAtt LambdaApiUserAuthorizer.Arn
            FunctionPayloadType: REQUEST
            Identity: 
              Headers:
                - Authorization
        DefaultAuthorizer: LambdaRequestAuthorizer

Als nächstes erstellen wir die Lambda-Funktionen, die unsere Anwendungs-API unterstützen werden.

  LambdaTenantInfoGet:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/Api/TenantInfoGet
      Handler: get.handler
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref TenantTable
      Environment:
        Variables:
          DYNAMODB_TABLE_NAME: !Ref TenantTable
      Events:
        GetTenantInfoApi:
          Type: Api
          Properties:
            Path: /tenant/{tenantId}
            Method: get
            RestApiId: !Ref TenantAPi

  LambdaTenantInfoPut:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/Api/TenantInfoPut
      Handler: put.handler
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TenantTable
      Environment:
        Variables:
          DYNAMODB_TABLE_NAME: !Ref TenantTable
      Events:
        PutTenantInfoApi:
          Type: Api
          Properties:
            Path: /tenant/{tenantId}
            Method: put
            RestApiId: !Ref TenantAPi
  
  LambdaGetTenants:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/Api/TenantsList
      Handler: get.handler
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref TenantUserTable
      Environment:
        Variables:
          DYNAMODB_TABLE_NAME: !Ref TenantUserTable
          DYNAMODB_INDEX_NAME: user-index
      Events:
        GetTenantsForUserApi:
          Type: Api
          Properties:
            Path: /tenants/{userId}
            Method: get
            RestApiId: !Ref TenantAPi
            Auth:
              Authorizer: LambdaUserRequestAuthorizer

Als nächstes können wir zur Management-API wechseln, die AWS Iam zur Autorisierung der Aufrufe verwenden wird und eine Lambda-basierte Integration hat, um alle Mandanten für einen Benutzer abzurufen.

Bild zeigt die Mandantenmanagement-API

  AdminTenantAPi:
    Type: AWS::Serverless::Api
    Properties:
      Name: !Sub ${ApplicationName}-tenant-admin-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

  LambdaGetTenantsForUser:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/Internal/GetTenantForUser
      Handler: handler.handler
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref TenantUserTable
      Environment:
        Variables:
          DYNAMODB_TABLE_NAME: !Ref TenantUserTable
          DYNAMODB_INDEX_NAME: user-index
      Events:
        GetTenantsForUserApi:
          Type: Api
          Properties:
            Path: /tenants/{userId}
            Method: get
            RestApiId: !Ref AdminTenantAPi
            Auth:
              AuthorizationType: AWS_IAM

Cors cors cors überall

Lassen Sie mich zunächst den einzigartigen Eric Johnson zitieren. Wenn CORS ein Gesicht hätte, würde ich es in die Nase schlagen das fasst im Grunde meine Gefühle zu CORS zusammen.

Zunächst einmal müssen wir CORS auf unserer API einrichten, indem wir AllowMethods, AllowHeaders, AllowOrigin auf unserer API-Ressource angeben, dies wird OPTIONS hinzufügen, um Preflight-Fetching vom Browser zu ermöglichen.

TenantAPi:
    Type: AWS::Serverless::Api
    Properties:
    .....
      Cors:
        AllowMethods: "'GET,PUT,POST,DELETE,OPTIONS'"
        AllowHeaders: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"
        AllowOrigin: "'*'"

Wenn Sie einen Autorisierungsdienst auf der API-Ressource einrichten und Sie nicht explizit AddDefaultAuthorizerToCorsPreflight: false festlegen, wird die Autorisierung zu OPTIONS hinzugefügt, was in den meisten Fällen den Preflight-Fetching fehlschlagen lässt und einen CORS-Fehler im Browser erzeugt.

Aber da wir eine Proxy-Lambda-Integration verwenden, reicht es nicht aus, CORS auf der API einzustellen. Die Lambda-Funktion ist dafür verantwortlich, die CORS-Header einzustellen und zurückzugeben, daher müssen wir sie auch in der Rückgabe unserer Funktionen hinzufügen.

return {
    "statusCode": 200,
    "body": json.dumps(response["Items"]),
    "headers": {
        "Access-Control-Allow-Origin": "*",  # Alle Ursprünge zulassen
        "Access-Control-Allow-Headers": "Content-Type,Authorization",
        "Access-Control-Allow-Methods": "GET,POST,OPTIONS,PUT,DELETE",  # Erlaubte Methoden
    },
}

Benutzerautorisierung

In diesem Teil beginnen wir langsam, APIs einzuführen, und dafür benötigen wir natürlich eine Form der Benutzerautorisierung. Wir werden Lambda-Autorisierungsdienste auf der öffentlichen Webanwendungs-API verwenden. In der ersten Version und ziemlich primitiver Funktionalität werden wir uns darauf verlassen, dass der User Pool den JWT-Token mit einer Mandanten-ID anreichert.

Cognito Pre Token Generation

Um benutzerdefinierte Claims hinzuzufügen und den JWT-Token anzureichern, greifen wir in den User-Pool-Authentifizierungsfluss ein und verwenden den Pre Token Generation-Hook. Im Moment verwende ich die Version 1 dieses Hooks, die die Claims zum ID-Token hinzufügt. Es ist noch nicht lange her, dass die Möglichkeit, das Zugriffstoken anzupassen) hinzugefügt wurde. In späteren Teilen werden wir darauf aufbauen und zur Version 2 des Ereignisses wechseln, die sowohl das ID- als auch das Zugriffstoken anpasst.

Während des Prozesses werden wir die Mandanten-API aufrufen und die Mandanten-ID für den Benutzer abrufen und diese zum Token hinzufügen.

Bild zeigt Pre Token Generation

Wenn wir die Version 1 des Ereignisses verwenden, ist es immer noch nur möglich, benutzerdefinierte Zeichenfolgenwerte hinzuzufügen, es ist nicht möglich, Arrays hinzuzufügen, was ich als einen gewissen Nachteil empfinde.

Für die Machine-2-Machine-Autorisierung werden wir uns auf AWS IAM verlassen, was bedeutet, dass wir unsere Anfragen mit sigv4 unterschreiben müssen.

import boto3
import os
import json
import requests
from requests_aws4auth import AWS4Auth

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

def handler(event, context):
    user_id = event["request"]["userAttributes"]["sub"]
    try:
        # Get AWS credentials
        session = boto3.Session()
        credentials = session.get_credentials().get_frozen_credentials()

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

        response = requests.get(api_endpoint + "/tenants/" + user_id, auth=auth)

        if response.status_code == 200:
            tenants = response.json()["tenants"]

            event["response"]["claimsOverrideDetails"] = {
                "claimsToAddOrOverride": {"tenant": tenants[0]}
            }
            return event
        else:
            print(f"Error fetching tenant: {response.status_code}")

    except requests.RequestException as e:
        # Handle any exceptions that occur during the API call
        print(f"Error making API request: {str(e)}")

    return event

Benutzerdefinierte Lambda-Autorisierungsdienste

Um die Aufrufe zu autorisieren und sicherzustellen, dass der Benutzer, der den Aufruf tätigt, um Mandantendaten abzurufen, tatsächlich Zugriff auf diesen Mandanten hat, verwenden wir einen benutzerdefinierten Lambda-Autorisierungsdienst auf unserer API. Wir werden uns darauf verlassen, dass der User Pool die Mandanten-ID während des Authentifizierungsprozesses zum JWT-Token hinzufügt. Wir werden daher den JWT-Token decodieren und die Signatur des Tokens validieren und die Mandanten-ID aus dem Token auslesen. Die Mandanten-ID im Token wird dann mit dem Mandanten verglichen, auf den der Benutzer zugreifen möchte. Es ist ein bisschen primitiv, aber es funktioniert für unseren aktuellen Anwendungsfall.

In späteren Teilen dieser Serie werden wir einen zentralen Autorisierungsdienst erstellen, der Aufrufe in allen Teilen des Systems validiert und autorisiert.

import os
import json
import jwt
from jwt import PyJWKClient


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

        jwks_url = os.environ["JWKS_URL"]
        jwks_client = PyJWKClient(jwks_url)
        signing_key = jwks_client.get_signing_key_from_jwt(token)

        # Decode the JWT and validate the signature
        decoded_token = jwt.decode(
            token,
            signing_key.key,
            algorithms=["RS256"],
            audience=os.environ["AUDIENCE"],
        )

        token_tenant_id = decoded_token.get("tenant")

        if token_tenant_id == path_tenant_id:
            return generate_policy(
                decoded_token["sub"], "Allow", event["methodArn"], decoded_token
            )

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

    # Generate a default policy that deny access
    return generate_policy(
        decoded_token["sub"], "Deny", event["methodArn"], decoded_token
    )

def generate_policy(principal_id, effect, resource, context):
    auth_response = {
        "principalId": principal_id,
        "policyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {"Action": "execute-api:Invoke", "Effect": effect, "Resource": resource}
            ],
        },
        "context": context,
    }
    return auth_response

Dashboard

Wir erweitern das Dashboard um eine weitere Seite, die die Mandantendaten anzeigt und die Möglichkeit bietet, einen neuen Namen für den Mandanten festzulegen.

Bild zeigt Pre Token Generation

Holen Sie sich den Code

Das komplette Setup mit allem Code ist verfügbar auf Serverless Handbook

Abschließende Worte

Dies war ein Beitrag, in dem ich Teil drei der Serie über Connected BBQ als SaaS durchgehe und Mandanten und die Mandantenerstellung diskutiere. Wenn Sie den Quiz-Teil genießen, schauen Sie vorbei bei kvist.ai

Schauen Sie sich mein Serverless Handbook für einige der in diesem Beitrag erwähnten Konzepte an.

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

Wie Werner sagt! Jetzt loslegen!

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