AWS IAM SDK obtient tous les documents de stratégie pour un rôle spécifique lors de l'utilisation des rôles de groupe Cognito

Aug 17 2020

J'utilise des groupes de groupes d'utilisateurs AWS Cognito pour gérer les autorisations d'une API API Gateway. Je pense que c'est une utilisation valide pour les groupes, comme le dit la documentation ici:https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html#using-groups-to-control-permission-with-amazon-api-gateway.

Malheureusement, la documentation est fondamentalement inexistante pour ce cas d'utilisation pour autant que je sache (à part ce petit paragraphe). J'essaie de comprendre comment cela fonctionne avec une fonction Lambda d'autorisation API Gateway personnalisée. J'ai créé un rôle de test et l'ai attribué à un groupe de test dans Cognito. Le rôle est associé à une seule stratégie, mais à l'avenir, les rôles auront plusieurs stratégies.

Maintenant, dans mon autorisateur personnalisé, je valide déjà un jeton d'accès, etc., et tout fonctionne bien. J'essaie maintenant d'ajouter ce contrôle d'accès précis en utilisant les groupes/rôles/politiques. J'ai installé le SDK IAM et j'ai fouillé pour voir quels appels je dois passer. Il semble qu'il n'y ait pas de moyen facile d'obtenir un rôle et toutes ses politiques. Le meilleur que j'ai trouvé est le suivant:

public async Task<IEnumerable<string>> GetGroupPermissionsForUserAsync(Models.User user)
{
    if (!user.UserAttributes.TryGetValue(UserAttributeName.UserPoolId, out var userPoolId))
    {
        return null;
    }

    var groups = await GetUserGroups(user.Username, userPoolId);
    var groupRoleArn = groups.FirstOrDefault()?.RoleArn;
    var policies = new List<string>();
    if (!string.IsNullOrWhiteSpace(groupRoleArn))
    {
        var roleName = groupRoleArn.Substring(groupRoleArn.IndexOf('/') + 1);
        var rolePoliciesResponse = await _iamClient.ListAttachedRolePoliciesAsync(new ListAttachedRolePoliciesRequest { RoleName = roleName });
        
        foreach (var rolePolicy in rolePoliciesResponse.AttachedPolicies)
        {
            var policyVersionsResponse = await _iamClient.ListPolicyVersionsAsync(new ListPolicyVersionsRequest
            {
                PolicyArn = rolePolicy.PolicyArn
            });

            var latestPolicyVerson = policyVersionsResponse.Versions.OrderByDescending(x => x.CreateDate).LastOrDefault();
            var policyVersionResponse = await _iamClient.GetPolicyVersionAsync(new GetPolicyVersionRequest
            {
                PolicyArn = rolePolicy.PolicyArn,
                VersionId = latestPolicyVerson.VersionId
            });
            
            if (!string.IsNullOrWhiteSpace(policyVersionResponse?.PolicyVersion.Document))
            {
                policies.Add(HttpUtility.UrlDecode(policyVersionResponse.PolicyVersion.Document));
            }
        }
    }

    return policies;
}

private async Task<IEnumerable<GroupType>> GetUserGroups(string username, string userPoolId)
{
    string nextToken = null;
    var groups = new List<GroupType>();
    do
    {
        var response = await _cognitoClient.AdminListGroupsForUserAsync(new AdminListGroupsForUserRequest
        {
            Username = username,
            UserPoolId = userPoolId
        });

        groups.AddRange(response.Groups);

        nextToken = response.NextToken;
    } while (!string.IsNullOrWhiteSpace(nextToken));

    return groups.OrderBy(x => x.Precedence);
}

Comme vous pouvez le voir, je dois faire un tas d'appels juste pour obtenir les politiques d'un rôle.

  1. _cognitoClient.AdminListGroupsForUserAsyncpour obtenir les groupes de l'utilisateur depuis Cognito.
  2. _iamClient.ListAttachedRolePoliciesAsyncpour obtenir les stratégies associées aux rôles.
  3. _iamClient.ListPolicyVersionsAsyncpour obtenir les versions de chaque stratégie.
  4. _iamClient.GetPolicyVersionAsyncpour obtenir la version de politique individuelle qui a finalement le document de politique.

ListPolicyVersionsAsyncrenvoie une réponse avec une propriété de document mais elle est toujours nulle pour une raison quelconque, d'où la nécessité de l'appel supplémentaire GetPolicyVersionAsync.

Non seulement tout cela ajoute de la latence (ce qui est un problème sur une fonction d'autorisation où chaque appel à l'API va passer par ce code), mais cela me laisse avec juste un tas de politiques individuelles dont j'ai besoin d'une manière ou d'une autre dédupliquer avant de revenir à API Gateway.

