PEP et PDP pour une autorisation sécurisée avec AVP

Ce fichier a ete traduit automatiquement par IA, des erreurs peuvent survenir
Dans la première partie, PEP et PDP pour une autorisation sécurisée avec Cognito, j'ai introduit le concept des Points de Mise en œuvre de Politique (PEP) et des Points de Décision de Politique (PDP) en utilisant Lambda Authorizer dans API Gateway comme PEP et un service basé sur Lambda comme PDP. La décision d'autorisation était basée sur les rôles des utilisateurs, représentés par des groupes Cognito, avec les autorisations mappées dans une table DynamoDB.
Lorsque nous exécutons une configuration PEP et PDP dans une solution SaaS multi-tenant, nous avons certainement besoin de quelque chose de plus puissant qu'une table DynamoDB avec des autorisations. C'est pourquoi dans cette partie, nous allons examiner l'utilisation d'Amazon Verified Permissions (AVP) comme fondement de notre PDP. Ce qui serait un excellent choix pour faire de l'autorisation dans SaaS.
Qu'est-ce que Amazon Verified Permissions ?
Amazon Verified Permissions (AVP) est un service entièrement géré qui simplifie la mise en œuvre d'un contrôle d'accès granulaire dans nos applications. AVP agit comme un Point de Décision de Politique (PDP) central, où il évalue les politiques et prend des décisions d'autorisation basées sur les attributs de la demande, les rôles des utilisateurs, etc. Il ne supporte pas seulement le contrôle d'accès basé sur les rôles (RBAC), mais AVP supporte également le contrôle d'accès basé sur les attributs (ABAC) et d'autres politiques dynamiques, permettant des décisions d'autorisation plus flexibles et granulaires.
Avec AVP, notre fonction Lambda (agissant comme le PDP) n'a plus besoin d'analyser et d'évaluer manuellement les politiques, et nous n'avons pas besoin de stocker le mappage des autorisations dans une table DynamoDB. Au lieu de cela, nous définissons et gérons nos politiques directement dans AVP, qui prend ensuite la décision d'autorisation pour nous.
Solutions Open Source alternatives à AVP
Bien qu'AVP soit un excellent choix pour les applications, en particulier si elles fonctionnent sur AWS, ce n'est pas la seule solution pour mettre en œuvre une autorisation granulaire. Une alternative est Oso, un cadre d'autorisation open source conçu pour fournir un contrôle d'accès basé sur des politiques flexibles. Oso nous permet de définir la logique d'autorisation dans un langage déclaratif et s'intègre à nos applications sur différents environnements. Le choix entre AVP et Oso dépend de notre cas d'utilisation spécifique, de notre infrastructure, etc.
Mise en cache des décisions d'autorisation
Maintenant que nous avons examiné l'utilisation d'AVP comme partie centrale de notre PDP, nous devrions penser à la mise en cache. Il y a des avantages et des inconvénients à la mise en cache, par exemple AWS IAM ne met jamais en cache aucune décision, ce qui garantit que les mises à jour des politiques sont reflétées immédiatement. Avec un cache, nous pouvons réduire le nombre d'appels à AVP et ainsi réduire les coûts. Nous pouvons également mettre en cache les décisions dans le client ou dans nos systèmes backend. Je ne vais pas entrer dans les détails de ce sujet plus que cela dans cette solution, nous utiliserons un cache externe dans notre système backend. Le cache sera placé dans DynamoDB. La raison de mettre en cache dans une source externe, comme DynamoDB, au lieu de dans la mémoire de la fonction Lambda, est que toutes les invocations de la fonction Lambda bénéficieront du cache. Avec un tel cache, nous aurions besoin d'un moyen d'invalider le cache si un ensemble d'autorisations d'un utilisateur est modifié. Je n'implémenterai aucun mécanisme d'invalidation de cache dans ce billet.
Comprendre AVP et le langage de politique Cedar
Amazon Verified Permissions (AVP) utilise Cedar, un langage de politique basé sur le code, conçu pour une autorisation granulaire. Cedar nous permet de définir et d'appliquer des politiques de contrôle d'accès qui dictent qui peut effectuer quelles actions sur quelles ressources.
Les politiques Cedar sont déclaratives, ce qui signifie qu'elles indiquent explicitement les autorisations sans nécessiter de logique procédurale. Elles sont conçues pour être faciles à lire tout en prenant en charge des conditions puissantes et un contrôle d'accès basé sur les attributs (ABAC).
Exemple de politique Cedar de base
Commençons par une politique RBAC (Role-Based Access Control) simple qui permet aux utilisateurs du groupe "Admin" d'effectuer certaines actions sur toutes les ressources
permit (
principal in UserGroup::"Admin",
action in [
Action::"create",
Action::"read",
Action::"update",
Action::"delete"
],
resource
);Cette politique signifie que tout principal (utilisateur) appartenant au groupe UserGroup Admin est autorisé à effectuer les actions listées sur toutes les ressources, car la ressource n'est pas restreinte.
Un deuxième exemple, où nous nous orientons vers une méthode ABAC, serait.
permit (
principal in UserGroup::"JimmysFriends",
action == Action::"viewPhoto",
resource
)
when {
resource.tags.contains("Shared")
};Cela signifie que les utilisateurs de JimmysFriends peuvent voir des photos lorsque la photo a la balise Shared.
Dans un système SaaS multi-tenant, nous pourrions vouloir restreindre l'accès aux données appartenant à un locataire spécifique. Cedar permet des conditions basées sur des attributs pour cela.
permit (
principal,
action == Action::"view",
resource
)
when {
principal.tenant == resource.tenant
};Cela garantit que l'utilisateur ne peut voir que les ressources appartenant au même locataire.
Dans l'implémentation de notre PDP basé sur AVP, nous utiliserons RBAC et une politique similaire au premier exemple.
Utiliser AVP pour un contrôle d'accès évolutif et flexible
En intégrant (AVP) comme partie de notre PDP central, nous avons simplifié la gestion de nos politiques et amélioré l'évolutivité et la flexibilité du système d'autorisation. La prise en charge par AVP des politiques dynamiques et du contrôle d'accès granulaire sont des outils puissants pour toute application SaaS.
Implémentation
Avec l'introduction d'AVP, convertissons notre PDP pour utiliser AVP pour le contrôle d'accès basé sur les rôles (RBAC) au lieu d'un mappage d'autorisations DynamoDB. Nous allons simplement déployer un deuxième PDP dans notre solution et échanger afin que notre PEP utilise le nouveau PDP basé sur AVP au lieu de celui-ci. Toute cette configuration est basée sur la solution introduite dans la première partie, PEP et PDP pour une autorisation sécurisée avec Cognito,, et comme prérequis, cette solution doit être déployée. Vous trouverez toute la solution sur Serverless Handbook PEP et PDP.
Aperçu de l'architecture
Juste pour rappel, tout le code et l'architecture peuvent être trouvés sur Serverless Handbook PEP et PDP
En regardant l'aperçu de l'architecture et le flux d'appels, nous pouvons voir qu'il n'y a pas de changements majeurs, mais au lieu de récupérer un mappage d'autorisations depuis DynamoDB, nous appellerons AVP et laisserons le service utiliser son moteur de politique pour prendre une décision d'autorisation.

