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

2025-05-06
This post cover image
#aws
#cloud
#serverless
#AuthZ
Voice provided by Amazon Polly

Ce fichier a été 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 de Policy Enforcement Points (PEPs) et de Policy Decision Points (PDPs) 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 détenus par les utilisateurs, représentés par les groupes Cognito, avec les permissions mappées dans une table DynamoDB.

Dans la deuxième partie, PEP et PDP pour une autorisation sécurisée avec AVP, nous avons remplacé la mappage des permissions DynamoDB par Amazon Verified Permissions (AVP) et implémenté un système de contrôle d'accès basé sur les rôles (RBAC) utilisant des politiques Cedar. Cela nous a donné une manière beaucoup plus puissante de gérer l'autorisation, en particulier pour les applications SaaS multi-locataires.

Maintenant, dans cette troisième partie, nous allons faire passer notre système d'autorisation au niveau supérieur en ajoutant le contrôle d'accès basé sur les attributs (ABAC) aux côtés de RBAC en utilisant AVP. Cette combinaison fournit des décisions d'autorisation encore plus dynamiques et contextuelles, ce qui est exactement ce dont nous avons besoin pour les applications multi-locataires complexes.

Comprendre le contrôle d'accès basé sur les attributs (ABAC)

Alors, qu'est-ce que l'ABAC exactement ? À la base, l'ABAC est une stratégie d'autorisation qui prend des décisions basées sur les attributs associés aux utilisateurs, aux ressources, aux actions et à l'environnement. Contrairement au RBAC, qui accorde des permissions uniquement basées sur les rôles, l'ABAC évalue divers attributs pour prendre des décisions d'autorisation plus finement granulaires.

Pensez-y de cette façon - le RBAC est comme avoir différentes clés (rôles) qui ouvrent différentes portes (ressources). L'ABAC, d'autre part, est comme avoir une serrure intelligente qui considère non seulement qui vous êtes, mais aussi d'où vous venez, à quelle heure c'est, ce que vous essayez de faire, et même ce qui se passe autour de vous avant de décider si vous laissez entrer.

Par exemple, certains attributs couramment utilisés dans l'ABAC pourraient être :

Attributs utilisateur : Département, emplacement, niveau de habilitation, ancienneté
Attributs de ressource : Classification, propriétaire, niveau de sensibilité
Attributs d'action : Heure de la journée, statut de chiffrement, méthode d'accès
Attributs environnementaux : Type d'appareil, emplacement du réseau, niveau de sécurité

Par exemple, une politique ABAC pourrait stipuler que les data scientists peuvent accéder aux données sensibles, mais uniquement depuis les réseaux d'entreprise, pendant les heures de bureau, et s'ils ont suivi une formation de sécurité au cours des 6 derniers mois.

Pourquoi combiner RBAC et ABAC ?

Mais si l'ABAC est si puissant, pourquoi ne pas l'utiliser à la place du RBAC ? Je dirais que le RBAC est vraiment simple à comprendre et à mettre en œuvre, ce qui le rend idéal pour les besoins d'autorisation de base. Cependant, lorsque les permissions doivent être plus granulaires ou dépendantes du contexte, cela peut devenir très difficile et complexe à gérer.

D'autre part, l'ABAC offre une flexibilité incroyable mais peut être très complexe à mettre en œuvre à partir de zéro et plus difficile à raisonner. En combinant les deux approches, nous obtenons le meilleur des deux mondes !

Dans notre exemple de course de licornes, nous pouvons utiliser des rôles pour définir des modèles d'accès larges (Admin, Trainer, Rider), puis nous utiliserons un attribut dataAccess pour restreindre l'accès à des ensembles de données spécifiques au sein de ces rôles.

Implémenter ABAC dans Amazon Verified Permissions

AVP avec son langage de politique Cedar est parfaitement adapté pour implémenter l'ABAC. Les politiques Cedar peuvent évaluer des conditions basées sur des attributs du principal (utilisateur), de la ressource, de l'action et du contexte.

Mappage de jetons dans AVP - Un concept critique

Une partie importante de la mise en œuvre de l'ABAC est de comprendre comment les réclamations de jetons sont mappées à votre schéma de politique Cedar. AVP gère cela différemment selon que vous utilisez des jetons ID ou des jetons d'accès.

Jeton ID : Les réclamations sont mappées aux attributs de l'entité principal représentant l'utilisateur
Jeton d'accès : Les réclamations sont mappées à l'objet context.token dans l'évaluation de la politique

Cette différence est très importante lors de l'écriture de politiques Cedar car elle affecte la façon dont vous accédez aux attributs dans vos conditions de politique !

