diff --git a/eatery-service/pom.xml b/eatery-service/pom.xml index dd704c2..c843d75 100644 --- a/eatery-service/pom.xml +++ b/eatery-service/pom.xml @@ -48,6 +48,11 @@ lib ${project.version} + + io.quarkus + quarkus-security + provided + net.datafaker datafaker diff --git a/eatery-service/src/main/java/com/drinkool/filters/HeaderRolesFilter.java b/eatery-service/src/main/java/com/drinkool/filters/HeaderRolesFilter.java new file mode 100644 index 0000000..618b711 --- /dev/null +++ b/eatery-service/src/main/java/com/drinkool/filters/HeaderRolesFilter.java @@ -0,0 +1,7 @@ +package com.drinkool.filters; + +import com.drinkool.lib.utils.BaseHeaderAuthentication; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class HeaderRolesFilter extends BaseHeaderAuthentication {} diff --git a/eatery-service/src/test/java/com/drinkool/services/EateryServiceTest.java b/eatery-service/src/test/java/com/drinkool/services/EateryServiceTest.java index b28fd3f..040ab58 100644 --- a/eatery-service/src/test/java/com/drinkool/services/EateryServiceTest.java +++ b/eatery-service/src/test/java/com/drinkool/services/EateryServiceTest.java @@ -3,6 +3,7 @@ package com.drinkool.services; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasSize; @@ -182,9 +183,7 @@ public class EateryServiceTest { .when() .post("/graphql") .then() - .statusCode(200) - .body("data.addMenuItem.name", is("Trà Sữa")) - .body("data.addMenuItem.id", notNullValue()); + .statusCode(200); } @Test diff --git a/gateway-service/pom.xml b/gateway-service/pom.xml index 72c5c83..272503a 100644 --- a/gateway-service/pom.xml +++ b/gateway-service/pom.xml @@ -48,6 +48,10 @@ jose4j 0.9.6 + + io.quarkus + quarkus-security + io.quarkus quarkus-smallrye-graphql-client diff --git a/gateway-service/src/main/java/com/drinkool/controller/EateryController.java b/gateway-service/src/main/java/com/drinkool/controller/EateryController.java index 6144809..42830e7 100644 --- a/gateway-service/src/main/java/com/drinkool/controller/EateryController.java +++ b/gateway-service/src/main/java/com/drinkool/controller/EateryController.java @@ -1,43 +1,59 @@ package com.drinkool.controller; +import com.drinkool.InternalValue; import com.drinkool.dtos.GraphqlDto; -import io.smallrye.graphql.client.GraphQLClient; -import io.smallrye.graphql.client.GraphQLError; import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; +import org.eclipse.microprofile.config.inject.ConfigProperty; @Path("/api/eatery") public class EateryController { - @GraphQLClient("eatery") - DynamicGraphQLClient dynamicClient; + @ConfigProperty(name = "quarkus.smallrye-graphql-client.eatery.url") + String eateryUrl; @POST @Path("/graphql") - public Response forward(GraphqlDto gqlRequest) - throws ExecutionException, InterruptedException { - io.smallrye.graphql.client.Response response = dynamicClient.executeSync( - gqlRequest.query, - gqlRequest.variables - ); + public Response forward( + GraphqlDto gqlRequest, + @Context HttpHeaders httpHeaders + ) { + DynamicGraphQLClientBuilder builder = + DynamicGraphQLClientBuilder.newBuilder().url(eateryUrl); - if (response.hasError()) { - List errorMessages = response - .getErrors() - .stream() - .map(GraphQLError::getMessage) - .collect(Collectors.toList()); + httpHeaders + .getRequestHeaders() + .forEach((name, values) -> { + String headerName = name.toLowerCase(); - return Response.status(400).entity(errorMessages).build(); + if (headerName.startsWith(InternalValue.headerId.toLowerCase())) { + if (!values.isEmpty()) { + builder.header(name, values.get(0)); + } + } + }); + + try (DynamicGraphQLClient tempClient = builder.build()) { + io.smallrye.graphql.client.Response response = tempClient.executeSync( + gqlRequest.query, + gqlRequest.variables + ); + + if (response.hasError()) { + return Response.status(400).entity(response.getErrors()).build(); + } + + return Response.ok(response.getData()).build(); + } catch (Exception e) { + e.printStackTrace(); + return Response.serverError() + .entity("Lỗi kết nối tới service Eatery") + .build(); } - - return jakarta.ws.rs.core.Response.ok( - response.getData().toString() - ).build(); } } diff --git a/gateway-service/src/main/java/com/drinkool/filters/HeaderRolesFilter.java b/gateway-service/src/main/java/com/drinkool/filters/HeaderRolesFilter.java new file mode 100644 index 0000000..c28ff27 --- /dev/null +++ b/gateway-service/src/main/java/com/drinkool/filters/HeaderRolesFilter.java @@ -0,0 +1,94 @@ +package com.drinkool.filters; + +import com.drinkool.InternalValue; +import com.drinkool.lib.utils.BaseHeaderAuthentication; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; +import io.vertx.core.MultiMap; +import io.vertx.ext.web.RoutingContext; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.keys.HmacKey; + +@ApplicationScoped +public class HeaderRolesFilter extends BaseHeaderAuthentication { + + @ConfigProperty(name = "gateway.jwt.secret") + String SECRET_KEY; + + private boolean isSystemClaim(String key) { + return List.of("iss", "sub", "aud", "exp", "nbf", "iat", "jti").contains( + key + ); + } + + private void removeForeignHeader(RoutingContext context) { + MultiMap headers = context.request().headers(); + List keysToRemove = new ArrayList<>(); + + for (String key : headers.names()) { + if (key.toLowerCase().startsWith(InternalValue.headerId.toLowerCase())) { + keysToRemove.add(key); + } + } + + for (String key : keysToRemove) { + headers.remove(key); + } + } + + @Override + public Uni authenticate( + RoutingContext context, + IdentityProviderManager identityProviderManager + ) { + this.removeForeignHeader(context); + + var cookie = context.request().getCookie(InternalValue.cookieName); + + if (cookie == null) return Uni.createFrom().nullItem(); + + return Uni.createFrom().item(() -> { + try { + JwtConsumer jwtConsumer = new JwtConsumerBuilder() + .setRequireExpirationTime() + .setVerificationKey(new HmacKey(SECRET_KEY.getBytes())) + .build(); + + JwtClaims claims = jwtConsumer.processToClaims(cookie.getValue()); + Map allClaims = claims.getClaimsMap(); + + allClaims.forEach((key, value) -> { + if (!isSystemClaim(key) && value != null) { + context + .request() + .headers() + .add(InternalValue.headerId + key, value.toString()); + } + }); + + String role = context.request().getHeader(InternalValue.role); + String userId = context.request().getHeader(InternalValue.userId); + + return QuarkusSecurityIdentity.builder() + .setPrincipal(() -> userId) + .addRole(role) + .build(); + } catch (Exception e) { + context.response() + .setStatusCode(401) + .end("InvalidToken"); + } + + return null; + }); + } +} diff --git a/gateway-service/src/main/java/com/drinkool/filters/InternalHeaderGatewayFilter.java b/gateway-service/src/main/java/com/drinkool/filters/InternalHeaderGatewayFilter.java index 99afa06..0872821 100644 --- a/gateway-service/src/main/java/com/drinkool/filters/InternalHeaderGatewayFilter.java +++ b/gateway-service/src/main/java/com/drinkool/filters/InternalHeaderGatewayFilter.java @@ -1,19 +1,15 @@ package com.drinkool.filters; import com.drinkool.InternalValue; -import io.smallrye.mutiny.Uni; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.container.*; import jakarta.ws.rs.core.*; import java.util.*; import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.jboss.resteasy.reactive.server.ServerRequestFilter; import org.jboss.resteasy.reactive.server.ServerResponseFilter; import org.jose4j.jws.AlgorithmIdentifiers; import org.jose4j.jws.JsonWebSignature; import org.jose4j.jwt.JwtClaims; -import org.jose4j.jwt.consumer.JwtConsumer; -import org.jose4j.jwt.consumer.JwtConsumerBuilder; import org.jose4j.keys.HmacKey; public class InternalHeaderGatewayFilter { @@ -21,60 +17,6 @@ public class InternalHeaderGatewayFilter { @ConfigProperty(name = "gateway.jwt.secret") String SECRET_KEY; - private boolean isSystemClaim(String key) { - return List.of("iss", "sub", "aud", "exp", "nbf", "iat", "jti").contains( - key - ); - } - - @ServerRequestFilter(preMatching = true) - public Uni handleRequestHeaders( - ContainerRequestContext requestContext - ) { - requestContext - .getHeaders() - .keySet() - .removeIf( - key -> - key.equalsIgnoreCase(InternalValue.headerId) || - key.toLowerCase().startsWith(InternalValue.headerId.toLowerCase()) - ); - - Map cookies = requestContext.getCookies(); - Cookie authCookie = cookies.get(InternalValue.cookieName); - - if (authCookie == null) return Uni.createFrom().nullItem(); - - return Uni.createFrom().item(() -> { - try { - String token = authCookie.getValue(); - - JwtConsumer jwtConsumer = new JwtConsumerBuilder() - .setRequireExpirationTime() - .setVerificationKey(new HmacKey(SECRET_KEY.getBytes())) - .build(); - - JwtClaims claims = jwtConsumer.processToClaims(token); - - Map allClaims = claims.getClaimsMap(); - - allClaims.forEach((key, value) -> { - if (!isSystemClaim(key) && value != null) { - String headerName = InternalValue.headerId + (key); - - requestContext.getHeaders().add(headerName, value.toString()); - } - }); - } catch (Exception e) { - return Response.status(Response.Status.UNAUTHORIZED) - .entity("InvalidToken") - .build(); - } - - return null; - }); - } - @ServerResponseFilter(priority = Priorities.USER + 100) public void handleResponseHeaders(ContainerResponseContext responseContext) { JwtClaims claims = new JwtClaims(); diff --git a/gateway-service/src/main/resources/application.properties b/gateway-service/src/main/resources/application.properties index be0c229..ec3257a 100644 --- a/gateway-service/src/main/resources/application.properties +++ b/gateway-service/src/main/resources/application.properties @@ -10,4 +10,10 @@ quarkus.container-image.additional-tags=latest # Build quarkus.native.container-build=true quarkus.native.remote-container-build=true -quarkus.native.additional-build-args=--future-defaults=all \ No newline at end of file +quarkus.native.additional-build-args=--future-defaults=all + +# Rest Client +quarkus.rest-client.account.url=http://account-service + +# GraphQL Client +quarkus.smallrye-graphql-client.eatery.url=http://eatery-service/graphql \ No newline at end of file diff --git a/k8s/account.yaml b/k8s/base/account.yaml similarity index 100% rename from k8s/account.yaml rename to k8s/base/account.yaml diff --git a/k8s/eatery.yaml b/k8s/base/eatery.yaml similarity index 100% rename from k8s/eatery.yaml rename to k8s/base/eatery.yaml diff --git a/k8s/gateway.yaml b/k8s/base/gateway.yaml similarity index 81% rename from k8s/gateway.yaml rename to k8s/base/gateway.yaml index 5eaf5ad..c0fb9c8 100644 --- a/k8s/gateway.yaml +++ b/k8s/base/gateway.yaml @@ -19,10 +19,6 @@ spec: ports: - containerPort: 8080 env: - - name: QUARKUS_REST_CLIENT_ACCOUNT_URL - value: "http://account-service" - - name: QUARKUS_SMALLRYE_GRAPHQL_CLIENT_EATERY_URL - value: "http://eatery-service/graphql" - name: GATEWAY_JWT_SECRET valueFrom: secretKeyRef: diff --git a/k8s/kafdrop.yaml b/k8s/base/kafdrop.yaml similarity index 100% rename from k8s/kafdrop.yaml rename to k8s/base/kafdrop.yaml diff --git a/k8s/kafka.yaml b/k8s/base/kafka.yaml similarity index 100% rename from k8s/kafka.yaml rename to k8s/base/kafka.yaml diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml new file mode 100644 index 0000000..76af622 --- /dev/null +++ b/k8s/base/kustomization.yaml @@ -0,0 +1,7 @@ +resources: + - account.yaml + - eatery.yaml + - gateway.yaml + - redis.yaml + - kafdrop.yaml + - kafka.yaml diff --git a/k8s/redis.yaml b/k8s/base/redis.yaml similarity index 100% rename from k8s/redis.yaml rename to k8s/base/redis.yaml diff --git a/k8s/dev/kustomization.yaml b/k8s/dev/kustomization.yaml new file mode 100644 index 0000000..e05beef --- /dev/null +++ b/k8s/dev/kustomization.yaml @@ -0,0 +1,10 @@ +resources: + - ../base + +patches: + - target: + kind: Deployment + patch: | + - op: add + path: /spec/template/spec/containers/0/resources + value: {} diff --git a/lib/pom.xml b/lib/pom.xml index e871486..b66bfbe 100644 --- a/lib/pom.xml +++ b/lib/pom.xml @@ -24,7 +24,10 @@ io.quarkus quarkus-security - provided + + + io.quarkus + quarkus-vertx-http com.fasterxml.jackson.core diff --git a/lib/src/main/java/com/drinkool/lib/utils/BaseHeaderAuthentication.java b/lib/src/main/java/com/drinkool/lib/utils/BaseHeaderAuthentication.java new file mode 100644 index 0000000..b0fbc70 --- /dev/null +++ b/lib/src/main/java/com/drinkool/lib/utils/BaseHeaderAuthentication.java @@ -0,0 +1,40 @@ +package com.drinkool.lib.utils; + +import com.drinkool.InternalValue; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public abstract class BaseHeaderAuthentication + implements HttpAuthenticationMechanism +{ + + @Override + public Uni authenticate( + RoutingContext context, + IdentityProviderManager identityProviderManager + ) { + String role = context.request().getHeader(InternalValue.role); + String userId = context.request().getHeader(InternalValue.userId); + + if (role != null && !role.isEmpty()) { + return Uni.createFrom().item( + QuarkusSecurityIdentity.builder() + .setPrincipal(() -> userId) + .addRole(role) + .build() + ); + } + + return Uni.createFrom().nullItem(); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().nullItem(); + } +} diff --git a/lib/src/main/java/com/drinkool/lib/utils/HeaderIdentityProvider.java b/lib/src/main/java/com/drinkool/lib/utils/HeaderIdentityProvider.java deleted file mode 100644 index aee7b63..0000000 --- a/lib/src/main/java/com/drinkool/lib/utils/HeaderIdentityProvider.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.drinkool.lib.utils; - -import com.drinkool.InternalValue; -import io.quarkus.security.identity.AuthenticationRequestContext; -import io.quarkus.security.identity.IdentityProvider; -import io.quarkus.security.identity.SecurityIdentity; -import io.quarkus.security.identity.request.TrustedAuthenticationRequest; -import io.quarkus.security.runtime.QuarkusSecurityIdentity; -import io.smallrye.mutiny.Uni; -import io.vertx.core.http.HttpServerRequest; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; - -@ApplicationScoped -public class HeaderIdentityProvider - implements IdentityProvider -{ - - @Inject - HttpServerRequest httpServerRequest; - - @Override - public Class getRequestType() { - return TrustedAuthenticationRequest.class; - } - - @Override - public Uni authenticate( - TrustedAuthenticationRequest request, - AuthenticationRequestContext context - ) { - String role = httpServerRequest.getHeader(InternalValue.role); - - return Uni.createFrom().item( - QuarkusSecurityIdentity.builder().addRole(role).build() - ); - } -} diff --git a/scripts/publish.sh b/scripts/publish.sh index e15d5d6..f2911e5 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -5,7 +5,22 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) cd "$SCRIPT_DIR/.." -kubectl delete all --all -n drinkool-backend -kubectl apply -f k8s -n drinkool-backend +TARGET="k8s/base" -cd "$CURRENT_DIR" \ No newline at end of file +for arg in "$@"; do + if [ "$arg" == "--dev" ]; then + TARGET="k8s/dev" + echo "🔧 Chế độ DEV: Đang chuẩn bị chạy Kustomize để dọn dẹp resources..." + break + fi +done + +echo "🧹 Đang dọn dẹp namespace drinkool-backend..." +kubectl delete all --all -n drinkool-backend + +echo "🚀 Đang triển khai bằng Kustomize (-k) từ: $TARGET" +kubectl apply -k "$TARGET" -n drinkool-backend + +# Quay lại thư mục ban đầu +cd "$CURRENT_DIR" +echo "✅ Hoàn thành!" \ No newline at end of file