Bygg en serverlös ansluten grill som SaaS - Del 3 - Hyresgäster

Questo file e stato tradotto automaticamente dall'IA, potrebbero verificarsi errori
Den tiden har kommit för del 3 i serien om att skapa en Serverlös Ansluten Grill som SaaS. I denna tredje post kommer vi att titta på hyresgästskapande, autentisering och auktorisering. Vi kommer att skapa en ny hyresgästtjänst och koppla ihop den med användartjänsten från del två.
Om du inte redan har tittat på den, här är del ett och del två
Hyresgäster i en SaaS
I en Software as a Service (SaaS)-lösning är en hyresgäst den organisation eller individ som abonnerar på och använder tjänsten.
En av de mest avgörande delarna i en multi-hyresgäst SaaS-lösning är hyresgästdataisolering, vilket säkerställer att varje hyresgästs data förblir separat och säker från andra inom samma miljö. Detta är särskilt viktigt för att upprätthålla dataintegritet, uppfylla regler och skydda mot dataintrång.
Tillvägagångssätt för hyresgästdataisolering i AWS
För att isolera hyresgästdata finns det flera olika tillvägagångssätt vi kan använda.
Dedikerad databas / databas per hyresgäst
Varje hyresgäst har en separat databasinstans eller databas, som S3. Detta ger den starkaste isoleringen eftersom varje hyresgästs data är helt åtskilda på databas- / databasnivå. I detta tillvägagångssätt kan vi också använda separata KMS-nycklar för varje hyresgäst, vilket säkerställer att data är säkrad även på krypteringsnivå.
Några av utmaningarna med detta tillvägagångssätt skulle vara högre kostnader på grund av flera databasinstanser. Skalning kan bli komplex när antalet hyresgäster växer.
Tabell per hyresgäst
Varje hyresgäst har en separat tabell i databasen. För en databas som S3 kan varje hyresgäst ha ett unikt prefix som data lagras under. För att använda separata KMS-nycklar måste data krypteras på klientnivå och inte på lagringsnivå, vilket kan ge ännu mer isolering.
Detta tillvägagångssätt ger en bra kompromiss mellan kostnad och isolering. och lättare att genomdriva dataisolering på tabellnivå.
Några av utmaningarna med detta tillvägagångssätt är att det fortfarande kräver noggrann hantering av tabellnamn och schemautveckling. Prestanda kan påverkas.
Radnivåsäkerhet (RLS)
Data för alla hyresgäster lagras i en enda tabell, men med åtkomstkontroll på radnivå för att säkerställa att varje hyresgäst endast kan åtkomst sin egen data. Denna åtkomstkontroll kan vara på lagringsnivå med inbyggd RLS eller på klientnivå med auktoriseringskontroller med tjänster som Amazon Verified Permissions. För en databas som S3 kan varje hyresgäst fortfarande ha ett unikt prefix som data lagras under.
Några av utmaningarna med detta tillvägagångssätt är att det är mer komplext att genomdriva korrekt; eventuella buggar i logik kan exponera data. Prestanda kan påverkas av komplexa frågefiltreringar.
Vilket tillvägagångssätt bör jag använda?
Det tillvägagångssätt du använder beror på ditt användningsfallet, den nivå av efterlevnad du behöver och kundkraven.
I denna serie om grilldata kommer jag att ta detta tillvägagångssätt och använda radnivåsäkerhet.
Arkitekturöversikt
I denna arkitektur fortsätter vi att bygga på den händeldrivna uppställningen som introducerades i del 2. När en användare registrerar sig, skapas en händelse som skickas från användartjänsten till en EventBridge-händelsebuss, detta kommer nu att anropa hyresgästtjänsten. Hyresgästtjänsten har två primära DynamoDB-tabeller, en som lagrar hyresgästinformationen, ID, namn, etc och en som kartlägger användarnas åtkomst till en hyresgäst. Tjänsten börjar med att skapa ett hyresgäst-ID, lagrar det och sedan kartlägger användaren till denna nya hyresgäst.

Vår hyresgästtjänst kan anropas via två separata API:er. En för vår webbapplikation som kommer att hämta och uppdatera hyresgästinformationen, och en admin-API som inte bara kommer att användas av SaaS-administratörer utan också för kommunikation mellan tjänster. Vi kommer att introducera den första rundan av API-auktorisering i denna post, IAM för maskin-till-maskin och Oauth och JWT-tokenvalidering för webbapplikations-API:et. Vi kommer att gå djupare in på både API:erna och auktorisering senare i denna post.

Hyresgästskapande
Först skapar vi de resurser som behövs för hyresgästskapande. Här skapar vi två nya DynamoDB-tabeller. I en tabell lagrar vi hyresgästerna och information om dem som hyresgästnamn etc. I den andra tabellen kommer vi att lagra en mappning mellan hyresgäster och användare för åtkomst. Vi måste också kunna fråga vilka hyresgäster en användare har åtkomst till, för detta ställer vi också in ett index för tabellen.
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_REQUESTNär en användare skapas måste vi skapa den hyresgäst som denna användare äger. Användartjänsten kommer att skicka en händelse på applikationens händelsebuss när en användare skapas, så här fortsätter vi att bygga på ett händeldrivet saga-mönster och skapa hyresgäst.
Genom att använda StepFunctions blir det enkelt att skriva till DynamoDB och skicka en händelse för att nästa del ska ta över.

Efter att hyresgästern skapats och lagrats måste vi mappa den första admin-användaren till den, i princip användaren som äger hyresgästen. Denna StepFunction anropas av händelsen att hyresgästen skapades.