Par exemple, avec un jeton ID, une politique ressemblerait à ceci

permit (principal, action, resource)
when { principal.department == "Engineering" };

Mais avec un jeton d'accès, nous devons utiliser l'objet contexte et non le principal

permit (principal, action, resource)
when { context.token["department"] == "Engineering" };

Dans notre implémentation, nous utiliserons l'approche du jeton d'accès car je pense qu'elle fournit une séparation plus nette entre l'identité (qui est l'utilisateur) et les permissions d'accès et les attributs (ce qu'ils peuvent faire et quelles propriétés ils ont).

Pourquoi utiliser des jetons d'accès pour l'autorisation

Alors que les jetons ID sont principalement destinés à l'authentification (preuve d'identité), les jetons d'accès sont spécifiquement conçus pour l'autorisation (détermination des permissions). Je vois plusieurs avantages à utiliser des jetons d'accès :

Séparation des préoccupations : Les détails d'authentification restent dans les jetons ID, tandis que les détails d'autorisation résident dans les jetons d'accès
Taille de jeton réduite : Les jetons ID ne seront pas gonflés avec des attributs d'autorisation
Durée de vie indépendante : L'expiration du jeton d'accès peut être plus courte que celle du jeton ID
Révocation de jeton plus facile : Vous pouvez révoquer l'accès sans affecter l'authentification

Dans notre PDP basé sur AVP, nous utiliserons des jetons d'accès et mapperons les réclamations personnalisées au contexte pour de meilleures décisions d'autorisation.

Implémenter AVP avec RBAC et ABAC

Maintenant que nous comprenons les concepts, mettons-nous au travail et implémentons notre système d'autorisation amélioré !

Architecture

Notre architecture reste similaire mais avec quelques changements. Nous ajoutons une nouvelle fonction Lambda pour pouvoir enrichir notre jeton d'accès avec des réclamations personnalisées.

Diagramme d'architecture montrant PEP et PDP avec ABAC

Cela nous donnera un flux d'appel mis à jour comme celui-ci.

Flux d'appel PEP et PDP avec ABAC

Les changements que nous devons faire seraient les suivants.

Tout d'abord, nous devons ajouter dataAccess comme réclamation personnalisée dans notre jeton d'accès, pour cela nous devons configurer une fonction Lambda de Pré-génération de jeton, qui sera invoquée par Cognito.

Ensuite, nous devons mettre à jour notre schéma et ajouter l'attribut dataAccess sur chaque action que nous voulons que la politique puisse évaluer selon l'ABAC.

Enfin, nous devons mettre à jour nos politiques Cedar pour qu'elles soient contextuelles et évaluent à la fois le rôle (RBAC) et les attributs (ABAC)

Ajouter des attributs personnalisés aux jetons d'accès

Pour implémenter l'ABAC, nous avons besoin d'un moyen d'ajouter des attributs personnalisés à nos jetons d'accès. Nous utiliserons le déclencheur Pre-Token Generation Lambda de Cognito pour cela, et nous devons utiliser la version 2 ou 3 de l'événement. Avec la version 1, nous ne pouvons modifier que le jeton ID.

Voici une implémentation qui ajoute l'attribut dataAccess des attributs utilisateur au jeton d'accès :

def handler(event, context):
    user_attributes = event["request"]["userAttributes"]
    claims_to_add_to_access_token = {}

    if "custom:dataAccess" in user_attributes:
        data_access_values = [
            value.strip() for value in user_attributes["custom:dataAccess"].split(",")
        ]
        claims_to_add_to_access_token["custom:dataAccess"] = data_access_values

    response = {
        "claimsAndScopeOverrideDetails": {
            "accessTokenGeneration": {
                "claimsToAddOrOverride": claims_to_add_to_access_token,
            }
        }
    }

    event["response"] = response
    return event

Cette fonction vérifie si l'utilisateur a un attribut custom:dataAccess. Si trouvé, il divise les valeurs séparées par des virgules en un tableau et les ajoute au jeton d'accès. De cette façon, nos politiques AVP peuvent vérifier si l'utilisateur a accès à des catégories de données spécifiques.

Pour que Cognito appelle notre fonction, nous devons configurer les déclencheurs Lambda du pool d'utilisateurs.

  PreTokenGenerationFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/PreTokenGeneration
      Handler: index.handler
      Runtime: python3.12
      Architectures:
        - x86_64
      MemorySize: 128
      Description: Une fonction Lambda qui ajoute des attributs personnalisés au jeton d'accès JWT
      Policies:
        - AWSLambdaBasicExecutionRole

  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UsernameConfiguration:
        CaseSensitive: false
      AutoVerifiedAttributes:
        - email
      UserPoolName: !Sub ${ApplicationName}-user-pool
      Schema:
        - Name: email
          AttributeDataType: String
          Mutable: false
          Required: true
        - Name: name
          AttributeDataType: String
          Mutable: true
          Required: true
        - Name: dataAccess
          AttributeDataType: String
          Mutable: true
          Required: false
      LambdaConfig:
        PreTokenGenerationConfig:
          LambdaArn: !GetAtt PreTokenGenerationFunction.Arn
          LambdaVersion: V2_0

Schéma de politique Cedar avec attributs de contexte

Notre schéma Cedar définit la structure des principaux, des ressources, des actions et de leurs relations. Pour l'ABAC, nous devons mettre à jour le schéma pour inclure les attributs de contexte :

{
  "${AVPNameSpace}": {
    "actions": {
      "get /rider": {
        "appliesTo": {
          "context": {
            "type": "Record",
            "attributes": {
              "dataAccess": {
                "type": "String",
                "required": true
              }
            }
          },
          "principalTypes": ["CognitoUser"],
          "resourceTypes": ["Application"]
        }
      }
      // Autres actions...
    }
  }
}

Ce schéma nous permet d'utiliser l'attribut dataAccess dans nos politiques d'autorisation.

Politiques qui combinent RBAC et ABAC

Nos politiques Cedar combinent maintenant la logique RBAC (basée sur les rôles) et ABAC (basée sur les attributs) :

permit(
  principal in ${AVPNameSpace}::CognitoUserGroup::"${UserPoolId}|Admin",
  action in [
    ${AVPNameSpace}::Action::"get /rider",
    ${AVPNameSpace}::Action::"get /riders",
    ${AVPNameSpace}::Action::"get /trainer",
    // plus d'actions...
  ],
  resource
)
when {
  // Condition ABAC utilisant des attributs
  context.dataAccess == "" ||
  (context.token has "custom:dataAccess" &&
   context.token["custom:dataAccess"].contains(context.dataAccess))
};

Cette politique permet aux utilisateurs du groupe Admin d'accéder aux points de terminaison (partie RBAC), mais uniquement si l'une de ces conditions est vraie (partie ABAC) :

  • Aucune vérification d'accès aux données spécifiques n'est nécessaire (context.dataAccess vide), OU
  • L'utilisateur a l'attribut d'accès aux données requis dans son jeton

Décomposons un peu la condition pour une meilleure compréhension.

  • Si context.dataAccess est vide, nous n'appliquons pas de restrictions d'accès aux données
  • Sinon, nous vérifions si le jeton de l'utilisateur a la réclamation custom:dataAccess qui contient la valeur requise

Logique d'autorisation contextuelle

Notre fonction Lambda PDP gère maintenant l'autorisation basée sur le contexte. Elle extrait les attributs pertinents des requêtes et les inclut dans l'appel d'autorisation AVP. La fonction ci-dessous a été simplifiée et certaines parties ont été supprimées, vérifiez le code source pour une version complète.

def validate_permission(event):
    jwt_token = event["jwt_token"]
    resource = event["resource"]
    action = event["action"]
    resource_tags = {}

    if "resource_tags" in event:
        resource_tags = event["resource_tags"]

    parsed_token = decode_token(jwt_token)

    try:
        action_id = f"{action.lower()} {resource.lower()}"
        user_principal = parsed_token["sub"]

        # Appeler AVP avec le contexte pour la décision d'autorisation
        context = {
            "dataAccess": resource_tags.get("Data", "")
        }

        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},
            context=context
        )

        # Mettre en cache le résultat avec le hachage du contexte
        store_auth_cache(user_principal, action_id, context_hash, auth_response)

        # Générer la réponse
        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"},
        }

    except Exception as e:
        print(f"Erreur lors de la validation des 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"},
    }

