diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 57afb5e..d18d00a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,84 +9,8 @@ env: AWS_REGION: us-east-1 jobs: - - # ========================================================= - # TESTS (JUnit + JaCoCo) - # ========================================================= - test: - name: Test Application - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - - - name: Cache Maven - uses: actions/cache@v4 - with: - path: ~/.m2 - key: maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - maven- - - - name: Run tests with coverage - run: | - cd app - mvn clean verify - - # ========================================================= - # SONARCLOUD - # ========================================================= - sonarqube: - name: SonarCloud - runs-on: ubuntu-latest - needs: test - if: github.ref == 'refs/heads/main' - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: zulu - - - name: Cache SonarCloud packages - uses: actions/cache@v4 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - - name: Cache Maven packages - uses: actions/cache@v4 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Build and analyze - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - cd app - mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=soat-tech-challenge-2025_lambda-identification-auth - - # ========================================================= - # DEPLOY LAMBDA (ORIGINAL, só com depends) ⚠️ - # ========================================================= deploy: runs-on: ubuntu-latest - needs: sonarqube environment: Production permissions: id-token: write diff --git a/.github/workflows/destroy.yml b/.github/workflows/destroy.yml index e55748e..24b02fb 100644 --- a/.github/workflows/destroy.yml +++ b/.github/workflows/destroy.yml @@ -1,49 +1,43 @@ -name: Destroy Lambda - -on: - workflow_dispatch: # Dispara manualmente - -env: - AWS_REGION: us-east-1 - -jobs: - destroy-lambda: - runs-on: ubuntu-latest - environment: Production - - steps: - # 1️⃣ Checkout do repositório - - uses: actions/checkout@v3 - - # 2️⃣ Configurar credenciais AWS - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} - aws-region: ${{ env.AWS_REGION }} - - # 3️⃣ Verificar se a Lambda existe - - name: Check if Lambda exists - id: check_lambda - run: | - if aws lambda get-function --function-name lambda-identification-auth --region $AWS_REGION > /dev/null 2>&1; then - echo "Lambda exists" - echo "exists=true" >> $GITHUB_OUTPUT - else - echo "Lambda does not exist" - echo "exists=false" >> $GITHUB_OUTPUT - fi - - # 4️⃣ Deletar Lambda se existir - - name: Delete Lambda - if: steps.check_lambda.outputs.exists == 'true' - run: | - echo "Deleting Lambda..." - aws lambda delete-function --function-name lambda-identification-auth --region $AWS_REGION - echo "✅ Lambda deleted successfully" - - # 5️⃣ Mensagem caso a Lambda não exista - - name: Lambda not found - if: steps.check_lambda.outputs.exists == 'false' - run: echo "⚠️ Lambda not found, nothing to delete" \ No newline at end of file + yaml + name: Destroy Infrastructure + + on: + workflow_dispatch: + + env: + AWS_REGION: us-east-1 + TF_DIR: . + + jobs: + destroy: + runs-on: ubuntu-latest + environment: Production + + steps: + - uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: '1.5.7' + + - name: Terraform Init + working-directory: ${{ env.TF_DIR }} + run: terraform init -input=false + + - name: Terraform Plan Destroy (preview) + working-directory: ${{ env.TF_DIR }} + run: terraform plan -destroy -input=false -out=tfplan_destroy + + - name: Terraform Destroy (apply) + working-directory: ${{ env.TF_DIR }} + run: terraform apply -input=false -auto-approve tfplan_destroy + env: + AWS_REGION: ${{ env.AWS_REGION }} diff --git a/api_gateway.tf b/api_gateway.tf index d7cc53a..e291f5c 100644 --- a/api_gateway.tf +++ b/api_gateway.tf @@ -6,8 +6,14 @@ resource "aws_apigatewayv2_integration" "lambda_backend" { payload_format_version = "2.0" } -resource "aws_apigatewayv2_route" "auth_token_route" { +resource "aws_apigatewayv2_route" "login" { api_id = data.aws_apigatewayv2_api.tc_api.id - route_key = "POST /auth/token" + route_key = "POST /login" + target = "integrations/${aws_apigatewayv2_integration.lambda_backend.id}" +} + +resource "aws_apigatewayv2_route" "me" { + api_id = data.aws_apigatewayv2_api.tc_api.id + route_key = "GET /me" target = "integrations/${aws_apigatewayv2_integration.lambda_backend.id}" } \ No newline at end of file diff --git a/app/src/main/java/tech/buildrun/lambda/Handler.java b/app/src/main/java/tech/buildrun/lambda/Handler.java index 3154d6f..a23693a 100644 --- a/app/src/main/java/tech/buildrun/lambda/Handler.java +++ b/app/src/main/java/tech/buildrun/lambda/Handler.java @@ -1,12 +1,15 @@ package tech.buildrun.lambda; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.charset.StandardCharsets; @@ -25,39 +28,79 @@ public class Handler implements RequestHandler body = - mapper.readValue(request.getBody(), Map.class); - String username = body.get("user"); + try { + String path = request.getPath(); + String method = request.getHttpMethod(); - if (username == null) { - return response(400, Map.of("message", "user obrigatórios")); + if ("/login".equals(path) && "POST".equalsIgnoreCase(method)) { + return login(request); } - String token = gerarToken(username); + if ("/me".equals(path) && "GET".equalsIgnoreCase(method)) { + return me(request); + } - return response(200, Map.of("token", token)); + return response(404, Map.of("message", "Rota não encontrada")); } catch (Exception e) { try { - return response(500, Map.of("message", "Erro ao gerar token")); + return response(500, Map.of("message", "Erro interno")); } catch (Exception ex) { throw new RuntimeException(ex); } } } - private String gerarToken(String username) { + /* ================= LOGIN ================= */ - if (JWT_SECRET == null || JWT_SECRET.length() < 32) { - throw new IllegalStateException("JWT_SECRET deve ter no mínimo 32 caracteres"); + private APIGatewayProxyResponseEvent login(APIGatewayProxyRequestEvent request) throws Exception { + + if (request.getBody() == null || request.getBody().isBlank()) { + return response(400, Map.of("message", "Body obrigatório")); + } + + Map body = + mapper.readValue(request.getBody(), Map.class); + + String username = body.get("user"); + + if (username == null || username.isBlank()) { + return response(400, Map.of("message", "user é obrigatório")); + } + + String token = gerarToken(username); + + return response(200, Map.of("token", token)); + } + + /* ================= ME ================= */ + + private APIGatewayProxyResponseEvent me(APIGatewayProxyRequestEvent request) + throws Exception { + + String authHeader = request.getHeaders().get("authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return response(401, Map.of("message", "Token não informado")); } + String token = authHeader.replace("Bearer ", ""); + + Claims claims = validarToken(token); + + return response(200, Map.of( + "user", claims.getSubject(), + "role", claims.get("role") + )); + } + + /* ================= JWT ================= */ + + private String gerarToken(String username) { + + validarSecret(); + SecretKey key = Keys.hmacShaKeyFor( JWT_SECRET.getBytes(StandardCharsets.UTF_8) ); @@ -71,10 +114,33 @@ private String gerarToken(String username) { .compact(); } + private Claims validarToken(String token) { + + validarSecret(); + + SecretKey key = Keys.hmacShaKeyFor( + JWT_SECRET.getBytes(StandardCharsets.UTF_8) + ); + + return Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + private void validarSecret() { + if (JWT_SECRET == null || JWT_SECRET.length() < 32) { + throw new IllegalStateException("JWT_SECRET deve ter no mínimo 32 caracteres"); + } + } + + /* ================= RESPONSE ================= */ + private APIGatewayProxyResponseEvent response(int status, Object body) throws Exception { return new APIGatewayProxyResponseEvent() .withStatusCode(status) .withHeaders(Map.of("Content-Type", "application/json")) .withBody(mapper.writeValueAsString(body)); } -} \ No newline at end of file +} diff --git a/lambda.tf b/lambda.tf index 8c8b316..478c839 100644 --- a/lambda.tf +++ b/lambda.tf @@ -2,8 +2,19 @@ data "aws_iam_role" "lambda_exec_role" { name = "tc-infra-id-lambda-exec-role" } -data "aws_security_group" "id_lambda" { - name = "tc-id-lambda-sg" +resource "aws_security_group" "id_lambda" { + name = "tc-id-lambda-sg" + description = "Security group for Lambda ID function" + vpc_id = data.aws_vpc.tc_lambda_vpc.id + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = var.tags } resource "aws_lambda_function" "id_lambda" { @@ -29,7 +40,7 @@ resource "aws_lambda_function" "id_lambda" { } vpc_config { subnet_ids = data.aws_subnets.tc_lambda_subnets.ids - security_group_ids = [data.aws_security_group.id_lambda.id] + security_group_ids = [aws_security_group.id_lambda.id] } tags = var.tags