Como automatizar permissões personalizadas por usuário no Amazon QuickSight

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/)

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *