PEP und PDP für sichere Autorisierung mit AVP

Diese Datei wurde automatisch von KI ubersetzt, es konnen Fehler auftreten
Im ersten Teil, PEP und PDP für sichere Autorisierung mit Cognito, habe ich das Konzept von Policy Enforcement Points (PEPs) und Policy Decision Points (PDPs) eingeführt, indem ich den Lambda Authorizer in API Gateway als PEP und einen Lambda-basierten Dienst als PDP verwendet habe. Die Autorisierungsentscheidung basierte auf den Rollen, die Benutzer innehaben, dargestellt durch Cognito-Gruppen, wobei die Berechtigungen in einer DynamoDB-Tabelle abgebildet wurden.
Wenn wir ein PEP- und PDP-Setup in einer Multi-Tenant-SaaS-Lösung betreiben, brauchen wir sicherlich etwas Leistungsstärkeres als eine DynamoDB-Tabelle mit Berechtigungen. Daher werden wir in diesem Teil die Verwendung von Amazon Verified Permissions (AVP) als Grundlage für unseren PDP betrachten. Dies wäre eine großartige Wahl, um Autorisierung in SaaS durchzuführen.
Was ist Amazon Verified Permissions?
Amazon Verified Permissions (AVP) ist ein vollständig verwalteter Dienst, der die Implementierung von feingranulierter Zugriffskontrolle in unseren Anwendungen vereinfacht. AVP fungiert als zentraler Policy Decision Point (PDP), an dem Richtlinien ausgewertet und Autorisierungsentscheidungen auf Basis der Attribute der Anfrage, Benutzerrollen und ähnlichem getroffen werden. AVP unterstützt nicht nur rollenbasierte Zugriffskontrolle (RBAC), sondern auch attributbasierte Zugriffskontrolle (ABAC) und andere dynamische Richtlinien, was flexiblere und granularere Autorisierungsentscheidungen ermöglicht.
Mit AVP muss unsere Lambda-Funktion (die als PDP fungiert) Richtlinien nicht mehr manuell parsen und auswerten, und wir müssen Berechtigungszuordnungen nicht in einer DynamoDB-Tabelle speichern. Stattdessen definieren und verwalten wir unsere Richtlinien direkt in AVP, das dann die Autorisierungsentscheidung für uns trifft.
Alternative Open-Source-Lösungen zu AVP
Während AVP eine ausgezeichnete Wahl für Anwendungen ist, insbesondere wenn sie in AWS ausgeführt werden, ist es nicht die einzige Lösung für die Implementierung von feingranulierter Autorisierung. Eine Alternative ist Oso, ein Open-Source-Autorisierungs-Framework, das entwickelt wurde, um flexible, richtlinienbasierte Zugriffskontrolle bereitzustellen. Oso ermöglicht es uns, Autorisierungslogik in einer deklarativen Sprache zu definieren und sie in unseren Anwendungen in verschiedenen Umgebungen zu integrieren. Die Wahl zwischen AVP und Oso hängt von unserem spezifischen Anwendungsfall, unserer Infrastruktur usw. ab.
Caching von Autorisierungsentscheidungen
Nachdem wir AVP als zentralen Teil unseres PDP betrachtet haben, sollten wir über Caching nachdenken. Es gibt Vor- und Nachteile beim Caching, zum Beispiel cachet AWS IAM keine Entscheidungen, was sicherstellt, dass Updates von Richtlinien sofort wirksam werden. Mit einem Cache können wir die Anzahl der Aufrufe an AVP reduzieren und damit die Kosten senken. Wir können Entscheidungen entweder im Client oder in unseren Backend-Systemen cachen. In dieser Lösung werde ich nicht tiefer auf dieses Thema eingehen, als dass wir einen externen Cache in unserem Backend-System verwenden werden. Der Cache wird in DynamoDB platziert. Der Grund, den Cache in einer externen Quelle wie DynamoDB statt im Lambda-Funktionsspeicher zu platzieren, ist, dass alle Aufrufe der Lambda-Funktion vom Cache profitieren können. Mit einem solchen Cache benötigen wir eine Möglichkeit, den Cache zu leeren, wenn ein Berechtigungssatz für einen Benutzer geändert wird. In diesem Beitrag werde ich kein Cache-Invalidierungsmechanismus implementieren.
AVP und die Cedar-Policy-Sprache verstehen
Amazon Verified Permissions (AVP) verwendet Cedar, eine speziell entwickelte Policy-as-Code-Sprache für feingranulierte Autorisierung. Cedar ermöglicht es uns, Zugriffskontrollrichtlinien zu definieren und durchzusetzen, die festlegen, wer welche Aktionen auf welchen Ressourcen ausführen kann.
Cedar-Richtlinien sind deklarativ, was bedeutet, dass sie die Berechtigungen explizit festlegen, ohne prozedurale Logik zu erfordern. Sie sind einfach zu lesen und unterstützen leistungsstarke Bedingungen und attributbasierte Zugriffskontrolle (ABAC).
Einfaches Cedar-Policy-Beispiel
Beginnen wir mit einer einfachen RBAC-Richtlinie (Role-Based Access Control), die Benutzern in der Gruppe "Admin" ermöglicht, bestimmte Aktionen auf allen Ressourcen auszuführen
permit (
principal in UserGroup::"Admin",
action in [
Action::"create",
Action::"read",
Action::"update",
Action::"delete"
],
resource
);Diese Richtlinie bedeutet, dass jedes principal (Benutzer), das zur UserGroup Admin gehört, berechtigt ist, die aufgeführten Aktionen auf allen Ressourcen auszuführen, da die Ressource nicht eingeschränkt ist.
Ein zweites Beispiel, bei dem wir uns einer ABAC-Methode nähern, wäre.
permit (
principal in UserGroup::"JimmysFriends",
action == Action::"viewPhoto",
resource
)
when {
resource.tags.contains("Shared")
};Dies bedeutet, dass Benutzer in JimmysFriends Fotos anzeigen können, wenn das Foto das Tag Shared hat.
In einem Multi-Tenant-SaaS-System möchten wir möglicherweise den Zugriff auf Daten einschränken, die einem bestimmten Tenant gehören. Cedar ermöglicht attributbasierte Bedingungen dafür.
permit (
principal,
action == Action::"view",
resource
)
when {
principal.tenant == resource.tenant
};Dies stellt sicher, dass der Benutzer nur Ressourcen anzeigen kann, die zum gleichen Tenant gehören.
Bei der Implementierung für unseren AVP-basierten PDP werden wir RBAC und Richtlinien ähnlich dem ersten Beispiel verwenden.
AVP für skalierbare und flexible Zugriffskontrolle verwenden
Durch die Integration von (AVP) als Teil unseres zentralen PDP haben wir unser Richtlinienmanagement vereinfacht und die Skalierbarkeit und Flexibilität des Autorisierungssystems verbessert. Die Unterstützung von dynamischen Richtlinien und feingranulierter Zugriffskontrolle durch AVP sind leistungsstarke Werkzeuge für jede SaaS-Anwendung.
Implementierung
Mit der Einführung von AVP konvertieren wir unseren PDP, um AVP für Role Based Access (RBAC) anstelle einer DynamoDB-Berechtigungszuordnung zu verwenden. Wir werden einfach einen zweiten PDP in unsere Lösung deployen und dann umstellen, damit unser PEP den neuen PDP basierend auf AVP verwendet. Dieses gesamte Setup basiert auf der in Teil eins eingeführten Lösung, PEP und PDP für sichere Autorisierung mit Cognito,, und als Voraussetzung sollte diese Lösung deployt sein. Die gesamte Lösung finden Sie auf Serverless Handbook PEP und PDP.
Architekturüberblick
Nur als Erinnerung, der gesamte Code und die gesamte Architektur finden Sie auf Serverless Handbook PEP und PDP
Betrachten wir den Überblick über die Architektur und den Aufrufablauf, können wir sehen, dass es keine größeren Änderungen gibt, sondern dass wir anstelle des Abrufs einer Berechtigungszuordnung aus DynamoDB AVP aufrufen und den Dienst seine Policy-Engine zur Entscheidungsfindung nutzen lassen.

Um den Ablauf während eines API-Zugriffs besser zu verstehen.

Setup und Deploy AVP-basierter PDP
Zuerst müssen wir alle Ressourcen deployen, die von AVP benötigt werden. Dazu gehört, einen Policy Store mit unserem Policy-Schema zu erstellen. Die Policies für unsere drei verschiedenen Rollen mit einer Cedar-basierten Richtlinie, um zu bestimmen, was die Rolle berechtigt ist zu tun, und wir müssen unsere Identity Source einrichten.
Diese Vorlage kann ziemlich lang sein, da die Cedar-Richtlinien und das Schema tendenziell recht umfangreich sind. Daher wurden Teile dieser Vorlage weggelassen, besuchen Sie daher Serverless Handbook PEP und PDP für die vollständige Vorlage.
AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: PDP Service
Parameters:
ApplicationName:
Type: String
Description: Name of owning application
UserManagementStackName:
Type: String
Description: The name of the stack that contains the user management part, e.g the Cognito UserPool
AVPNameSpace:
Type: String
Description: The name space for Amazon Verified Permissions
AllowedPattern: "[a-z]+"
UserPoolId:
Type: String
Description: The ID of the Cognito User Pool
Resources:
PolicyStore:
Type: AWS::VerifiedPermissions::PolicyStore
Properties:
Description: !Sub Policy Store for ${ApplicationName}
ValidationSettings:
Mode: "OFF"
Schema:
CedarJson: !Sub |
{
"${AVPNameSpace}": {
"entityTypes": {
"CognitoUser": {
"shape": {
"type": "Record",
"attributes": {}
},
"memberOfTypes": [
"CognitoUserGroup"
]
},
"CognitoUserGroup": {
"shape": {
"attributes": {},
"type": "Record"
}
},
"Application": {
"shape": {
"attributes": {},
"type": "Record"
}
}
},
"actions": {
"get /rider": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
},
"get /riders": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
},
"get /trainer": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
},
"get /trainers": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
},
"get /unicorn": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
},
"get /unicorns": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {}
},
"principalTypes": [
"CognitoUser"
],
"resourceTypes": [
"Application"
]
}
}
}
}
}
CognitoIdentitySource:
Type: AWS::VerifiedPermissions::IdentitySource
Properties:
Configuration:
CognitoUserPoolConfiguration:
ClientIds:
- Fn::ImportValue: !Sub ${UserManagementStackName}:app-audience
GroupConfiguration:
GroupEntityType: !Sub ${AVPNameSpace}::CognitoUserGroup
UserPoolArn:
Fn::ImportValue: !Sub ${UserManagementStackName}:user-pool-arn
PolicyStoreId: !Ref PolicyStore
PrincipalEntityType: !Sub ${AVPNameSpace}::CognitoUser
AdminUserPolicy:
Type: AWS::VerifiedPermissions::Policy
Properties:
Definition:
Static:
Description: Policy for Admin User group in Cognito
Statement: !Sub |
permit(
principal in ${AVPNameSpace}::CognitoUserGroup::"${UserPoolId}|Admin",
action in [ ${AVPNameSpace}::Action::"get /rider", ${AVPNameSpace}::Action::"get /riders", ${AVPNameSpace}::Action::"get /trainer", ${AVPNameSpace}::Action::"get /trainers", ${AVPNameSpace}::Action::"get /unicorn", ${AVPNameSpace}::Action::"get /unicorns" ],
resource
);
PolicyStoreId: !Ref PolicyStore
LambdaPDPFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: Lambda/AuthZ
Handler: authz.handler
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref AVPCacheTable
- Version: "2012-10-17"
Statement:
Effect: Allow
Action:
- "verifiedpermissions:EvaluatePolicy"
- "verifiedpermissions:GetPolicy"
- "verifiedpermissions:IsAuthorizedWithToken"
Resource: "*"
Environment:
Variables:
JWKS_URL:
Fn::ImportValue: !Sub ${UserManagementStackName}:jwks-url
AUDIENCE:
Fn::ImportValue: !Sub ${UserManagementStackName}:app-audience
POLICY_STORE_ID:
!Ref PolicyStore
NAMESPACE:
!Ref AVPNameSpace
TOKEN_TYPE: "accessToken"Autorisierungslogik
Der PDP Lambda erhält das jwt-token, die action und die resource vom PEP, unserem Lambda Authorizer in der API Gateway. Die Funktion erhält das principal, das Subjekt, aus dem JWT-Token und prüft, ob es eine gecachte Entscheidung gibt. Wenn es keine gibt, ruft sie AVP mit allen Informationen auf, um eine Autorisierungsentscheidung zu erhalten.
import json
import os
import base64
import boto3
import time
from datetime import datetime, timedelta, timezone
policy_store_id = os.getenv("POLICY_STORE_ID")
namespace = os.getenv("NAMESPACE")
token_type = os.getenv("TOKEN_TYPE")
resource_type = f"{namespace}::Application"
resource_id = namespace
action_type = f"{namespace}::Action"
table_name = os.environ["PERMISSION_CACHE_TABLE"]
avp_client = boto3.client("verifiedpermissions")
dynamodb_client = boto3.client("dynamodb")
def decode_token(bearer_token):
return json.loads(base64.b64decode(bearer_token.split(".")[1]).decode("utf-8"))
def generate_access(principal, effect, action, resource):
auth_response = {
"statusCode": 200 if effect == "Allow" else 403,
"principalId": principal,
"effect": effect,
"action": action,
"resource": resource,
}
return auth_response
def get_auth_cache(principal, action):
try:
response = dynamodb_client.get_item(
TableName=table_name, Key={"PK": {"S": principal}, "SK": {"S": action}}
)
item = response.get("Item")
if not item:
return None
ttl = int(item.get("TTL")["N"])
if ttl and ttl < int(time.time() * 1000): # TTL is in milliseconds
return None
return item.get("Effect")["S"]
except Exception as e:
print(f"Error getting auth cache: {e}")
return None
def store_auth_cache(principal, action, 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": action},
"TTL": {"N": str(ttl)},
"Effect": {"S": effect},
},
)
except Exception as e:
print(f"Error storing auth cache: {e}")
def validate_permission(event):
jwt_token = event["jwt_token"]
resource = event["resource"]
action = event["action"]
parsed_token = decode_token(jwt_token)
try:
action_id = f"{action.lower()} {resource.lower()}"
user_principal = parsed_token["sub"]
cached_auth = get_auth_cache(user_principal, action_id)
if cached_auth is None:
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},
)
store_auth_cache(user_principal, action_id, auth_response)
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"},
}
else:
response_body = generate_access(
user_principal, cached_auth, action, resource
)
return {
"statusCode": 200,
"body": json.dumps(response_body),
"headers": {"Content-Type": "application/json"},
}
except Exception as e:
print(f"Error validating permissions: {e}")
response_body = generate_access(parsed_token["sub"], "Deny", action, resource)
return {
"statusCode": 200,
"body": json.dumps(response_body),
"headers": {"Content-Type": "application/json"},
}
def handler(event, context):
print(f"Event: {json.dumps(event)}")
permissions = validate_permission(event)
return permissions
PEP aktualisieren, um AVP-basierten PDP zu verwenden
Um unseren PEP zu aktualisieren, damit er den neuen AVP-basierten PDP anstelle des DynamoDB-basierten verwendet, navigieren Sie zum Ordner API und ändern Sie die Datei samconfig.yaml, aktualisieren Sie den Wert von PDP zu PDPStackName=pep-pdp-cognito-pdp-auth-service-avp und deployen Sie dann den API-Teil der Lösung neu. Dies wird nun den verwendeten PDP austauschen.
Testen
Um das Setup von AVP zu testen, können wir zum AVP-Teil der Konsole navigieren.
Unter Policy Stores sollten Sie den Policy Store sehen, der für unseren PDP erstellt wurde.

Wenn Sie auf die ID des Policy Store klicken und Policies im Menü auswählen, sehen wir die Liste der drei erstellten Richtlinien.

Wenn Sie die Riders-Richtlinie auswählen, können wir jetzt die erstellte Richtlinie inspizieren.

Wenn Sie Schema im Menü auswählen, können wir das erstellte Schema in einer visuellen Form inspizieren.


Jetzt können wir zum Test Bench navigieren, um unsere Richtlinien zu testen, füllen Sie die Informationen wie im Bild unten gezeigt aus. Die Gruppe muss mit der Cognito User Pool-ID präfixiert sein und dem Muster <COGNITO_USER_POOL_ID>|<GROUP_NAME> folgen

Wenn wir die Aktion /get trainers auswählen und auf Run Authorization request klicken, sollten wir eine Ablehnung zurückbekommen, da die Trainer-Rolle keinen Zugriff auf diese Action hat

Wenn wir zur Aktion /get trainer wechseln, sollten wir stattdessen eine Zulassung zurückbekommen.

Zusammenfassung und Fazit
Die Implementierung von PEP und PDP in unserem Autorisierungsfluss bietet eine hoch skalierbare, flexible und sichere Möglichkeit, den Zugriff auf Ressourcen zu kontrollieren. Durch die Nutzung von AWS Lambda und API Gateway können wir ein serverloses Autorisierungssystem erstellen, das Authentifizierungs- und Autorisierungsprobleme trennt, bei Bedarf skaliert und das Richtlinienmanagement vereinfacht.
Mit der Ergänzung von Role-Based Access Control und Amazon Verified Permissions (AVP), kombiniert mit Caching für verbesserte Leistung, können wir eine Autorisierungslösung erstellen, die sowohl den aktuellen als auch den zukünftigen Anforderungen entspricht. Die Verwendung von AVP in unseren SaaS-Lösungen bietet uns eine sehr leistungsstarke Möglichkeit, Multi-Tenancy zu handhaben.
Viel Spaß beim Codieren und bleiben Sie sicher!
Quellcode
Das gesamte Setup mit detaillierten Deployungsanweisungen und dem gesamten Code finden Sie auf Serverless Handbook PEP und PDP
Abschließende Worte
Vergessen Sie nicht, mir auf LinkedIn und X zu folgen, um weitere Inhalte zu erhalten, und lesen Sie meine anderen Blogs
Wie Werner sagt! Jetzt loslegen!