Como Aceder a Objetos S3 Privados com AWS Cognito

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.

É 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.
- Como Criar um Bucket S3 Privado?
- Como Criar uma Política Personalizada para Permissão de Acesso a Objetos no Bucket S3 Privado?
- Como Criar uma Função Lambda para Aceder a Objetos no Bucket S3 Privado?
- Como Criar um API Gateway para Usar a Função Lambda?
- Como Criar um Bucket S3 Público para Usar como Pasta Web?
- Como Criar um Pool de Utilizadores Cognito e Configurar as Definições?
- 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.

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.

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
GetObjectdo seu bucket S3 privado como mostrado abaixo:

Crie uma política personalizada como mostrado abaixo. Selecione S3 como serviço e apenas
GetObject como ação (action):
Selecione "specific" como recurso (resource) e especifique o seu bucket S3 privado para que a política tenha as capacidades desejadas:

Dê um nome à sua política e crie-a. Pode dar qualquer nome, mas precisará de o lembrar.

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:

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.

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.

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:

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.
É 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.

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:
O recurso (resource) criado aqui será usado mais tarde no URL do API.

Crie o método
GET para o recurso que criou:
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.

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:
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.

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.

Depois do deploy, o URL será como mostrado abaixo. Agora é possível usar este link a partir de qualquer aplicação.

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.

Depois de o Cognito-based Authorizer ser definido, pode ser usado como mostrado abaixo:

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"}
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.

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.
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.pdfCampos:
client_id=7uuggclp7269oguth08mi2ee04response_type=tokenscope=openid+profile+emailredirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.htmlstate=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:

As definições da App client podem ser confirmadas como mostrado abaixo:

Um nome de domínio (domain name) deve ser definido para que possa ser usado como URL para o Hosted UI.

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çoContacte-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