Avec cette implémentation, notre PDP évalue maintenant à la fois les règles basées sur les rôles (RBAC) et les conditions basées sur les attributs (ABAC).

Mise en cache contextuelle

L'un des défis de l'ajout de l'ABAC est que les décisions d'autorisation dépendent maintenant non seulement de qui vous êtes et de ce que vous essayez de faire, mais aussi du contexte. Cela signifie que notre stratégie de mise en cache doit être mise à jour.

Nous inclurons les informations de contexte dans notre clé de cache en créant un hachage des attributs de contexte :

def generate_context_hash(context_dict):
    sorted_items = sorted(context_dict.items())
    context_str = json.dumps(sorted_items)
    return hashlib.md5(context_str.encode()).hexdigest()

def get_auth_cache(principal, action, context_hash):
    try:
        response = dynamodb_client.get_item(
            TableName=table_name, 
            Key={
                "PK": {"S": principal}, 
                "SK": {"S": f"{action}#{context_hash}"}
            }
        )

        item = response.get("Item")
        if not item:
            return None

        ttl = int(item.get("TTL")["N"])
        if ttl and ttl < int(time.time() * 1000):  # TTL est en millisecondes
            return None

        return item.get("Effect")["S"]

    except Exception as e:
        print(f"Erreur lors de l'obtention du cache d'authentification : {e}")
        return None

