Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ CI (GitHub Actions): mesmo comando em Python 3.12 em todo PR/`push` na `main`

## CDK / Deploy

Stack único em `cdk/app.py` (ADR-001 PR2): **DynamoDB + Lambda + HTTP API** no mesmo stack — sem `from_function_name`, sem Cognito (PR3).
Stack único em `cdk/app.py` (ADR-001): **Cognito + DynamoDB + Lambda + HTTP API** no mesmo stack — sem `from_function_name`.

- Tabela: PK `cliente_id` / SK `request_id`, on-demand; `grant_read_write_data` na Lambda.
- Env: `TABLE_NAME` (nome da tabela), `PIX_FEE` (default `50.00`).
- Handler: `app.lambda_handler` (código em `lambda/`).
- Throttle HTTP API: **100 rps / burst 200** (espelha `aws-api-gateway-cdk`).
- Throttle HTTP API: **100 rps / burst 200**.
- Auth: Cognito JWT nas rotas mutáveis/status; **`GET /health` público**.
- CORS lab: `http://localhost:3000` e `http://127.0.0.1:3000` (sem `*`). Override via CDK context `corsOrigins` (lista de origins).
- Rotas: `POST /solicitar-exclusao-cliente`, `GET /status-exclusao/{cliente_id}`, `POST /confirmar-pagamento`, `GET /health`.

```bash
Expand All @@ -66,4 +68,57 @@ npx aws-cdk@2 synth # requer Node + AWS CDK CLI
# npx aws-cdk@2 deploy # só com CAB / conta lab
```

Outputs úteis após deploy: `ApiUrl`, `UserPoolId`, `UserPoolClientId`, `JwtIssuer`.

**Nota:** o asset Lambda é o source em `lambda/` — empacotar Powertools (layer ou bundle) fica em follow-up; este PR só faz o wiring IaC.

---

## Cognito JWT — obter token e testar (lab)

Após `cdk deploy` (com CAB), anote `UserPoolId`, `UserPoolClientId` e `ApiUrl`.

### 1) Criar usuário (lab)

```bash
aws cognito-idp sign-up \
--client-id "$CLIENT_ID" \
--username "lab@example.com" \
--password 'LabPass123' \
--user-attributes Name=email,Value=lab@example.com

# Confirmar (lab — admin; self-sign-up exige confirmação de e-mail ou admin-confirm)
aws cognito-idp admin-confirm-sign-up \
--user-pool-id "$USER_POOL_ID" \
--username "lab@example.com"
```

### 2) Obter IdToken

```bash
aws cognito-idp initiate-auth \
--client-id "$CLIENT_ID" \
--auth-flow USER_PASSWORD_AUTH \
--auth-parameters USERNAME=lab@example.com,PASSWORD='LabPass123'
```

Use o campo `AuthenticationResult.IdToken` (não o AccessToken) no header.

### 3) Chamar API

```bash
# Público
curl -s "$API_URL/health"

# Protegido
PAYLOAD='{"cliente_id":"c1","motivo":"lab"}'
curl -s -X POST "$API_URL/solicitar-exclusao-cliente" \
-H "Authorization: Bearer $ID_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"

curl -s "$API_URL/status-exclusao/c1" \
-H "Authorization: Bearer $ID_TOKEN"
```

Sem Bearer nas rotas protegidas → API Gateway responde **401** antes da Lambda.
51 changes: 41 additions & 10 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
# Segurança

## Medidas Implementadas
- Idempotência
- Validação de input
- Throttling
- Configuração via env vars

## Melhorias Planejadas
- Authorizer
- WAF
- Segurança no QR Code Pix real
## O que este stack CDK cria (honesto)

Recursos em `cdk/app.py` hoje:

| Controle | Status |
|----------|--------|
| Cognito User Pool + App Client (lab) | **Sim** — email sign-in; `USER_PASSWORD` + `USER_SRP`; sem client secret |
| HTTP API JWT authorizer (Cognito) | **Sim** — nas rotas protegidas |
| `/health` público | **Sim** — sem authorizer (probes / smoke lab) |
| Throttle HTTP API | **Sim** — 100 rps / burst 200 |
| CORS | **Restrito a localhost** (`http://localhost:3000`, `http://127.0.0.1:3000`); override via CDK context `corsOrigins` |
| DynamoDB on-demand + IAM grant mínimo na Lambda | **Sim** |
| Validação / idempotência no handler | **Sim** (código Lambda) |
| WAF | **Não** — fora do CDK; não prometer |
| ACM / domínio custom / mTLS | **Não** — fora do CDK |
| Secrets no repositório | **Não** — User Pool / Client IDs só via CloudFormation outputs |

### Rotas

| Rota | Auth |
|------|------|
| `POST /solicitar-exclusao-cliente` | Cognito JWT |
| `GET /status-exclusao/{cliente_id}` | Cognito JWT |
| `POST /confirmar-pagamento` | Cognito JWT |
| `GET /health` | Público |

Enviar `Authorization: Bearer <IdToken>` nas rotas protegidas.

## Limitações de lab (aceitáveis até próximo ADR)