Pour mieux comprendre le flux lors d'un accès API.

Configuration et déploiement du PDP basé sur AVP
Tout d'abord, nous devons déployer toutes les ressources nécessaires à AVP, ce que nous devons faire est de créer un Policy Store avec notre schéma de politique. Les Policies pour nos trois rôles différents avec une politique basée sur Cedar pour déterminer ce que le rôle a le droit de faire, et nous devons configurer notre Identity Source.
Ce modèle peut être assez long car les politiques et schémas Cedar ont tendance à être assez volumineux. Par conséquent, certaines parties de ce modèle ont été omises, donc visitez Serverless Handbook PEP et PDP pour le modèle complet.
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"Logique d'autorisation
Le PDP Lambda recevra le jwt-token, l'action et la ressource du PEP, notre Lambda Authorizer dans API Gateway. La fonction obtiendra le principal, le sujet, à partir du JWT-Token et vérifiera s'il y a une décision en cache. S'il n'y en a pas, elle appellera AVP avec toutes les informations pour obtenir une décision d'autorisation.
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
Mettre à jour PEP pour utiliser le PDP basé sur AVP
Pour mettre à jour notre PEP afin d'utiliser le nouveau PDP basé sur AVP au lieu de celui basé sur DynamoDB, naviguez dans le dossier API et modifiez le fichier samconfig.yaml, mettez à jour la valeur de PDP à PDPStackName=pep-pdp-cognito-pdp-auth-service-avp puis déployez à nouveau la partie API de la solution. Cela va maintenant échanger le PDP utilisé.
Tests
Pour tester la configuration d'AVP, nous pouvons naviguer dans la partie AVP de la console.
Sous Policy Stores, vous devriez voir le store de politique qui a été créé pour notre PDP.

En cliquant sur l'ID du store de politique et en sélectionnant Policies dans le menu, nous voyons la liste des trois politiques créées.

En sélectionnant la politique Riders, nous pouvons maintenant inspecter la politique créée.

En sélectionnant Schema dans le menu, nous pouvons inspecter le schéma créé sous une forme visuelle.


Maintenant, nous pouvons naviguer vers le Test Bench pour tester nos politiques, remplissez les informations comme indiqué dans l'image ci-dessous. Le groupe doit être préfixé par l'ID du Cognito User Pool et suivre le modèle <COGNITO_USER_POOL_ID>|<GROUP_NAME>

Si nous sélectionnons l'action /get trainers et cliquons sur Run Authorization request, nous devrions obtenir un refus, car le rôle Trainer n'a pas accès à cette Action

En changeant pour l'action /get trainer, nous devrions obtenir une autorisation.

Résumé et conclusion
L'implémentation de PEP et PDP dans notre flux d'autorisation offre une manière très évolutive, flexible et sécurisée de contrôler l'accès aux ressources. En utilisant AWS Lambda et API Gateway, nous pouvons construire un système d'autorisation sans serveur qui sépare les préoccupations d'authentification et d'autorisation, s'adapte à la demande et simplifie la gestion des politiques.
Avec l'ajout du contrôle d'accès basé sur les rôles et d'Amazon Verified Permissions (AVP), combiné à la mise en cache pour une performance améliorée, nous pouvons créer une solution d'autorisation qui répond aux besoins actuels et futurs. L'utilisation d'AVP dans nos solutions SaaS nous offre une manière très puissante de gérer le multi-locataire.
Bonne programmation, et restez sécurisés !
Code source
Toute la configuration, avec des instructions de déploiement détaillées et tout le code, peut être trouvée sur Serverless Handbook PEP et PDP
Derniers mots
N'oubliez pas de me suivre sur LinkedIn et X pour plus de contenu, et lisez le reste de mes Blogs
Comme le dit Werner ! Maintenant, allez construire !