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

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.

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.

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_REQUESTWenn 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.

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.

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: EXPRESSComment: 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: trueComment: 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: trueAPIs
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.

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: LambdaUserRequestAuthorizerAls 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.

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_IAMCors 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.

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 eventBenutzerdefinierte 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_responseDashboard
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.

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!