AWS IAM SDK obtiene todos los documentos de políticas para un rol específico cuando se usan roles de grupo de Cognito

Aug 17 2020

Estoy utilizando grupos de grupos de usuarios de AWS Cognito para administrar permisos para una API de API Gateway. Creo que este es un uso válido para Grupos como lo dice la documentación aquí:https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html#using-groups-to-control-permission-with-amazon-api-gateway.

Desafortunadamente, la documentación es básicamente inexistente para este caso de uso por lo que puedo decir (aparte de ese pequeño párrafo). Estoy tratando de averiguar cómo funciona esto con una función personalizada Lambda del autorizador de API Gateway. Creé un rol de prueba y lo asigné a un grupo de prueba en Cognito. El rol tiene una sola política adjunta, pero en el futuro los roles tendrán varias políticas.

Ahora, en mi autorizador personalizado, ya estoy validando un token de acceso, etc., y todo funciona bien. Ahora estoy tratando de agregar este control de acceso detallado al usar los grupos/roles/políticas. Instalé el SDK de IAM y estuve investigando para ver qué llamadas debo hacer. Parece que no hay una manera fácil de conseguir un papel y todas sus políticas. Lo mejor que se me ha ocurrido es lo siguiente:

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);
}

Como puede ver, tengo que hacer un montón de llamadas solo para obtener las políticas para un rol.

  1. _cognitoClient.AdminListGroupsForUserAsyncpara obtener los grupos de usuarios de Cognito.
  2. _iamClient.ListAttachedRolePoliciesAsyncpara obtener las políticas asociadas con los roles.
  3. _iamClient.ListPolicyVersionsAsyncpara obtener las versiones de cada política.
  4. _iamClient.GetPolicyVersionAsyncpara obtener la versión de póliza individual que finalmente tiene el documento de póliza.

ListPolicyVersionsAsyncdevuelve una respuesta con una propiedad de documento pero siempre es nula por algún motivo, de ahí la necesidad de una GetPolicyVersionAsyncllamada adicional.

Todo esto no solo agrega latencia (que es un problema en una función de autorización donde cada llamada a la API se ejecutará a través de este código), sino que me deja solo con un montón de políticas individuales que necesito desduplicar de alguna manera. antes de volver a API Gateway.

¿Realmente no hay una manera más fácil/rápida de hacer esto? ¿Hay alguna forma de obtener toda esta información con menos llamadas y hay alguna forma de aplanar las políticas en caso de que sus reglas se superpongan?

Respuestas

lemming Aug 24 2020 at 17:00

Hay tantas formas diferentes de configurar la Autorización con Cognito y API Gateway que es difícil decir cuál será mejor para usted sin saber a qué recursos de AWS intenta acceder a través de sus métodos de API Gateway. Por ejemplo, ¿sus métodos de API Gateway se integran con métodos lambda que acceden a la información de la base de datos o están llamando a los servicios directamente?

Si entiendo correctamente, desea combinar los permisos para los (potencialmente) múltiples grupos a los que puede pertenecer un usuario para obtener un único conjunto de permisos para usar para esa solicitud en particular. Entonces, ¿el userrol puede permitir el acceso a los datos del usuario y el adminrol puede ampliar aún más el acceso a otros puntos finales de administración?

Suponiendo que tiene un escenario bastante estándar en el que sus métodos API Gateway se integran con un método lambda que luego accede a los recursos subyacentes de AWS, como una base de datos, entonces podría usar Autorizadores personalizados de la siguiente manera:

Simplifica mucho las cosas si los autorizadores personalizados solo devuelven los permisos necesarios para ejecutar el método API solicitado:

{
  "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"
        ]
      }
    ]
  }
}

En un escenario estándar, una solicitud autorizada correctamente ejecutaría la lambda que contiene la lógica de la API. Esta lambda de respaldo tendría una función de ejecución que define todos los permisos para acceder a recursos protegidos, como datos en tablas de DynamoDB, etc. Por lo tanto, cada vez que su lambda de respaldo acceda a un recurso de AWS diferente, no tiene que actualizar los permisos para una serie de roles de grupo, pero en su lugar administra esos permisos en una sola ubicación en su sistema.

Con esta configuración, podría tener un autorizador personalizado bastante simple para cada área de su API que necesite protección.

Por ejemplo, para proteger sus /user/*terminales, podría tener un autorizador personalizado que solo verifique si el token de ID que se pasa al autorizador contiene el usergrupo en su cognito:groupsreclamación. Si es así, devuelve los permisos necesarios para ejecutar el método API solicitado ( execute-api:Invoke). Luego, puede tener otro autorizador personalizado que proteja sus /admin/*rutas verificando si el admingrupo está en el cognito:groupsreclamo del token de ID, etc.

Almacenamiento en caché de respuestas personalizadas del autorizador

Cada autorizador personalizado tiene una configuración TTL opcional, que determina cuánto tiempo se almacenará en caché su respuesta (para un token JWT en particular). Esto ayuda a reducir el tiempo de calentamiento de lambda o el tiempo que tardan las llamadas adicionales en el autorizador.

Si un autorizador enfrenta una serie de métodos, por ejemplo:

  • OBTENER /usuario/dirección
  • POST /usuario/dirección
  • OBTENER /usuario/móvil

entonces es importante tener en cuenta que se almacenará en caché una respuesta autorizada con éxito para todos estos métodos hasta que se agote el tiempo de almacenamiento en caché. Por lo tanto, la política devuelta por el autorizador personalizado debe devolver una lista de recursos que definan todos los métodos que el usuario puede ejecutar o usar comodines.

Ejemplo de respuesta para un autorizador que cubre todas las /user/*rutas

{
  "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"
        ]
      }
    ]
  }
}

Respuesta de ejemplo (usando comodines) para un autorizador que cubre todas las /user/*rutas

{
  "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/*"
        ]
      }
    ]
  }
}