def store_auth_cache(principal, action, context_hash, 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": f"{action}#{context_hash}"},
                "TTL": {"N": str(ttl)},
                "Effect": {"S": effect},
            },
        )

    except Exception as e:
        print(f"Erreur lors du stockage du cache d'authentification : {e}")

En incluant un hachage du contexte dans notre clé de cache, nous nous assurons que les décisions d'autorisation sont correctement mises en cache et récupérées en fonction à la fois du rôle de l'utilisateur et des attributs de contexte pertinents.

Modifier le PEP pour transmettre le contexte

Notre PEP (Lambda Authorizer) a également besoin d'une petite mise à jour pour transmettre les informations de contexte au PDP, ceci est une implémentation très basique, et dans un système de production vous auriez sûrement quelque chose de plus sophistiqué, comme regarder les balises de ressources sur les ressources AWS.

def get_resource_tags_for_path(path):
    if (
        path.startswith("/unicorn")
        or path.startswith("/rider")
        or path.startswith("/trainer")
    ):
        return {"Data": "Unicorn"}
    elif path.startswith("/race"):
        return {"Data": "Races"}
    else:
        return {}

# À l'intérieur de la fonction handler
resource_tags = get_resource_tags_for_path(path)

data = {
    "jwt_token": token,
    "resource": path,
    "action": method,
    "resource_tags": resource_tags,
}

Cette fonction détermine à quelle catégorie de données appartient une ressource en fonction de son chemin, et transmet cette information au PDP pour une autorisation contextuelle.

Tester l'implémentation ABAC

Maintenant que nous avons notre implémentation en place, il est temps de la tester ! Voici comment nous pouvons tester notre solution combinée RBAC + ABAC.

Tout d'abord, nous devons créer ou mettre à jour un utilisateur dans différents groupes Cognito (Admin, Trainer, Rider). Nous devons également attribuer différents attributs dataAccess à ces utilisateurs (par exemple, "Unicorn", "Races").

Ensuite, naviguez vers la page web déployée avec la distribution CloudFront et inspectez les jetons JWT, les cookies. Image montrant les cookies

Si nous copions le jeton d'accès et le décodons, j'utilise jwt.io, nous pouvons voir que mon utilisateur a la réclamation custom:dataAccess que notre PEP et PDP utiliseront plus tard pour les permissions.

Maintenant, nous pouvons utiliser un outil comme Bruno ou Postman pour inclure le jeton d'accès dans les appels à l'API. Bien sûr, selon la façon dont vous avez configuré l'accès, vous devriez obtenir soit un Allow soit un Deny du PDP, ce qui entraînera des résultats différents de l'API. Voici quelques exemples de vues où j'utilise Bruno pour appeler l'API avec et sans accès.

Image montrant l'autorisation

Image montrant le refus

Conclusion

En combinant RBAC et ABAC avec Amazon Verified Permissions, nous avons créé un système d'autorisation puissant et flexible qui peut gérer des exigences de contrôle d'accès complexes. Cette approche offre la simplicité de l'accès basé sur les rôles tout en permettant le contrôle finement granulaire des décisions basées sur les attributs.

Cette approche d'implémentation peut être utilisée dans des applications SaaS multi-locataires où la ségrégation des données, basée sur le locataire, et le contrôle d'accès contextuel sont essentiels. Avec AVP gérant l'évaluation des politiques et notre architecture serverless pour l'application, nous avons une solution évolutive et maintenable pour même les besoins d'autorisation les plus exigeants.

Code source

Toute la configuration, avec des instructions de déploiement détaillées, et tout le code peuvent être trouvés sur Serverless Handbook PEP et PDP

Mots finaux

N'oubliez pas de me suivre sur LinkedIn et X pour plus de contenu, et lisez le reste de mes Blogs

Comme dit Werner ! Maintenant, allez construire !

Post Quiz

Test what you just learned by doing this five question quiz.
Scan the QR code below or click the link above.

Powered by kvist.ai your AI generated quiz solution!