Computação em Nuvem
14 min de leitura

Como Aceder a Objetos S3 Privados com AWS Cognito

Guia para proporcionar acesso seguro a ficheiros privados com integração de pools de utilizadores Cognito, API Gateway, Lambda e S3.

N
Necmettin Demir
21 de julho de 2023
Carregando...

Como Aceder a Objetos S3 Privados com AWS Cognito

AWS Cognito S3
AWS Cognito S3

Cenário

Suponhamos que está a desenvolver algumas aplicações para o seu cliente. No entanto, existem alguns ficheiros como PDF, Word, Excel, etc., relacionados com os registos nas aplicações. Para simplificar o cenário, vamos assumir que estes ficheiros são armazenados num único bucket S3 privado (private) na AWS.
Os utilizadores precisam de poder aceder a estes ficheiros relevantes a partir do bucket S3 privado através de um link URL nas aplicações. A nossa solução precisa de funcionar como uma solução portável (portable) para qualquer software interno da empresa.

Introdução

O objetivo deste artigo é demonstrar como descarregar ficheiros de um bucket S3 privado utilizando pools de utilizadores Cognito (user pools). Além do Cognito, é demonstrado o fluxo do Cognito para o API Gateway com Authorizer e a colaboração do API Gateway com Lambda.
Foram partilhadas o máximo de capturas de ecrã possível para cada passo a partir da consola AWS. Especialmente para iniciantes, foram adicionadas muitas imagens para tornar os passos mais claros.

Contexto

Algumas leituras prévias podem ser úteis para melhor compreender o que é desenvolvido neste artigo. Especialmente para iniciantes na AWS, os seguintes links serão úteis:

O Que Deve Ser Feito?

Podem ser codificados muitos fluxos ou métodos para tal tarefa. Aqui, vamos implementar o método mostrado abaixo. Uma breve explicação de como o cenário será implementado é apresentada na imagem seguinte.
A imagem seguinte mostra que precisamos de criar alguns elementos como Pool de Utilizadores Cognito, buckets S3, Métodos API Gateway, Funções Lambda, etc. Depois de criar todas as entidades no ambiente AWS, precisamos de as configurar adequadamente para que todas possam trabalhar em conjunto.
Arquitetura do Sistema
Arquitetura do Sistema
É melhor criar todos os elementos no ambiente AWS por ordem inversa. Por exemplo, para usar Lambda com um método API, se a função Lambda for desenvolvida primeiro, a função pode ser facilmente ligada quando o método API Gateway for criado. Da mesma forma, no Passo 5 devemos criar o bucket S3 web e colocar dentro o ficheiro callback.html, para que no Passo 6 ao criar o Pool de Utilizadores Cognito possamos usar este ficheiro. Naturalmente isto não é obrigatório, mas esta ordem facilitará o desenvolvimento. Por isso, esta abordagem foi preferida aqui.

Esboço

Vamos procurar respostas para as seguintes perguntas. Note que precisa de ter uma conta AWS para implementar todos os passos neste artigo.
  1. Como Criar um Bucket S3 Privado?
  2. Como Criar uma Política Personalizada para Permissão de Acesso a Objetos no Bucket S3 Privado?
  3. Como Criar uma Função Lambda para Aceder a Objetos no Bucket S3 Privado?
  4. Como Criar um API Gateway para Usar a Função Lambda?
  5. Como Criar um Bucket S3 Público para Usar como Pasta Web?
  6. Como Criar um Pool de Utilizadores Cognito e Configurar as Definições?
  7. Como Testar o Cenário?

1. Como Criar um Bucket S3 Privado?