- User Pool com self sign-up e remoção `DESTROY` — adequado só a conta lab.
- Throttle ≠ proteção de abuso (sem WAF / rate por cliente).
- Pix continua stub; QR não é canal autenticado.
- CORS sem `*`, mas origins de lab são localhost; frontends em outro host precisam de `corsOrigins` no deploy.

## Melhorias planejadas (não implementadas neste PR)

- WAF / AWS Shield
- ACM + custom domain
- Alarmes / X-Ray
- Restringir self sign-up / MFA em ambientes não-lab
71 changes: 66 additions & 5 deletions cdk/app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env python3
"""CDK app — exclusão Pix (ADR-001 PR2).
"""CDK app — exclusão Pix (ADR-001 PR3).

Single stack: DynamoDB table + Lambda + HTTP API.
No Cognito (PR3). No from_function_name.
Single stack: Cognito User Pool + DynamoDB + Lambda + HTTP API (JWT).
Health is public; mutable/status routes require Cognito JWT.
No WAF/ACM in this stack.
"""
from aws_cdk import (
App,
Expand All @@ -11,7 +12,9 @@
RemovalPolicy,
Stack,
aws_apigatewayv2 as apigwv2,
aws_apigatewayv2_authorizers as apigwv2_authorizers,
aws_apigatewayv2_integrations as apigwv2_integrations,
aws_cognito as cognito,
aws_dynamodb as dynamodb,
aws_lambda as _lambda,
)
Expand All @@ -35,6 +38,42 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
removal_policy=RemovalPolicy.DESTROY,
)

# Lab Cognito: email sign-in, USER_PASSWORD + USER_SRP (no client secret).
user_pool = cognito.UserPool(
self,
"ExclusaoClienteUserPool",
self_sign_up_enabled=True,
sign_in_aliases=cognito.SignInAliases(email=True),
auto_verify=cognito.AutoVerifiedAttrs(email=True),
password_policy=cognito.PasswordPolicy(
min_length=8,
require_lowercase=True,
require_uppercase=True,
require_digits=True,
require_symbols=False,
),
removal_policy=RemovalPolicy.DESTROY,
)

user_pool_client = user_pool.add_client(
"ExclusaoClienteAppClient",
auth_flows=cognito.AuthFlow(
user_password=True,
user_srp=True,
),
generate_secret=False,
)

jwt_issuer = (
f"https://cognito-idp.{self.region}.amazonaws.com/"
f"{user_pool.user_pool_id}"
)
jwt_authorizer = apigwv2_authorizers.HttpJwtAuthorizer(
"CognitoJwtAuthorizer",
jwt_issuer,
jwt_audience=[user_pool_client.user_pool_client_id],
)

# Handler: lambda/app.py → lambda_handler (CI uses Python 3.12)
# Powertools packaging/layer left for follow-up (asset is source only).
fn = _lambda.Function(
Expand All @@ -53,13 +92,25 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:

table.grant_read_write_data(fn)

# Lab CORS: no wildcard. Browser callers must use localhost (or override
# via CDK context key corsOrigins = ["https://app.example"]).
cors_origins = self.node.try_get_context("corsOrigins") or [
"http://localhost:3000",
"http://127.0.0.1:3000",
]

http_api = apigwv2.HttpApi(
self,
"ExclusaoClienteHttpApi",
description="API Gateway para Exclusão de Cliente com Pix",
cors_preflight=apigwv2.CorsPreflightOptions(
allow_origins=["*"],
allow_methods=[apigwv2.CorsHttpMethod.ANY],
allow_origins=cors_origins,
allow_methods=[
apigwv2.CorsHttpMethod.GET,
apigwv2.CorsHttpMethod.POST,
apigwv2.CorsHttpMethod.OPTIONS,
],
allow_headers=["Authorization", "Content-Type"],
),
create_default_stage=False,
)
Expand All @@ -78,21 +129,26 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
"LambdaIntegration", fn
)

# Protected (Cognito JWT): mutáveis + status
http_api.add_routes(
path="/solicitar-exclusao-cliente",
methods=[apigwv2.HttpMethod.POST],
integration=integration,
authorizer=jwt_authorizer,
)
http_api.add_routes(
path="/status-exclusao/{cliente_id}",
methods=[apigwv2.HttpMethod.GET],
integration=integration,
authorizer=jwt_authorizer,
)
http_api.add_routes(
path="/confirmar-pagamento",
methods=[apigwv2.HttpMethod.POST],
integration=integration,
authorizer=jwt_authorizer,
)
# Public: health (probes / lab smoke)
http_api.add_routes(
path="/health",
methods=[apigwv2.HttpMethod.GET],
Expand All @@ -103,6 +159,11 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
CfnOutput(self, "ApiId", value=http_api.api_id)
CfnOutput(self, "DynamoTableName", value=table.table_name)
CfnOutput(self, "LambdaName", value=fn.function_name)
CfnOutput(self, "UserPoolId", value=user_pool.user_pool_id)
CfnOutput(
self, "UserPoolClientId", value=user_pool_client.user_pool_client_id
)
CfnOutput(self, "JwtIssuer", value=jwt_issuer)


app = App()
Expand Down
Loading