Il n'y a vraiment pas de moyen plus simple / plus rapide de le faire? Existe-t-il un moyen d'obtenir toutes ces informations avec moins d'appels et existe-t-il un moyen d'aplanir les politiques au cas où leurs règles se chevaucheraient ?

Réponses

lemming Aug 24 2020 at 17:00

Il existe tellement de façons différentes de configurer l'autorisation avec Cognito et API Gateway qu'il est difficile de dire ce qui vous conviendra le mieux sans savoir à quelles ressources AWS vous essayez d'accéder via vos méthodes API Gateway. Par exemple, vos méthodes API Gateway s'intègrent-elles aux méthodes lambda qui accèdent aux informations de la base de données, ou appellent-elles directement les services ?

Si je comprends bien, vous souhaitez combiner les autorisations pour les groupes (potentiellement) multiples auxquels un utilisateur peut appartenir afin de proposer un ensemble unique d'autorisations à utiliser pour cette demande particulière. Ainsi, le userrôle peut autoriser l'accès aux données utilisateur, et le adminrôle peut étendre davantage l'accès à d'autres terminaux d'administration ?

En supposant que vous ayez un scénario assez standard dans lequel vos méthodes API Gateway s'intègrent à une méthode lambda qui accède ensuite aux ressources AWS sous-jacentes telles qu'une base de données, vous pouvez utiliser les autorisateurs personnalisés comme suit :

Cela simplifie beaucoup les choses si les autorisateurs personnalisés renvoient simplement les autorisations nécessaires pour exécuter la méthode API demandée :

{
  "principalId": "sub-from-ID-token",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Allow",
        "Resource": [
          "arn:aws:execute-api:eu-west-1:1234567890:qwerty:prod:GET/user/address"
        ]
      }
    ]
  }
}

Dans un scénario standard, une demande autorisée avec succès exécuterait alors le lambda qui contient la logique api. Ce lambda de sauvegarde aurait un rôle d'exécution qui définit toutes les autorisations d'accès aux ressources protégées telles que les données dans les tables DynamoDB, etc. Ainsi, chaque fois qu'une ressource AWS différente est accessible par votre lambda de sauvegarde, vous n'avez pas à mettre à jour les un certain nombre de rôles de groupe, mais à la place, vous gérez ces autorisations à un seul endroit de votre système.

Avec cette configuration, vous pourriez avoir un autorisateur personnalisé assez simple pour chaque zone de votre API qui doit être protégée.

Par exemple, pour protéger vos points de /user/*terminaison, vous pouvez avoir un mécanisme d'autorisation personnalisé qui vérifie simplement si le jeton d'identification transmis à l'autorisation contient le usergroupe dans sa cognito:groupsrevendication. Si tel est le cas, vous renvoyez les autorisations requises pour exécuter la méthode API demandée ( execute-api:Invoke). Vous pouvez alors avoir un autre mécanisme d'autorisation personnalisé qui protège vos /admin/*routes en vérifiant si le admingroupe est dans la cognito:groupsréclamation du jeton d'identification, etc.

Mise en cache des réponses d'autorisation personnalisées

Chaque mécanisme d'autorisation personnalisé a un paramètre TTL facultatif, qui détermine la durée pendant laquelle sa réponse (pour un jeton JWT particulier) sera mise en cache. Cela permet de réduire le temps de préchauffage lambda ou le temps pris par des appels supplémentaires dans l'autorisateur.

Si un approbateur fait face à un certain nombre de méthodes, par exemple :

  • GET /utilisateur/adresse
  • POST /utilisateur/adresse
  • GET /utilisateur/mobile

alors il est important de noter qu'une réponse autorisée avec succès sera mise en cache pour toutes ces méthodes jusqu'à ce que la mise en cache expire. Par conséquent, la stratégie renvoyée par l'autorisateur personnalisé doit renvoyer soit une liste de ressources qui définissent toutes les méthodes que l'utilisateur peut exécuter, soit utiliser des caractères génériques.

Exemple de réponse pour un approbateur qui couvre toutes les /user/*routes

{
  "principalId": "sub-from-ID-token",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Allow",
        "Resource": [
          "arn:aws:execute-api:eu-west-1:1234567890:qwerty:prod:GET/user/address",
          "arn:aws:execute-api:eu-west-1:1234567890:qwerty:prod:POST/user/address",
          "arn:aws:execute-api:eu-west-1:1234567890:qwerty:prod:GET/user/mobile"
        ]
      }
    ]
  }
}

Exemple de réponse (utilisant des caractères génériques) pour un approbateur qui couvre toutes les /user/*routes

{
  "principalId": "sub-from-ID-token",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Allow",
        "Resource": [
          "arn:aws:execute-api:eu-west-1:1234567890:qwerty:prod:*/user/*"
        ]
      }
    ]
  }
}