O S3 é um dos serviços baseados em região (region-based) na AWS. Os itens nos buckets S3 são chamados de objetos (object). Por isso, na AWS, os termos objeto e ficheiro para buckets S3 podem ser usados alternadamente.
Mantenha a caixa de seleção "Bloquear Todo o Acesso Público" (Block All Public Access) marcada. Aqui é criado um bucket S3 privado. Embora existam muitas opções de configuração extra, estamos a criar com valores padrão por simplicidade da solução.
Criar Bucket S3
Criar Bucket S3
Carregue alguns objetos para o bucket S3 para testar o acesso privado. Depois, tente aceder a estes objetos com utilizadores não autorizados ou possíveis links de acesso. Embora conheçamos ficheiros como PDF, DOC, XLS, etc., na terminologia AWS S3 todos são chamados de objetos.
Carregar Ficheiros
Carregar Ficheiros

2. Criar Política para Permissão de Acesso a Objetos no Bucket S3 Privado

Na AWS, o IAM (Gestão de Identidade e Acesso) é a base de todos os serviços! Utilizadores, Grupos, Funções e Políticas são conceitos fundamentais com os quais precisamos de nos familiarizar.
Existem muitas funções incorporadas (built-in), e cada função tem muitas políticas incorporadas que significam permissões. Estas são chamadas de "AWS Managed". No entanto, também é possível criar funções e políticas "Customer Managed" (Geridas pelo Cliente). Assim, aqui é criada uma política personalizada.
  • Crie uma política IAM personalizada para obter objetos do seu bucket S3 privado.
  • Encontre a lista de políticas existentes na AWS e crie uma nova apenas para executar a operação GetObject do seu bucket S3 privado como mostrado abaixo:
Lista de Políticas
Lista de Políticas
Crie uma política personalizada como mostrado abaixo. Selecione S3 como serviço e apenas GetObject como ação (action):
Definições da Política 1
Definições da Política 1
Selecione "specific" como recurso (resource) e especifique o seu bucket S3 privado para que a política tenha as capacidades desejadas:
Definições da Política 2
Definições da Política 2
Dê um nome à sua política e crie-a. Pode dar qualquer nome, mas precisará de o lembrar.
Definições da Política 3
Definições da Política 3
O resumo da sua política personalizada será como mostrado abaixo. Também é possível criar a política usando diretamente este conteúdo JSON:
JSON da Política
JSON da Política
Definição JSON da Política:
JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::private-s3-for-interfacing/*"
        }
    ]
}

3. Criar Função Lambda para Aceder a Objetos no Bucket S3 Privado

Aqui é usada a versão mais recente do NodeJS para a função Lambda. Crie uma função Lambda e selecione NodeJS. É possível selecionar qualquer linguagem suportada como Python, Go, Java, .NET Core, etc. para a função Lambda.
Criar Lambda
Criar Lambda
Quando cria a função Lambda, é mostrado um código de exemplo "hello". Em vez disso, precisamos de desenvolver o nosso próprio código.
Como pode ver, o ambiente de desenvolvimento Lambda assemelha-se a um IDE leve baseado em web.
Alterar Código Lambda
Alterar Código Lambda
Substitua o código existente pelo código de exemplo curto fornecido. A nova versão do código será como mostrado abaixo. Depois de alterar o código, clique no botão "Deploy" para usar a função Lambda.
Por simplicidade do cenário, o nome do bucket é usado estaticamente. O nome do ficheiro é enviado como parâmetro com o nome fn. Embora o tipo de conteúdo (content type) padrão seja assumido como pdf, pode ser qualquer tipo de ficheiro implementado no código da função Lambda. Como preferiremos usar a funcionalidade proxy da função Lambda na ligação API Gateway, o cabeçalho de resposta (response header) contém alguns dados adicionais necessários.
Código Lambda NodeJS (retornar como Blob):
JavaScript
// O código da função Lambda terá este aspeto
// Este código retornará a resposta como conteúdo blob
// Para descarregar o ficheiro, pode usar Callback-to-Download-Blob.html nos anexos

const AWS = require('aws-sdk');
const S3= new AWS.S3();
exports.handler = async (event, context) => {
    
  let fileName;
  let bucketName;
  let contentType;
  let fileExt;
    
  try {
    bucketName = 'private-s3-for-interfacing';
    fileName = event["queryStringParameters"]['fn']
    contentType = 'application/pdf';
    fileExt = 'pdf';
    
    //------------
    fileExt = fileName.split('.').pop();
    
    switch (fileExt) {
        case 'pdf': contentType = 'application/pdf'; break;        
        case 'png': contentType = 'image/png'; break;
        case 'gif': contentType = 'image/gif'; break;
        case 'jpeg': case 'jpg': contentType = 'image/jpeg'; break;
        case 'svg': contentType = 'image/svg+xml'; break;
        case 'docx': contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; break;
        case 'xlsx': contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; break;
        case 'pptx': contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; break;
        case 'doc': contentType = 'application/msword'; break;
        case 'xls': contentType = 'application/vnd.ms-excel'; break;
        case 'csv': contentType = 'text/csv'; break;
        case 'ppt': contentType = 'application/vnd.ms-powerpoint'; break;
        case 'rtf': contentType = 'application/rtf'; break;
        case 'zip': contentType = 'application/zip'; break;
        case 'rar': contentType = 'application/vnd.rar'; break;
        case '7z': contentType = 'application/x-7z-compressed'; break;
        default: ;
    }
    
    //------------
    const data = await S3.getObject({Bucket: bucketName, Key: fileName}).promise();
    
    return {
       headers: {
          'Content-Type': contentType,
          'Content-Disposition': 'attachment; filename=' + fileName, // A chave do sucesso
          'Content-Encoding': 'base64',
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 
          'Access-Control-Allow-Methods': 'GET,OPTIONS'
      },
      body: data.Body.toString('base64'),
      isBase64Encoded: true,
      statusCode: 200
    }
  } catch (err) {
    return {
      statusCode: err.statusCode || 400,
      body: err.message || JSON.stringify(err.message) + ' - fileName: '+ fileName + ' - bucketName: ' + bucketName
    }
  }
}
Também é possível usar código Python na função Lambda como mostrado abaixo:
Python
# O código seguinte pode ser desenvolvido como o exemplo NodeJS acima
    
import base64
import boto3
import json
import random

s3 = boto3.client('s3')

def lambda_handler(event, context):
    try:
        fileName = event['queryStringParameters']['fn']
        bucketName = 'private-s3-for-interfacing'        
        contentType = 'application/pdf'
        
        response = s3.get_object(
            Bucket=bucketName,
            Key=fileName,
        )
        
        file = response['Body'].read()
        
        return {
            'statusCode': 200,
            'headers': {  
                         'Content-Type': contentType,                            
                         'Content-Disposition': 'attachment; filename='+ fileName,
                         'Content-Encoding': 'base64'
                         # Códigos relacionados com CORS podem ser adicionados aqui se necessário
                        },
            'body': base64.b64encode(file).decode('utf-8'),           
            'isBase64Encoded': True
        }
    except:
        return {
            'headers': { 'Content-type': 'text/html' },
            'statusCode': 200,
            'body': 'Error occurred in Lambda!' 
        }
Outro método pode ser criar um presigned URL com Lambda:
JavaScript
// Este método fornecerá um presigned url
// Para usar o link presigned URL, pode usar o ficheiro Callback-for-preSignedUrl.html

var AWS = require('aws-sdk');
var S3 = new AWS.S3({
  signatureVersion: 'v4',
});

exports.handler = async (event, context) => {
    
  let fileName;
  let bucketName;
  let contentType;
    
  bucketName = 'private-s3-for-interfacing';
  fileName = event["queryStringParameters"]['fn'];
  contentType = 'application/json';
    
  const presignedUrl = S3.getSignedUrl('getObject', {
    Bucket: bucketName,
    Key: fileName,
    Expires: 300 // segundos
  });

  let responseBody = {'presignedUrl': presignedUrl};
  
  return {
       headers: {
          'Content-Type': contentType,
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 
          'Access-Control-Allow-Methods': 'GET,OPTIONS'
      },
      body: JSON.stringify(responseBody), 
      statusCode: 200
    }    
};
Quando a função Lambda é criada, é criada uma função (role) juntamente com ela. No entanto, esta função não tem permissão para aceder aos objetos no seu bucket S3 privado. Agora, precisamos de adicionar a política "Customer Managed" que criámos nos passos anteriores a esta função criada com a função Lambda.
Depois de criar a função Lambda, podemos encontrar a função criada automaticamente como mostrado abaixo:
Encontrar Função Lambda
Encontrar Função Lambda
Adicione a política personalizada que criou no passo anterior a esta função; assim a função Lambda terá direito de acesso GetObject restrito ao seu bucket S3.
Adicionar Política
Adicionar Política
É tudo o que precisa de ser feito para o Lambda aceder ao seu bucket S3. Agora, é hora de criar um método AWS Gateway para usar a nossa função Lambda.

4. Criar API Gateway para Usar a Função Lambda

Crie um AWS Gateway REST API como mostrado abaixo. Como pode ver, existem muitas opções, mas estamos a criar um API "REST" como "New API". Dê um nome ao seu API Gateway.
Criar REST API
Criar REST API
Existem alguns passos para criar e executar o AWS GW API:
  • Criar API
  • Criar Resource
  • Criar Method
  • Deploy API
Crie um Resource para o seu REST API como mostrado abaixo:
Criar Resource Passo 1
Criar Resource Passo 1
O recurso (resource) criado aqui será usado mais tarde no URL do API.
Criar Resource Passo 2
Criar Resource Passo 2
Crie o método GET para o recurso que criou:
Criar Método GET
Criar Método GET
Aqui pode ser criado qualquer método HTTP como GET, POST, PUT, DELETE, etc. Para a nossa necessidade, estamos apenas a criar GET. Não se esqueça de ligar a função Lambda que criámos nos passos anteriores a este método.
A Lambda Proxy Integration está marcada aqui. Esta abordagem permite-nos processar todo o conteúdo relacionado com a resposta na Função Lambda.
Integração Lambda Proxy
Integração Lambda Proxy
Depois de o método GET ser criado, o fluxo entre o Método API Gateway e a função Lambda será como mostrado abaixo:
Vista do Fluxo
Vista do Fluxo
Ative o CORS para o Gateway API como mostrado abaixo. As opções Default 4xx e Default 5xx podem ser marcadas; assim mesmo os erros podem retornar sem problemas.
Ativar CORS
Ativar CORS
Depois de criar e configurar tudo relacionado com o método AWS Gateway, agora é hora de fazer o deploy (deploy) do API. O API é deployed para um stage como mostrado. Este nome de stage também será usado no URL público do API.
Deploy do API
Deploy do API
Depois do deploy, o URL será como mostrado abaixo. Agora é possível usar este link a partir de qualquer aplicação.
URL do Deploy
URL do Deploy
Para restringir o acesso ao API gateway, devemos definir um Authorizer (Autorizador). Podemos definir um Cognito Authorizer como mostrado abaixo.
Como pode ver na imagem abaixo, Authorization é o token JWT que deve ser adicionado ao header da requisição para usar o método API autorizado.
Quando o Cognito Hosted UI é enviado com um utilizador/password Cognito, o Cognito redirecionará o utilizador para o URL de callback transferindo o id_token e dados state adicionais.
Veja que o token que precisamos de adicionar ao header é chamado "Authorization" em Token Source.
Definir Cognito Authorizer
Definir Cognito Authorizer
Depois de o Cognito-based Authorizer ser definido, pode ser usado como mostrado abaixo:
Uso do Authorizer
Uso do Authorizer
Por outro lado, se não quiser definir Authorizer para o API Gateway, pode restringir o acesso ao URL do API com "Resource Policy" (Política de Recurso) como mostrado abaixo.
Se a Resource Policy for alterada/adicionada, o API deve ser re-deployed. O IP mostrado como xxx.xxx.xxx.xxx pode ser o IP do servidor. Quando alguém tentar aceder ao URL a partir de um IP diferente, a seguinte mensagem será mostrada:
{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:eu-west-2:********8165:... with an explicit deny"}
Definição Resource Policy
Definição Resource Policy
O código JSON da Resource Policy será assim:
JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "*"
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "*",
            "Condition": {
                "NotIpAddress": {
                    "aws:SourceIp": "xxx.xxx.xxx.xxx"
                }
            }
        }
    ]
}

5. Bucket S3 Público para Usar como Pasta Web

Precisamos de dois buckets S3 para a solução. O primeiro foi criado nas secções anteriores. O segundo está agora a ser criado e será usado como pasta web. O primeiro foi usado como bucket privado para armazenar todos os ficheiros.
Estrutura de Dois Buckets S3
Estrutura de Dois Buckets S3
Crie um bucket S3 público como pasta web. Este bucket contém um ficheiro callback.html, para que possa ser usado como endereço de callback do Cognito.
Criar Bucket Web
Criar Bucket Web
O bucket S3 para web deve ser público (public). Por isso, a seguinte política pode ser aplicada:
JSON
// O JSON da política terá este aspeto

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::web-s3-for-interfacing/*"
        }
    ]
}

Descarregar Ficheiros Fonte

Pode descarregar Callback.html e outros ficheiros fonte a partir dos links abaixo:

6. Criar e Configurar Pool de Utilizadores Cognito

  • Endereço callback: https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html
  • OAuth 2.0 Flows: marque a opção "implicit grant".
  • OAuth 2.0 Scopes: email, openid, profile.
Examine o link hosted UI abaixo. Para enviar parâmetros para a página de login Cognito hosted, adicione um parâmetro URL extra "state". O parâmetro "state" será passado para o ficheiro Callback.html.
O link Cognito Hosted UI contém muitos parâmetros URL como mostrado abaixo:
https://test-for-user-pool-for-s3.auth.eu-west-2.amazoncognito.com/login?client_id=7uuggclp7269oguth08mi2ee04&response_type=token&scope=openid+profile+email&redirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html&state=fn=testFile.pdf
Campos:
  • client_id=7uuggclp7269oguth08mi2ee04
  • response_type=token
  • scope=openid+profile+email
  • redirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html
  • state=fn=testFile.pdf
state é um parâmetro URL especial. Pode ser enviado para a página Hosted UI e retornado para a página Callback.html.
Uma app client deve ser criada como mostrado abaixo:
Criar App Client
Criar App Client
As definições da App client podem ser confirmadas como mostrado abaixo:
Definições App Client
Definições App Client
Um nome de domínio (domain name) deve ser definido para que possa ser usado como URL para o Hosted UI.
Definição de Domínio
Definição de Domínio

7. Como Testar o Cenário?

Vamos ver como testar o API que permite acesso restrito usando o Pool de Utilizadores Cognito.
Qualquer utilizador final pode clicar num link para iniciar este processo. Suponhamos que temos uma página web que contém o seguinte conteúdo HTML. Como pode ver, para cada ficheiro, o link é o URL do Cognito hosted UI.
O ficheiro LinkToS3Files.html pode ser usado para testar o cenário.

Descarregar Ficheiros de Teste


Conclusão

Espero que este artigo tenha sido útil para iniciantes no ambiente de nuvem AWS.

Serviços de Computação em Nuvem

Oferecemos serviços de design de infraestrutura, migração, gestão e otimização nas plataformas AWS, Azure e Google Cloud.

Explorar o Nosso Serviço

Contacte-nos

Entre em contacto com a nossa equipa para obter informações detalhadas sobre as nossas soluções AWS e computação em nuvem.

Contacto