O problema de escalar controle de acesso no QuickSight
Conforme os ambientes do Amazon QuickSight crescem — especialmente com a expansão das funcionalidades de IA — gerenciar permissões personalizadas de forma manual se torna inviável. O princípio do menor privilégio exige que cada usuário tenha acesso apenas ao que precisa, e fazer isso na mão para centenas ou milhares de usuários é receita para erro.
O QuickSight trabalha com uma hierarquia de três camadas para aplicar permissões: conta, função (role) e usuário. As configurações de usuário sobrepõem as de função, que por sua vez sobrepõem as de conta. Isso permite políticas de segurança em camadas bastante flexíveis. A AWS documentou quatro cenários práticos para automatizar essa atribuição nos momentos críticos do ciclo de vida do usuário.
Cenário 1: Aplicar permissões no momento do cadastro via API
Para quem controla o processo de criação de usuários de ponta a ponta — como plataformas SaaS que incorporam o QuickSight para seus clientes — a abordagem mais simples é incluir o parâmetro --custom-permissions-name diretamente na chamada da API RegisterUser. O perfil de permissão é aplicado no ato da criação, sem necessidade de infraestrutura adicional.
aws quicksight register-user \
--aws-account-id 123456789012 \
--namespace default \
--identity-type QUICKSIGHT \
--user-role AUTHOR \
--email user@example.com \
--user-name user_name \
--custom-permissions-name "Restricted-Author-Profile"
Um exemplo prático: restringir automaticamente funcionalidades premium — como relatórios paginados e GenBI — com base no plano contratado pelo cliente, no momento em que o usuário é provisionado.
Cenário 2: Definir padrões no nível de conta ou função
Antes de construir automações mais elaboradas, a AWS recomenda configurar padrões que se aplicam automaticamente a todos os usuários, atuais e futuros, com esforço operacional mínimo. Há duas APIs nativas para isso, e a escolha depende do nível de granularidade desejado.
Padrão no nível de conta
A API UpdateAccountCustomPermission define um perfil de fallback aplicado a qualquer usuário sem perfil explícito atribuído — incluindo novos usuários criados via provisionamento Just-In-Time.
aws quicksight update-account-custom-permission \
--aws-account-id 123456789012 \
--custom-permissions-name "Restricted-User-Profile"
Padrão no nível de função
Já a API UpdateRoleCustomPermission permite definir um perfil padrão por função do QuickSight (READER, AUTHOR, ADMIN e funções PRO).
aws quicksight update-role-custom-permission \
--aws-account-id 123456789012 \
--role AUTHOR \
--namespace default \
--custom-permissions-name "Restricted-Author-Profile"
Um cenário real: uma empresa com 50.000 usuários que precisa bloquear todas as novas funcionalidades de GenBI enquanto a equipe de segurança faz uma revisão de 60 a 90 dias. O padrão no nível de conta aplica essa restrição instantaneamente, sem automação por usuário e sem janela de exposição.
Cenário 3: Automação orientada a eventos com EventBridge e Lambda
Quando a organização precisa de perfis de permissão diferentes por grupo, é necessária uma arquitetura mais sofisticada. O QuickSight não oferece uma API nativa para atribuir permissões diretamente a grupos, então a solução detecta quando um usuário é adicionado ou removido de um grupo e aplica o perfil correspondente automaticamente.
A AWS recomenda combinar este cenário com o Cenário 2. Quando um usuário é provisionado via federação Just-In-Time, existe uma janela entre a criação da conta e a adição ao grupo correto. O Cenário 2 fecha essa janela com um padrão restritivo; o Cenário 3 refina as permissões assim que o usuário é atribuído ao grupo. A configuração no nível de usuário do Cenário 3 tem precedência sobre o padrão do Cenário 2 — sem conflito, apenas camadas complementares.
Como a arquitetura funciona
O QuickSight e o IAM Identity Center (IDC) emitem eventos distintos no AWS CloudTrail para mudanças de membros em grupos. O QuickSight emite CreateGroupMembership ao adicionar e DeleteGroupMembership ao remover. O IDC emite AddMemberToGroup ao adicionar e RemoveMemberFromGroup ao remover. O Amazon EventBridge detecta esses eventos e aciona o AWS Lambda, que aplica o perfil correto via API UpdateUserCustomPermission.
Pré-requisitos
- Uma conta AWS com acesso administrativo
- Permissões do AWS IAM para criar e gerenciar recursos via AWS CloudFormation
- AWS CloudTrail habilitado na região de destino
- AWS IAM Identity Center: necessário apenas se usar gatilhos baseados em grupos do IDC
- Python 3.9+ e AWS CLI v2 (para o script de atualização em lote do Cenário 4)
Implantação com CloudFormation
É possível implantar o pipeline completo — função IAM, função Lambda e regra do EventBridge — usando o template CloudFormation fornecido pela AWS. O stack deve ser implantado na mesma região da assinatura do QuickSight, pois as regras do EventBridge capturam eventos apenas dentro da própria região. Os parâmetros do template incluem: UseIdentityCenter, TargetGroupName, PermissionProfileName, QuickSightNamespace e LambdaFunctionPrefixName.
Implantação manual: passo a passo
Passo 1: Criar a função IAM para o Lambda. No Console do IAM, crie uma política chamada QuickSight-Lambda-Policy com as permissões necessárias para logs, QuickSight e Identity Store (se usar IDC). Em seguida, crie a função QuickSight-Auto-Permissions-Role vinculada a essa política.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:YOUR_ACCOUNT_ID:log-group:/aws/lambda/Auto-Assign-QS-Permissions:*"
},
{
"Effect": "Allow",
"Action": [
"quicksight:UpdateUserCustomPermission",
"quicksight:DeleteUserCustomPermission",
"quicksight:UpdateUser",
"quicksight:DescribeUser"
],
"Resource": "arn:aws:quicksight:*:*:user/*"
},
{
"Effect": "Allow",
"Action": [
"identitystore:DescribeUser",
"identitystore:DescribeGroup"
],
"Resource": "*"
}
]
}
Passo 2: Implantar a função Lambda. Crie a função Auto-Assign-QS-Permissions com runtime Python 3.14 (ou a versão mais recente disponível), usando a role criada no passo anterior. Aumente o timeout para 30 segundos e configure as variáveis de ambiente: PERMISSION_PROFILE, TARGET_GROUP_NAME e NAMESPACE. Cole o código abaixo no editor:
import logging
import os
import time
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
quicksight = boto3.client("quicksight")
identity_store = boto3.client("identitystore")
def lambda_handler(event, context):
detail = event.get("detail", {})
event_source = detail.get("eventSource", "")
event_name = detail.get("eventName", "")
permission_profile = os.environ["PERMISSION_PROFILE"]
target_group = os.environ["TARGET_GROUP_NAME"]
namespace = os.environ["NAMESPACE"]
account_id = (
detail.get("userIdentity", {}).get("accountId")
or detail.get("recipientAccountId")
)
logger.info(
"Event received: source=%s, name=%s", event_source, event_name
)
try:
if event_source == "sso-directory.amazonaws.com":
return handle_idc_event(detail, account_id, namespace, permission_profile, target_group)
elif event_source == "quicksight.amazonaws.com":
return handle_quicksight_event(detail, account_id, namespace, permission_profile, target_group)
else:
logger.warning("Unknown event source: %s", event_source)
return {"statusCode": 200, "body": "Unknown event source"}
except Exception as e:
logger.error("Error processing event: %s", str(e))
return {"statusCode": 500, "body": str(e)}
def handle_idc_event(detail, account_id, namespace, permission_profile, target_group):
"""Handle AddMemberToGroup or RemoveMemberFromGroup from sso-directory.amazonaws.com."""
request_params = detail.get("requestParameters") or {}
identity_store_id = request_params.get("identityStoreId")
group_id = request_params.get("groupId")
member_id = (request_params.get("memberId") or (request_params.get("member") or {}).get("memberId"))
if not identity_store_id or not group_id or not member_id:
logger.warning("Missing identityStoreId, groupId, or memberId.")
return {"statusCode": 200, "body": "Missing IDC event info"}
group_resp = identity_store.describe_group(
IdentityStoreId=identity_store_id,
GroupId=group_id,
)
group_name = group_resp.get("DisplayName", "")
logger.info("IDC group resolved: '%s' (ID: %s)", group_name, group_id)
if group_name != target_group:
logger.info(
"Group '%s' does not match target '%s'. Skipping.",
group_name, target_group,
)
return {"statusCode": 200, "body": f"Group {group_name} not targeted"}
idc_user = identity_store.describe_user(
IdentityStoreId=identity_store_id,
UserId=member_id,
)
user_name = idc_user.get("UserName")
if not user_name:
logger.warning("No UserName found for IDC user %s", member_id)
return {"statusCode": 200, "body": "No UserName in IDC"}
logger.info("IDC user resolved: %s (ID: %s)", user_name, member_id)
qs_user = None
for attempt in range(5):
try:
qs_user = quicksight.describe_user(
AwsAccountId=account_id,
Namespace=namespace,
UserName=user_name,
)
break
except quicksight.exceptions.ResourceNotFoundException:
logger.info(
"User '%s' not yet in QuickSight (attempt %d/5). Waiting...",
user_name, attempt + 1,
)
time.sleep(3)
if qs_user is None:
logger.warning(
"User '%s' not found in QuickSight after retries.", user_name,
)
return {"statusCode": 200, "body": f"User {user_name} not in QuickSight"}
event_name = detail.get("eventName", "")
if event_name == "RemoveMemberFromGroup":
return remove_permissions(account_id, namespace, user_name)
else:
return apply_permissions(account_id, namespace, user_name, permission_profile)
def handle_quicksight_event(detail, account_id, namespace, permission_profile, target_group):
"""Handle CreateGroupMembership or DeleteGroupMembership from quicksight.amazonaws.com."""
request_params = detail.get("requestParameters") or {}
group_name = request_params.get("groupName", "")
member_name = request_params.get("memberName", "")
if not group_name or not member_name:
logger.warning("Missing groupName or memberName in event.")
return {"statusCode": 200, "body": "Missing event info"}
event_name = detail.get("eventName", "")
action = "removed from" if event_name == "DeleteGroupMembership" else "added to"
logger.info(
"QuickSight group membership: user '%s' %s '%s'",
member_name, action, group_name,
)
if group_name != target_group:
logger.info(
"Group '%s' does not match target '%s'. Skipping.",
group_name, target_group,
)
return {"statusCode": 200, "body": f"Group {group_name} not targeted"}
if event_name == "DeleteGroupMembership":
return remove_permissions(account_id, namespace, member_name)
else:
return apply_permissions(account_id, namespace, member_name, permission_profile)
def apply_permissions(account_id, namespace, user_name, permission_profile):
"""Apply the custom permission profile to a QuickSight user."""
quicksight.update_user_custom_permission(
AwsAccountId=account_id,
Namespace=namespace,
UserName=user_name,
CustomPermissionsName=permission_profile,
)
logger.info(
"Permission profile '%s' applied to user '%s'.",
permission_profile, user_name,
)
return {
"statusCode": 200,
"body": f"Permission applied for user: {user_name}",
}
def remove_permissions(account_id, namespace, user_name):
"""Remove custom permissions, reverting user to account/role-level default."""
try:
quicksight.delete_user_custom_permission(
AwsAccountId=account_id,
Namespace=namespace,
UserName=user_name,
)
logger.info("Permission removed for user '%s'.", user_name)
return {
"statusCode": 200,
"body": f"Permission removed for user: {user_name}",
}
except quicksight.exceptions.ResourceNotFoundException:
logger.info("User '%s' had no custom permission to remove.", user_name)
return {"statusCode": 200, "body": f"No permission to remove for: {user_name}"}
Passo 3: Criar a regra no Amazon EventBridge. No console do EventBridge, crie uma regra chamada QuickSight-Group-Membership-Creation-Rule. O padrão de evento varia conforme o tipo de autenticação da conta:
Opção A — IAM Identity Center:
{
"source": ["aws.sso-directory"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventSource": ["sso-directory.amazonaws.com"],
"eventName": ["AddMemberToGroup", "RemoveMemberFromGroup"],
"errorCode": [{ "exists": false }]
}
}
Opção B — Outros métodos de autenticação (grupos nativos do QuickSight):
{
"source": ["aws.quicksight"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventSource": ["quicksight.amazonaws.com"],
"eventName": ["CreateGroupMembership", "DeleteGroupMembership"],
"errorCode": [{ "exists": false }]
}
}
Passo 4: Adicionar o gatilho do EventBridge à função Lambda. Na função Lambda, acesse Configuração → Gatilhos → Adicionar gatilho, selecione Amazon EventBridge e escolha a regra criada no passo anterior.
Passo 5: Verificar a configuração. Adicione manualmente um usuário ao grupo alvo e monitore os logs do CloudWatch da função Lambda. A confirmação de sucesso aparece como: Permission profile '123' applied to user 'XYZ'.
Pontos de atenção no design
Quando um usuário é removido do grupo alvo, a função Lambda chama DeleteUserCustomPermission, revertendo o usuário ao padrão de conta ou função definido no Cenário 2. Se um usuário pertencer a múltiplos grupos com perfis diferentes, o QuickSight suporta apenas um perfil por usuário por vez — a lógica de resolução de conflito entre grupos deve ser definida pela equipe com base nas regras de prioridade da organização.
Cenário 4: Atualização em lote para usuários já existentes
A automação orientada a eventos do Cenário 3 só captura eventos futuros — ela não resolve o caso de grupos que já existem com usuários que precisam de restrições aplicadas retroativamente. Para isso, a solução é um script Python que itera por todos os membros de um grupo do QuickSight e aplica o perfil de permissão a cada um. O script lida com paginação via NextToken, já que a API ListGroupMemberships retorna no máximo 100 membros por chamada.
import boto3
import sys
import time
import csv
from botocore.exceptions import ClientError
# =========================================================
# CONFIGURATION - Edit these values before running
# =========================================================
# 1. Your AWS Account ID
AWS_ACCOUNT_ID = '279938032093'
# 2. The QuickSight Group whose members you want to target
TARGET_GROUP_NAME = 'BlogTestGroup'
# 3. The name of the Custom Permission Profile you want to apply
PERMISSION_PROFILE_NAME = 'TestBlog'
# 4. Namespace (almost always 'default')
NAMESPACE = 'default'
# =========================================================
qs = boto3.client('quicksight')
def get_all_group_members(group_name):
print(f"Fetching members from group: '{group_name}'...")
next_token = None
while True:
try:
api_args = {
'AwsAccountId': AWS_ACCOUNT_ID,
'Namespace': NAMESPACE,
'GroupName': group_name,
'MaxResults': 100
}
if next_token:
api_args['NextToken'] = next_token
response = qs.list_group_memberships(**api_args)
for member in response.get('GroupMemberList', []):
yield member.get('MemberName')
next_token = response.get('NextToken')
if not next_token:
break
except qs.exceptions.ResourceNotFoundException:
print(f"ERROR: The group '{group_name}' does not exist.")
sys.exit(1)
except ClientError as e:
print(f"CRITICAL API ERROR: {e}")
sys.exit(1)
def apply_permission(user_name):
try:
qs.update_user_custom_permission(
AwsAccountId=AWS_ACCOUNT_ID,
UserName=user_name,
Namespace=NAMESPACE,
CustomPermissionsName=PERMISSION_PROFILE_NAME
)
return True, "Success"
except qs.exceptions.ResourceNotFoundException as e:
error_msg = str(e)
if 'User' in error_msg or 'user' in error_msg:
return False, "User not found (might have been deleted)"
else:
return False, f"Profile '{PERMISSION_PROFILE_NAME}' not found"
except ClientError as e:
return False, str(e)
def main():
print("--------------------------------------------")
print(f"STARTING BATCH UPDATE")
print(f"Target Group: {TARGET_GROUP_NAME}")
print(f"Applying Profile: {PERMISSION_PROFILE_NAME}")
print("--------------------------------------------\n")
success_count = 0
fail_count = 0
failed_users = []
for user_name in get_all_group_members(TARGET_GROUP_NAME):
print(f"Processing: {user_name}...", end=" ")
status, message = apply_permission(user_name)
if status:
print("OK")
success_count += 1
else:
print(f"FAILED ({message})")
fail_count += 1
failed_users.append({'UserName': user_name, 'Error': message})
time.sleep(0.1)
print("\n--------------------------------------------")
print("JOB COMPLETED")
print(f"Total Success: {success_count}")
print(f"Total Failed: {fail_count}")
print("--------------------------------------------")
if failed_users:
csv_filename = f"failed_users_{TARGET_GROUP_NAME}.csv"
with open(csv_filename, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['UserName', 'Error'])
writer.writeheader()
writer.writerows(failed_users)
print(f"\nFailed users written to: {csv_filename}")
if __name__ == "__main__":
main()
O script pode ser executado localmente ou diretamente no AWS CloudShell, que já vem pré-autenticado e com o Boto3 instalado. O intervalo padrão de 0,1 segundo entre chamadas (~10 requisições/segundo) é adequado para a maioria dos ambientes. Para grupos com mais de 10.000 membros, recomenda-se aumentar esse valor para evitar throttling.
Limpeza dos recursos
Se a implantação foi feita via CloudFormation, basta excluir o stack para remover todos os recursos criados. Na implantação manual, exclua: a regra do EventBridge (QuickSight-Group-Membership-Creation-Rule), a função Lambda (Auto-Assign-QS-Permissions), a função IAM (QuickSight-Auto-Permissions-Role) e a política IAM (QuickSight-Lambda-Policy). O script do Cenário 4 não implanta infraestrutura e não requer limpeza.
Qual cenário usar em cada situação
Os quatro cenários cobrem contextos distintos e, em alguns casos, se complementam:
- Cenário 1 — Cadastro via API: ideal para portais de onboarding personalizados que controlam a criação de usuários de ponta a ponta
- Cenário 2 — Padrões de conta ou função: restrições consistentes para todos os usuários ou por função, sem automação adicional — ponto de partida recomendado
- Cenário 3 — Eventos com EventBridge e Lambda: perfis distintos por grupo, com suporte a grupos nativos do QuickSight e grupos do IDC
- Cenário 4 — Atualização em lote: para usuários já existentes provisionados antes da automação entrar em vigor
Vale reforçar: os Cenários 2 e 3 são complementares, não alternativos. O Cenário 2 estabelece a linha de base restritiva e o Cenário 3 refina as permissões conforme os usuários são atribuídos a grupos. Para quem está começando, a recomendação é partir pelo Cenário 2. Para mais detalhes, consulte a documentação de permissões personalizadas do Amazon QuickSight.
Fonte
Automate user-level custom permissions for Amazon Quick (https://aws.amazon.com/blogs/machine-learning/automate-user-level-custom-permissions-for-amazon-quick/)