Sistaste steget i denna StepFunction publicerar också till EventBridge för framtida användning.
Låt oss lägga till dessa i StepFunctions till vår mall.
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: trueAPI:er
Hyresgästtjänsten har två separata API:er. Först av allt en API som används av webbapplikationen för att ladda, visa och uppdatera information om hyresgästen. Denna API kommer att vara central i kommande delar när vi utökar hur användare får åtkomst till hyresgäster. Den andra API:en är en hanteringsadmin-API som kan användas för speciella tjänste- och systemintegrationer.
För applikations-API:et kommer vi att använda de token som utfärdas av Cognito för att auktorisera användarna, och för hanterings-API:et kommer vi att använda maskin-till-maskin-auktorisering med AWS Iam, åtminstone för nu.
Båda API:erna kommer att backas av Lambda-funktioner för implementering av affärslogiken.
För att börja skapar vi applikations-API:et. Användare kommer att anropa API:et med den token de fick från Cognito. API Gateway har tre Lambda-integrationer som kommer att hämta rätt data, och vi har en Lambda Authorizer för att validera token.

Låt oss börja med att lägga till API-definitionen i mallen, vi kommer att använda AWS::Serverless::Api-resurser och dessa MÅSTE definieras i samma mall som Lambda-funktionerna för att enkelt skapa integrationen.
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
Nästa skapar vi Lambda-funktionerna som kommer att backa vår applikations-API.
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: LambdaUserRequestAuthorizerNästa kan vi gå över till hanterings-API:et, som kommer att använda AWS Iam för att auktorisera anropen och har en Lambda-baserad integration för att hämta alla hyresgäster för en användare.

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 överallt
Låt mig börja med att citera den enda och enda Eric Johnson. Om CORS hade ett ansikte skulle jag slå till i näsan det sammanfattar i princip mina känslor om CORS.
Först och främst måste vi sätta CORS på vår API, genom att ange AllowMethods, AllowHeaders, AllowOrigin på vår API-resurs, detta kommer att lägga till OPTIONS vilket tillåter preflight-hämtning från webbläsaren.
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: "'*'"Om du sätter en authorizer på API-resursen och du inte explicit sätt AddDefaultAuthorizerToCorsPreflight: false kommer auktorisering att läggas till OPTIONS vilket i de flesta fall kommer att göra att preflight misslyckas och genererar ett cors-fel i webbläsaren.
Men, eftersom vi använder en proxy Lambda-integration är det inte tillräckligt att sätta cors på API:et. Lambda-funktionen är ansvarig för att sätta och returnera cors-huvudena, så vi måste lägga till dem i returen i våra funktioner också.
return {
"statusCode": 200,
"body": json.dumps(response["Items"]),
"headers": {
"Access-Control-Allow-Origin": "*", # Tillåt alla ursprung
"Access-Control-Allow-Headers": "Content-Type,Authorization",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS,PUT,DELETE", # Tillåtna metoder
},
}Användarauktorisering
I denna del börjar vi långsamt introducera API:er och för det behöver vi naturligtvis någon form av användarauktorisering. Vi kommer att använda Lambda Authorizer på den offentliga webbapplikations-API:et. I den första versionen och ganska primitiv funktionalitet kommer vi att lita på att User Pool berikar JWT-token med ett hyresgäst-ID.
Cognito Pre Token Generation
För att lägga till anpassade anspråk och berika JWT-token kommer vi att ansluta oss till User Pool-autentiseringsflödet och använda Pre Token Generation-hook. Just nu använder jag version 1 av denna hook som kommer att lägga till anspråken till ID-token. Det var inte så länge sedan som möjligheten att anpassa åtkomsttoken läggs till. I senare delar kommer vi att utöka detta och gå över till version 2 av händelsen som kommer att anpassa både ID och åtkomsttoken.
Under processen kommer vi att anropa hyresgäst-API:et och hämta hyresgäst-ID för användaren och lägga till detta i token.

När du använder version 1-händelsen är det fortfarande endast möjligt att lägga till anpassade strängvärden, det är inte möjligt att lägga till arrayer, vilket jag känner är en liten nackdel.
För maskin-till-maskin-auktorisering kommer vi att lita på AWS IAM, vilket innebär att vi måste signera våra förfrågningar med sigv4.
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 eventAnpassade Lambda Authorizers
För att auktorisera anropen och säkerställa att användaren som gör anropet för att hämta hyresgästdata faktiskt har åtkomst till den hyresgästen, använder vi en anpassad Lambda Authorizer på vår API. Vi kommer att lita på att User Pool lägger till hyresgäst-ID i JWT-token under autentiseringsprocessen. Vi kommer därför att dekoda JWT-token och validera signaturen av token, och läsa ut hyresgäst-ID från token. Hyresgäst-ID:et i token kommer sedan att kontrolleras mot den hyresgäst som användaren försöker åtkomst. Det är lite primitivt men det fungerar för vårt nuvarande användningsfallet.
I senare delar av denna serie kommer vi att skapa en central auktoriseringstjänst som kommer att validera och auktorisera anrop i alla delar av systemet.
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
Vi utökar dashboarden med en ytterligare sida som visar hyresgästinformationen, och som har möjligheten att ange ett nytt namn på hyresgästen.

Få koden
Den fullständiga uppställningen med all kod är tillgänglig på Serverless Handbook
Avslutande ord
Detta var en post där jag går igenom del tre i serien om ansluten grill som Saas och diskuterar hyresgäster och hyresgästskapande. Om du tycker om quiz-delen gå och kolla in kvist.ai
Kolla in Min serverless-handbok för några av koncepten som nämns i denna post.
Glöm inte att följa mig på LinkedIn och X för mer innehåll, och läs resten av mina bloggar
Som Werner säger! Nu gå och bygg!