diff --git a/docs/modules/ROOT/attachments/examples.json b/docs/modules/ROOT/attachments/examples.json index 8d688d02..99d3d7ff 100644 --- a/docs/modules/ROOT/attachments/examples.json +++ b/docs/modules/ROOT/attachments/examples.json @@ -114,6 +114,11 @@ "description": "Shows how to secure platform HTTP with Keycloak", "link": "https://github.com/apache/camel-quarkus-examples/tree/main/platform-http-security-keycloak" }, + { + "title": "REST to SOAP with Keycloak and JMS", + "description": "Demonstrates REST-to-SOAP synchronous bridging with Keycloak security and asynchronous JMS topic events", + "link": "https://github.com/apache/camel-quarkus-examples/tree/main/rest-keycloak-soap-jms" + }, { "title": "REST with Jackson", "description": "Demonstrates how to create a REST service using the Camel REST DSL and Jackson.", diff --git a/pom.xml b/pom.xml index 01aa8cce..c9564387 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ platform-http-security-keycloak quarkus-rest-json rest-json + rest-keycloak-soap-jms saga spring-redis timer-log diff --git a/rest-keycloak-soap-jms/.gitignore b/rest-keycloak-soap-jms/.gitignore new file mode 100644 index 00000000..099afde9 --- /dev/null +++ b/rest-keycloak-soap-jms/.gitignore @@ -0,0 +1,18 @@ +target/ +*.log +.DS_Store +.classpath +.project +.settings/ +.vscode/ +.idea/ +*.iml +*.iws +*.ipr +.gradle/ +build/ +*.swp +*.swo +*~ +.env +.env.local \ No newline at end of file diff --git a/rest-keycloak-soap-jms/README.adoc b/rest-keycloak-soap-jms/README.adoc new file mode 100644 index 00000000..8a6c29b3 --- /dev/null +++ b/rest-keycloak-soap-jms/README.adoc @@ -0,0 +1,602 @@ += REST to SOAP with Keycloak and JMS: A Camel Quarkus example +:cq-example-description: An example that demonstrates REST-to-SOAP synchronous bridging with Keycloak security and asynchronous JMS topic events + +{cq-description} + +TIP: Check the https://camel.apache.org/camel-quarkus/latest/first-steps.html[Camel Quarkus User guide] for prerequisites +and other general information. + +== Overview + +This example demonstrates how to bridge REST frontends with SOAP backends while securing both with Keycloak authentication. +It also shows how to use AMQ Broker (Apache ActiveMQ Artemis) for asynchronous event processing without complicating the synchronous request-response flow. + +The example includes: + +* *REST-to-SOAP bridge*: REST endpoint calls SOAP service synchronously and returns response to client +* *Async event processing*: Order events are published to JMS topic for multiple independent consumers: +** Audit logging (compliance trail) +** Email notifications (simulated) +** Cache invalidation (consistency pattern) +* *Unified security*: Both REST (Quarkus OIDC) and SOAP (Camel Keycloak Policy) secured with Keycloak +* *Admin endpoint*: Demonstrates role-based access control with Keycloak + +=== Architecture + +[source] +---- +REST Client + ↓ POST /api/orders/submit (with Bearer token) +REST Endpoint (secured by Quarkus OIDC) + ├─→ wireTap (async, fire-and-forget) + │ ↓ + │ JMS Topic: order-events + │ ├─→ Audit Logger (Consumer 1) + │ ├─→ Email Notification (Consumer 2) + │ └─→ Cache Invalidation (Consumer 3) + │ + └─→ SOAP Service (sync, waits for response) + ↓ + SOAP Response (secured by Keycloak Policy) + ↓ + Returns to REST Client +---- + +*Key points*: +- **Synchronous**: REST client gets immediate SOAP response +- **Asynchronous**: JMS topic broadcasts order events to multiple consumers (audit, email, cache) +- **Decoupled**: Adding new consumers doesn't change the main REST-to-SOAP flow +- **Secured**: Both REST and SOAP endpoints require valid Keycloak tokens + +*Why this pattern?* +- Main flow stays simple (direct REST-to-SOAP bridge) +- Side effects (logging, notifications, cache) don't block response +- JMS provides message persistence and durable subscriptions +- Easy to add new event consumers without modifying existing code + +== Prerequisites + +The example application requires both a Keycloak instance and an AMQ Broker (Apache ActiveMQ Artemis) instance. + +You do not need to provide these instances yourself as long as you play with the example code in dev mode +(a.k.a. `mvn quarkus:dev`) - read more https://quarkus.io/guides/getting-started#development-mode[here] +or as long as you only run the supplied tests (`mvn test`). +In those situations, Quarkus tooling automatically starts both Keycloak and Artemis containers for you via +https://quarkus.io/guides/security-openid-connect-dev-services[Quarkus Dev Services] and +https://docs.quarkiverse.io/quarkus-artemis/dev/index.html#_dev_services[Artemis Dev Services], +and it also configures the application so that you do not need to touch anything in `application.properties`. + +[[users-configuration]] + +=== Users configuration + +In all scenarios which we will cover, we will need two users: +- *customer* (with role `customer-role` and password `customer-pass`) - can submit orders +- *admin* (with role `admin-role` and password `admin-pass`) - can submit orders and access admin endpoints + +WARNING: These hardcoded credentials are for demonstration purposes only and should never be used in production. + +=== Quarkus OIDC + +We use the approach described in https://quarkus.io/guides/security-openid-connect-client-reference[Quarkus Open ID Connect] +to protect the application with Keycloak as our OIDC provider. This automatically secures our +Camel Quarkus routes using the `keycloakPolicy` policy defined in the application. + +== Start in Development mode + +=== Run the app with Dev Services + +Run the application in development mode. Quarkus will automatically start Keycloak and AMQ Broker containers: + +[source,shell] +---- +$ mvn clean compile quarkus:dev +---- + +The above command compiles the project, starts the application, and starts both Keycloak and AMQ Broker instances via Dev Services. +The Quarkus tooling watches for changes in your workspace. Any modifications in your project will automatically take effect +in the running application. + +TIP: Please refer to the Development mode section of +https://camel.apache.org/camel-quarkus/latest/first-steps.html#_development_mode[Camel Quarkus User guide] for more details. + +Now you can move on to the <> section with the assumption that `KEYCLOAK_URL=http://localhost:8082` and `APP_URL=http://localhost:8080`. + +[[playground]] + +=== Playground + +The first thing to do is to obtain the Bearer token from the running Keycloak instance for each created user. Save those tokens for further authentication. + +For the `customer` user, extract value from response of key `access_token` and call it `CUSTOMER_TOKEN`: + +[source,shell] +---- +$ curl -d "client_id=quarkus-client" -d "client_secret=secret" -d "username=customer" -d "password=customer-pass" -d "grant_type=password" $KEYCLOAK_URL/realms/quarkus/protocol/openid-connect/token +---- + +For the `admin` user, extract value from response of key `access_token` and call it `ADMIN_TOKEN`: + +[source,shell] +---- +$ curl -d "client_id=quarkus-client" -d "client_secret=secret" -d "username=admin" -d "password=admin-pass" -d "grant_type=password" $KEYCLOAK_URL/realms/quarkus/protocol/openid-connect/token +---- + +Now we are ready to try the HTTP endpoints: + +==== Submit an order (authenticated endpoint) + +The `customer` user can submit orders (you should receive `200 OK` with the SOAP response showing updated stock): + +[source,shell] +---- +$ curl -i -X POST -H "Authorization: Bearer $CUSTOMER_TOKEN" -H "Content-Type: application/json" \ + -d '{"productId":"PRODUCT-001","quantity":5}' \ + $APP_URL/api/orders/submit +---- + +Expected response: +[source,json] +---- +{"success":true,"message":"Stock updated successfully","newStock":95} +---- + +TIP: Check the application logs to see the asynchronous JMS consumers in action. You should see three types of log entries: +- `AUDIT: Order event received` - Audit logging consumer +- `EMAIL: Notification sent` - Email notification consumer (simulated) +- `CACHE: Invalidated cache for key` - Cache invalidation consumer + +These consumers run independently and don't block the REST response. + +The `admin` user can also submit orders (same SOAP response format): + +[source,shell] +---- +$ curl -i -X POST -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \ + -d '{"productId":"PRODUCT-002","quantity":10}' \ + $APP_URL/api/orders/submit +---- + +Without authentication, the request should fail (you should receive `401 Unauthorized`): + +[source,shell] +---- +$ curl -i -X POST -H "Content-Type: application/json" \ + -d '{"productId":"PRODUCT-003","quantity":3}' \ + $APP_URL/api/orders/submit +---- + +==== Access admin endpoint (authorized endpoint) + +The `customer` user cannot access admin endpoints (you should receive `403 Forbidden`): + +[source,shell] +---- +$ curl -i -X GET -H "Authorization: Bearer $CUSTOMER_TOKEN" $APP_URL/api/admin/users +---- + +The `admin` user can access admin endpoints (you should receive `200 OK` with a list of Keycloak users): + +[source,shell] +---- +$ curl -i -X GET -H "Authorization: Bearer $ADMIN_TOKEN" $APP_URL/api/admin/users +---- + +==== SOAP Inventory Service + +The SOAP endpoint is available at `/services/inventory` and is also secured with Keycloak authentication. +You can test it using a SOAP client tool like SoapUI or curl with SOAP envelope: + +[source,shell] +---- +$ curl -i -X POST -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: text/xml" \ + -d ' + + + PRODUCT-001 + 5 + + + ' \ + $APP_URL/services/inventory +---- + +Expected response: +[source,xml] +---- + + + + true + Stock updated successfully + 95 + + + +---- + +[[external-instances-configuration]] + +== Prerequisites for externally running Keycloak and AMQ Broker instances + +For the next steps, we need to have externally running Keycloak and AMQ Broker instances. + +=== Run Keycloak + +You can start a Keycloak instance easily via Docker: + +[source,shell] +---- +$ docker run --name keycloak_amq_demo -p 8082:8080 \ + -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \ + quay.io/keycloak/keycloak:26.6.1 \ + start-dev +---- + +=== Configure the Keycloak realm + +Then go to `http://localhost:8082/` click on `Administration Console` and login with `admin:admin`. + +Create a new realm named `quarkus` with the following configuration: + +*Create a client:* +- Client ID: `quarkus-client` +- Client authentication: On +- Direct access grants: Enabled +- Client secret: `secret` + +*Create two roles:* +- `customer-role` +- `admin-role` + +*Create two users:* +- Username: `customer`, Password: `customer-pass`, Email: `customer@example.com`, Role: `customer-role` +- Username: `admin`, Password: `admin-pass`, Email: `admin@example.com`, Role: `admin-role` + +IMPORTANT: For each user, you must set: +- Email address (required - without it you'll get "Account is not fully set up" error) +- Email verified: On +- Password: Set as non-temporary +- Assign the appropriate role + +TIP: Make sure to enable direct access grants for the client to support password grant type. + +=== Run AMQ Broker (Apache ActiveMQ Artemis) + +You can start an AMQ Broker instance via Docker: + +[source,shell] +---- +$ docker run --name artemis_amq_demo -d -p 61616:61616 -p 8161:8161 \ + -e AMQ_USER=admin -e AMQ_PASSWORD=admin \ + quay.io/arkmq-org/activemq-artemis-broker:artemis.2.52.0 +---- + +The broker will be available at `tcp://localhost:61616` for JMS connections and `http://localhost:8161` for the web console. + +== JVM mode + +[source,shell] +---- +$ export QUARKUS_OIDC_AUTH_SERVER_URL=http://localhost:8082/realms/quarkus +$ export QUARKUS_OIDC_CLIENT_ID=quarkus-client +$ export QUARKUS_OIDC_CREDENTIALS_SECRET=secret +$ export QUARKUS_ARTEMIS_URL=tcp://localhost:61616 +$ export QUARKUS_ARTEMIS_USERNAME=admin +$ export QUARKUS_ARTEMIS_PASSWORD=admin +$ mvn clean package -DskipTests +$ java -jar target/quarkus-app/quarkus-run.jar +---- + +Now you can go to the <> section (with the assumption that `KEYCLOAK_URL=http://localhost:8082` and `APP_URL=http://localhost:8081`) and try it yourself. + +== Native mode + +IMPORTANT: Native mode requires having GraalVM and other tools installed. Please check the Prerequisites section +of https://camel.apache.org/camel-quarkus/latest/first-steps.html#_prerequisites[Camel Quarkus User guide]. + +To prepare a native executable using GraalVM, run the following command: + +[source,shell] +---- +$ export QUARKUS_OIDC_AUTH_SERVER_URL=http://localhost:8082/realms/quarkus +$ export QUARKUS_OIDC_CLIENT_ID=quarkus-client +$ export QUARKUS_OIDC_CREDENTIALS_SECRET=secret +$ export QUARKUS_ARTEMIS_URL=tcp://localhost:61616 +$ export QUARKUS_ARTEMIS_USERNAME=admin +$ export QUARKUS_ARTEMIS_PASSWORD=admin +$ mvn clean package -DskipTests -Dnative +$ ./target/*-runner +---- + +Now you can go to the <> section (with the assumption that `KEYCLOAK_URL=http://localhost:8082` and `APP_URL=http://localhost:8081`) and try it yourself. + +== Deploying to Kubernetes + +You can build a container image for the application like this. Refer to the https://quarkus.io/guides/deploying-to-kubernetes[Quarkus Kubernetes guide] for options around customizing image names, registries etc. + +This example uses Jib to create the container image for Kubernetes deployment. + +=== Deploy Keycloak to Kubernetes + +For a simple Keycloak deployment on Kubernetes: + +[source,shell] +---- +$ kubectl apply -f - <> to create the realm, client, roles, and users. + +IMPORTANT: When creating users via the Keycloak Admin Console or API, you MUST set the `firstName` and `lastName` fields. Without these fields, you will get an "Account is not fully set up" error when trying to authenticate. + +For local Docker Desktop Kubernetes testing, Keycloak will be available at `http://localhost:8080` (LoadBalancer service). +For remote clusters, obtain the Keycloak URL from your ingress or service configuration. + +=== Deploy Camel Quarkus application to Kubernetes + +TIP: Because we use `quarkus.kubernetes.env.secrets=quarkus-keycloak` in `application.properties` all properties from the secret `quarkus-keycloak` will be presented as ENV variables to the pod. + +TIP: To trust self-signed certificates from the Kubernetes API server use `-Dquarkus.kubernetes-client.trust-certs=true` in the deploy command. + +**Create the Kubernetes secret:** + +[source,shell] +---- +$ kubectl create secret generic quarkus-keycloak \ + --from-literal=QUARKUS_OIDC_CREDENTIALS_SECRET=secret \ + --from-literal=QUARKUS_ARTEMIS_URL=tcp://artemis-broker:61616 \ + --from-literal=QUARKUS_ARTEMIS_USERNAME=admin \ + --from-literal=QUARKUS_ARTEMIS_PASSWORD=admin +---- + +**Deploy the application:** + +For local Docker Desktop Kubernetes (without pushing to a registry): + +[source,shell] +---- +$ mvn clean package -DskipTests -Pkubernetes \ + -Dquarkus.container-image.build=true \ + -Dquarkus.container-image.push=false \ + -Dquarkus.kubernetes.image-pull-policy=Never \ + -Dquarkus.kubernetes.namespace=default \ + -Dquarkus.kubernetes.env.vars.QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/realms/quarkus \ + -Dquarkus.kubernetes.env.vars.CAMEL_COMPONENT_KEYCLOAK_SERVER_URL=http://keycloak:8080 \ + -Dquarkus.kubernetes.env.vars.CAMEL_COMPONENT_KEYCLOAK_REALM=quarkus \ + -Dquarkus.kubernetes.deploy=true +---- + +NOTE: The `-Pkubernetes` profile activates the Quarkus Kubernetes extension for deployment. + +For production clusters with a container registry: + +[source,shell] +---- +$ mvn clean package -DskipTests -Pkubernetes \ + -Dquarkus.container-image.registry= \ + -Dquarkus.container-image.group= \ + -Dquarkus.container-image.push=true \ + -Dquarkus.kubernetes.namespace= \ + -Dquarkus.kubernetes.env.vars.QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/realms/quarkus \ + -Dquarkus.kubernetes.env.vars.CAMEL_COMPONENT_KEYCLOAK_SERVER_URL=http://keycloak:8080 \ + -Dquarkus.kubernetes.env.vars.CAMEL_COMPONENT_KEYCLOAK_REALM=quarkus \ + -Dquarkus.kubernetes.deploy=true +---- + +You can check the pod status: + +[source,shell] +---- +$ kubectl get pods -l app.kubernetes.io/name=camel-quarkus-examples-rest-keycloak-soap-jms +NAME READY STATUS RESTARTS AGE +camel-quarkus-examples-rest-keycloak-soap-jms-xxx-xxx 1/1 Running 0 10m +---- + +=== Testing the Kubernetes Deployment + +IMPORTANT: Due to JWT token issuer validation, the access token must be obtained from within the Kubernetes cluster (using service names like `http://keycloak:8080`) rather than from `localhost`. Tokens obtained from `http://localhost:8080` will be rejected because the issuer URL doesn't match. + +**Test from inside the pod:** + +[source,shell] +---- +$ kubectl exec deployment/camel-quarkus-examples-rest-keycloak-soap-jms -- sh -c ' +TOKEN=$(curl -s http://keycloak:8080/realms/quarkus/protocol/openid-connect/token \ + -d "username=customer" \ + -d "password=customer-pass" \ + -d "grant_type=password" \ + -d "client_id=quarkus-client" \ + -d "client_secret=secret" | grep -o "\"access_token\":\"[^\"]*" | cut -d\" -f4) + +curl -s http://localhost:8081/api/orders/submit \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"productId\":\"PRODUCT-001\",\"quantity\":5}" +' +---- + +Expected response: +[source,json] +---- +{"success":true,"message":"Stock updated successfully","newStock":95} +---- + +**Test admin endpoint with customer role (should get 403):** + +[source,shell] +---- +$ kubectl exec deployment/camel-quarkus-examples-rest-keycloak-soap-jms -- sh -c ' +CUSTOMER_TOKEN=$(curl -s http://keycloak:8080/realms/quarkus/protocol/openid-connect/token \ + -d "username=customer" \ + -d "password=customer-pass" \ + -d "grant_type=password" \ + -d "client_id=quarkus-client" \ + -d "client_secret=secret" | grep -o "\"access_token\":\"[^\"]*" | cut -d\" -f4) + +curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8081/api/admin/users \ + -H "Authorization: Bearer $CUSTOMER_TOKEN" +' +---- + +Expected: `HTTP 403` + +**Test admin endpoint with admin role (should get 200):** + +[source,shell] +---- +$ kubectl exec deployment/camel-quarkus-examples-rest-keycloak-soap-jms -- sh -c ' +ADMIN_TOKEN=$(curl -s http://keycloak:8080/realms/quarkus/protocol/openid-connect/token \ + -d "username=admin" \ + -d "password=admin-pass" \ + -d "grant_type=password" \ + -d "client_id=quarkus-client" \ + -d "client_secret=secret" | grep -o "\"access_token\":\"[^\"]*" | cut -d\" -f4) + +curl -s http://localhost:8081/api/admin/users \ + -H "Authorization: Bearer $ADMIN_TOKEN" +' +---- + +Expected response: +[source,json] +---- +[{"role":"customer-role","email":"customer@example.com","username":"customer"},{"role":"admin-role","email":"admin@example.com","username":"admin"}] +---- + +=== Clean up + +To clean up all Kubernetes resources: + +[source,shell] +---- +# Delete the application +$ kubectl delete all -l app.kubernetes.io/name=camel-quarkus-examples-rest-keycloak-soap-jms +$ kubectl delete secret quarkus-keycloak +$ kubectl delete ingress camel-quarkus-examples-rest-keycloak-soap-jms + +# Delete Keycloak +$ kubectl delete deployment,service keycloak + +# Delete AMQ Broker +$ kubectl delete deployment,service artemis-broker +---- + +== Feedback + +Please report bugs and propose improvements via https://github.com/apache/camel-quarkus/issues[GitHub issues of Camel Quarkus] project. \ No newline at end of file diff --git a/rest-keycloak-soap-jms/eclipse-formatter-config.xml b/rest-keycloak-soap-jms/eclipse-formatter-config.xml new file mode 100644 index 00000000..2248b2b8 --- /dev/null +++ b/rest-keycloak-soap-jms/eclipse-formatter-config.xml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rest-keycloak-soap-jms/pom.xml b/rest-keycloak-soap-jms/pom.xml new file mode 100644 index 00000000..d3f2e701 --- /dev/null +++ b/rest-keycloak-soap-jms/pom.xml @@ -0,0 +1,443 @@ + + + + 4.0.0 + + camel-quarkus-examples-rest-keycloak-soap-jms + org.apache.camel.quarkus.examples + 3.37.0-SNAPSHOT + + Camel Quarkus :: Examples :: REST Keycloak SOAP JMS + Camel Quarkus Example :: REST to SOAP bridge with Keycloak security and JMS async events + + + 3.36.1 + 3.37.0-SNAPSHOT + + io.quarkus + quarkus-bom + org.apache.camel.quarkus + camel-quarkus-bom + + UTF-8 + UTF-8 + 17 + + 2.29.0 + 1.13.0 + 5.0.0 + 3.15.0 + 3.5.0 + 3.3.1 + 3.5.6 + + 3.14.3 + + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + ${camel-quarkus.platform.group-id} + ${camel-quarkus.platform.artifact-id} + ${camel-quarkus.platform.version} + pom + import + + + + + + + + org.apache.camel.quarkus + camel-quarkus-direct + + + org.apache.camel.quarkus + camel-quarkus-log + + + + + org.apache.camel.quarkus + camel-quarkus-rest + + + org.apache.camel.quarkus + camel-quarkus-jackson + + + + + org.apache.camel.quarkus + camel-quarkus-jms + + + io.quarkiverse.artemis + quarkus-artemis-jms + ${quarkiverse-artemis.version} + + + + + org.apache.camel.quarkus + camel-quarkus-support-jaxb + + + + + org.apache.camel.quarkus + camel-quarkus-cxf-soap + + + + + org.apache.camel.quarkus + camel-quarkus-bean + + + + + org.apache.camel.quarkus + camel-quarkus-http + + + + + io.quarkus + quarkus-oidc + + + org.apache.camel.quarkus + camel-quarkus-keycloak + + + + + io.quarkus + quarkus-junit + test + + + org.apache.camel.quarkus + camel-quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.testcontainers + testcontainers + test + + + io.quarkus + quarkus-test-keycloak-server + test + + + + + + + + + net.revelc.code.formatter + formatter-maven-plugin + ${formatter-maven-plugin.version} + + ${maven.multiModuleProjectDirectory}/eclipse-formatter-config.xml + LF + + + + + net.revelc.code + impsort-maven-plugin + ${impsort-maven-plugin.version} + + java.,javax.,org.w3c.,org.xml.,junit. + true + true + java.,javax.,org.w3c.,org.xml.,junit. + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + true + true + + -Xlint:unchecked + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + false + + org.jboss.logmanager.LogManager + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-surefire-plugin.version} + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + com.mycila + license-maven-plugin + ${license-maven-plugin.version} + + true +
${maven.multiModuleProjectDirectory}/header.txt
+ + **/*.adoc + **/*.txt + **/LICENSE.txt + **/LICENSE + **/NOTICE.txt + **/NOTICE + **/README + **/pom.xml.versionsBackup + **/quarkus.log* + **/target/generated-sources/** + + + SLASHSTAR_STYLE + CAMEL_PROPERTIES_STYLE + XML_STYLE + + + ${maven.multiModuleProjectDirectory}/license-properties-headerdefinition.xml + + +
+
+ + + org.apache.cxf + cxf-codegen-plugin + 4.0.5 + +
+
+ + + + org.apache.cxf + cxf-codegen-plugin + + + generate-sources + generate-sources + + wsdl2java + + + + + ${project.basedir}/src/main/resources/wsdl/InventoryService.wsdl + + org.acme.inventory + + + + + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + + + build + + build + + + + + + + net.revelc.code.formatter + formatter-maven-plugin + + + format + + format + + process-sources + + + + + + net.revelc.code + impsort-maven-plugin + + + sort-imports + + sort + + process-sources + + + + + + com.mycila + license-maven-plugin + + + license-format + + format + + process-sources + + + + +
+ + + + native + + + native + + + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + ${quarkus.native.enabled} + + + + + + + + + + kubernetes + + + kubernetes + + + + + io.quarkus + quarkus-kubernetes + + + io.quarkus + quarkus-container-image-jib + + + + + openshift + + + openshift + + + + + io.quarkus + quarkus-openshift + + + + + skip-testcontainers-tests + + + skip-testcontainers-tests + + + + true + + + + +
diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/config/KeycloakConfig.java b/rest-keycloak-soap-jms/src/main/java/org/acme/config/KeycloakConfig.java new file mode 100644 index 00000000..3a204abc --- /dev/null +++ b/rest-keycloak-soap-jms/src/main/java/org/acme/config/KeycloakConfig.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.acme.config; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Named; +import org.acme.inventory.InventoryServicePortType; +import org.apache.camel.component.cxf.jaxws.CxfEndpoint; +import org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class KeycloakConfig { + + private static final String REALMS_PATH = "/realms/"; + + @ConfigProperty(name = "quarkus.oidc.auth-server-url") + private String authServerUrl; + + @ConfigProperty(name = "quarkus.oidc.client-id") + private String clientId; + + @ConfigProperty(name = "quarkus.oidc.credentials.secret", defaultValue = "") + private String clientSecret; + + @ConfigProperty(name = "soap.inventory.client.address") + private String soapClientAddress; + + @Produces + @Named("keycloakPolicy") + public KeycloakSecurityPolicy keycloakSecurityPolicy() { + // Validate auth-server-url is provided + if (authServerUrl == null || authServerUrl.trim().isEmpty()) { + throw new IllegalStateException( + "quarkus.oidc.auth-server-url is required but was not configured"); + } + + // Parse auth-server-url to extract serverUrl and realm + // Expected format: http://localhost:8082/realms/amq-demo + int realmsIndex = authServerUrl.indexOf(REALMS_PATH); + if (realmsIndex == -1) { + throw new IllegalArgumentException( + "Invalid auth-server-url format. Expected format: /realms/, " + + "but got: " + authServerUrl); + } + + String serverUrl = authServerUrl.substring(0, realmsIndex); + String realm = authServerUrl.substring(realmsIndex + REALMS_PATH.length()); + + // Validate realm name is not empty + if (realm.isEmpty()) { + throw new IllegalArgumentException( + "Realm name cannot be empty in auth-server-url: " + authServerUrl); + } + + KeycloakSecurityPolicy policy = new KeycloakSecurityPolicy(); + policy.setServerUrl(serverUrl); + policy.setRealm(realm); + policy.setClientId(clientId); + policy.setClientSecret(clientSecret); + return policy; + } + + @Produces + @ApplicationScoped + @Named("inventoryService") + public CxfEndpoint inventoryService() { + // SOAP Server endpoint + CxfEndpoint inventoryEndpoint = new CxfEndpoint(); + inventoryEndpoint.setWsdlURL("wsdl/InventoryService.wsdl"); + inventoryEndpoint.setServiceClass(InventoryServicePortType.class); + inventoryEndpoint.setAddress("/inventory"); + return inventoryEndpoint; + } + + @Produces + @ApplicationScoped + @Named("inventoryServiceClient") + public CxfEndpoint inventoryServiceClient() { + // SOAP Client endpoint - calls the local SOAP service + CxfEndpoint clientEndpoint = new CxfEndpoint(); + clientEndpoint.setWsdlURL("wsdl/InventoryService.wsdl"); + clientEndpoint.setServiceClass(InventoryServicePortType.class); + clientEndpoint.setAddress(soapClientAddress); + return clientEndpoint; + } +} diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/model/Order.java b/rest-keycloak-soap-jms/src/main/java/org/acme/model/Order.java new file mode 100644 index 00000000..68136325 --- /dev/null +++ b/rest-keycloak-soap-jms/src/main/java/org/acme/model/Order.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.acme.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection +public class Order { + private String productId; + private Integer quantity; + + public Order() { + } + + public Order(String productId, Integer quantity) { + this.productId = productId; + this.quantity = quantity; + } + + public String getProductId() { + return productId; + } + + public void setProductId(String productId) { + this.productId = productId; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } +} diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/routes/JmsOrderProcessorRoute.java b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/JmsOrderProcessorRoute.java new file mode 100644 index 00000000..af9160fd --- /dev/null +++ b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/JmsOrderProcessorRoute.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.acme.routes; + +import jakarta.enterprise.context.ApplicationScoped; +import org.acme.model.Order; +import org.apache.camel.builder.RouteBuilder; + +@ApplicationScoped +public class JmsOrderProcessorRoute extends RouteBuilder { + + @Override + public void configure() { + // JMS Consumer 1: Audit logging + // Uses durable subscription to survive application restarts + from("jms:topic:order-events?clientId=audit-consumer&durableSubscriptionName=audit") + .routeId("order-audit-logger") + .log("AUDIT: Order event received - ${body}") + .to("log:org.acme.audit?level=INFO&showHeaders=false"); + + // JMS Consumer 2: Email notification (simulated) + // Demonstrates how to add independent consumers without changing main flow + from("jms:topic:order-events?clientId=email-consumer&durableSubscriptionName=email") + .routeId("order-email-notification") + .unmarshal().json(Order.class) + .process(exchange -> { + Order order = exchange.getIn().getBody(Order.class); + String emailBody = String.format( + "Order Notification:\n- Product: %s\n- Quantity: %d\n- Status: Processing", + order.getProductId(), order.getQuantity()); + exchange.getIn().setBody(emailBody); + }) + .to("log:org.acme.notification.email?level=INFO") + .log("EMAIL: Notification sent for product ${header.productId}"); + + // JMS Consumer 3: Cache invalidation (simulated) + // Shows pattern for updating cache when inventory changes + from("jms:topic:order-events?clientId=cache-consumer&durableSubscriptionName=cache") + .routeId("order-cache-invalidation") + .unmarshal().json(Order.class) + .process(exchange -> { + Order order = exchange.getIn().getBody(Order.class); + // this product will be removed/invalidated from cache in real scenario (cache-aside) + String cacheKey = "inventory:" + order.getProductId(); + exchange.getIn().setHeader("cacheKey", cacheKey); + }) + .to("log:org.acme.cache?level=INFO") + .log("CACHE: Invalidated cache for key ${header.cacheKey}"); + } +} diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/routes/KeycloakAdminRoute.java b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/KeycloakAdminRoute.java new file mode 100644 index 00000000..4f7c950f --- /dev/null +++ b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/KeycloakAdminRoute.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.acme.routes; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.builder.RouteBuilder; + +@ApplicationScoped +public class KeycloakAdminRoute extends RouteBuilder { + + @Override + public void configure() { + // Error handling for Keycloak operations + onException(Exception.class) + .handled(true) + .setHeader("Content-Type", constant("application/json")) + .setHeader("CamelHttpResponseCode", constant(500)) + .setBody(constant("{\"error\": \"Internal server error\", \"message\": \"${exception.message}\"}")) + .log("Error in Keycloak admin route: ${exception.message}"); + + rest("/api/admin") + .get("/users") + .produces("application/json") + .to("direct:list-users"); + + from("direct:list-users") + .routeId("keycloak-admin-users") + .log("Admin endpoint accessed - returning user list") + .process(exchange -> { + // For this example, return a mock user list demonstrating the endpoint works + // In production, you would configure proper Keycloak Admin API access + List> users = new ArrayList<>(); + + Map customer = new HashMap<>(); + customer.put("username", "customer"); + customer.put("email", "customer@example.com"); + customer.put("role", "customer-role"); + users.add(customer); + + Map admin = new HashMap<>(); + admin.put("username", "admin"); + admin.put("email", "admin@example.com"); + admin.put("role", "admin-role"); + users.add(admin); + + exchange.getMessage().setBody(users); + }) + .marshal().json(); + } +} diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/routes/RestOrderRoute.java b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/RestOrderRoute.java new file mode 100644 index 00000000..a8027ece --- /dev/null +++ b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/RestOrderRoute.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.acme.routes; + +import jakarta.enterprise.context.ApplicationScoped; +import org.acme.inventory.UpdateStockRequest; +import org.acme.inventory.UpdateStockResponse; +import org.acme.model.Order; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.rest.RestBindingMode; + +@ApplicationScoped +public class RestOrderRoute extends RouteBuilder { + + @Override + public void configure() { + // REST endpoint: secured by Quarkus OIDC + rest("/api/orders") + .post("/submit") + .consumes("application/json") + .produces("application/json") + .bindingMode(RestBindingMode.json) + .type(Order.class) + .to("direct:process-order"); + + // Main route: REST → SOAP (synchronous) + async event notification + from("direct:process-order") + .routeId("rest-to-soap-bridge") + .log("Received order: ${body}") + // Async: Send event to JMS for audit/notification (fire-and-forget) + .wireTap("direct:order-events") + // Sync: Convert Order to SOAP UpdateStockRequest + .process(exchange -> { + Order order = exchange.getIn().getBody(Order.class); + UpdateStockRequest soapRequest = new UpdateStockRequest(); + soapRequest.setProductId(order.getProductId()); + soapRequest.setQuantity(order.getQuantity()); + exchange.getIn().setBody(soapRequest); + }) + // Sync: Call SOAP service and wait for response + .to("cxf:bean:inventoryServiceClient") + // Return SOAP response to REST client + .process(exchange -> { + UpdateStockResponse soapResponse = exchange.getIn().getBody(UpdateStockResponse.class); + String jsonResponse = String.format( + "{\"success\":%b,\"message\":\"%s\",\"newStock\":%d}", + soapResponse.isSuccess(), + soapResponse.getMessage(), + soapResponse.getNewStock()); + exchange.getIn().setBody(jsonResponse); + exchange.getIn().setHeader("Content-Type", "application/json"); + }); + + // Async event publisher: send to JMS topic + from("direct:order-events") + .routeId("order-event-publisher") + .marshal().json() + .to("jms:topic:order-events") + .log("Published order event to JMS topic"); + } +} diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/routes/SoapInventoryRoute.java b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/SoapInventoryRoute.java new file mode 100644 index 00000000..e4b16333 --- /dev/null +++ b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/SoapInventoryRoute.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.acme.routes; + +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.cxf.common.message.CxfConstants; + +@ApplicationScoped +public class SoapInventoryRoute extends RouteBuilder { + + @Override + public void configure() { + from("cxf:bean:inventoryService") + .policy("keycloakPolicy") + .removeHeader(CxfConstants.OPERATION_NAME) + .to("bean:inventoryServiceImpl?method=updateStock"); + } +} diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/service/InventoryServiceImpl.java b/rest-keycloak-soap-jms/src/main/java/org/acme/service/InventoryServiceImpl.java new file mode 100644 index 00000000..34c41e34 --- /dev/null +++ b/rest-keycloak-soap-jms/src/main/java/org/acme/service/InventoryServiceImpl.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.acme.service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Named; +import org.acme.inventory.UpdateStockRequest; +import org.acme.inventory.UpdateStockResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApplicationScoped +@Named("inventoryServiceImpl") +@RegisterForReflection(methods = true) +public class InventoryServiceImpl { + + private static final Logger LOG = LoggerFactory.getLogger(InventoryServiceImpl.class); + + private final Map inventory = new ConcurrentHashMap<>(); + + public InventoryServiceImpl() { + // Initialize with some sample inventory + inventory.put("PRODUCT-001", 100); + inventory.put("PRODUCT-002", 50); + inventory.put("PRODUCT-003", 75); + } + + public UpdateStockResponse updateStock(UpdateStockRequest request) { + if (request == null || request.getProductId() == null) { + UpdateStockResponse response = new UpdateStockResponse(); + response.setSuccess(false); + response.setMessage("Invalid request"); + response.setNewStock(0); + return response; + } + + LOG.info("SOAP: Updating stock for product: {}, quantity: {}", request.getProductId(), request.getQuantity()); + + Integer newStock = inventory.compute(request.getProductId(), (productId, currentStock) -> { + if (currentStock == null || currentStock - request.getQuantity() < 0) { + return currentStock; + } + return currentStock - request.getQuantity(); + }); + + UpdateStockResponse response = new UpdateStockResponse(); + if (newStock != null) { + response.setSuccess(true); + response.setMessage("Stock updated successfully"); + response.setNewStock(newStock); + } else { + response.setSuccess(false); + response.setMessage("Product not found or insufficient stock"); + response.setNewStock(0); + } + return response; + } +} diff --git a/rest-keycloak-soap-jms/src/main/resources/application.properties b/rest-keycloak-soap-jms/src/main/resources/application.properties new file mode 100644 index 00000000..201bff63 --- /dev/null +++ b/rest-keycloak-soap-jms/src/main/resources/application.properties @@ -0,0 +1,67 @@ +## --------------------------------------------------------------------------- +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## --------------------------------------------------------------------------- + +# Camel +camel.context.name = rest-keycloak-soap-jms-example + +# AMQ Artemis (dev services auto-start) +%prod.quarkus.artemis.url=tcp://localhost:61616 +%prod.quarkus.artemis.username=admin +%prod.quarkus.artemis.password=admin + +# Production HTTP port +%prod.quarkus.http.port=8081 + +# Keycloak OIDC +%prod.quarkus.oidc.auth-server-url=http://localhost:8082/realms/quarkus +quarkus.oidc.client-id=quarkus-client +quarkus.oidc.application-type=service + +# Keycloak Dev Services +quarkus.keycloak.devservices.port=8082 +quarkus.keycloak.devservices.users.customer=customer-pass +quarkus.keycloak.devservices.roles.customer=customer-role +quarkus.keycloak.devservices.users.admin=admin-pass +quarkus.keycloak.devservices.roles.admin=admin-role + +# HTTP Security - REST endpoints protected by Quarkus OIDC +quarkus.http.auth.permission.customer.paths=/api/orders/* +quarkus.http.auth.permission.customer.policy=authenticated +quarkus.http.auth.permission.admin.paths=/api/admin/* +quarkus.http.auth.permission.admin.policy=admin-policy +quarkus.http.auth.policy.admin-policy.roles-allowed=admin-role + +# Keycloak Component Configuration (for Camel Keycloak component) +camel.component.keycloak.server-url=http://localhost:8082 +camel.component.keycloak.realm=quarkus +camel.component.keycloak.client-id=quarkus-client +camel.component.keycloak.client-secret=${QUARKUS_OIDC_CREDENTIALS_SECRET:secret} + +# CXF SOAP +quarkus.cxf.path=/services +soap.inventory.client.address=http://localhost:8081/services/inventory + +# Kubernetes +quarkus.kubernetes.env.secrets=quarkus-keycloak + +# Dev Configuration +%dev.quarkus.artemis.devservices.enabled=true +%dev.soap.inventory.client.address=http://localhost:8080/services/inventory + +# Test Configuration +%test.quarkus.artemis.devservices.enabled=true +%test.soap.inventory.client.address=http://localhost:8081/services/inventory \ No newline at end of file diff --git a/rest-keycloak-soap-jms/src/main/resources/wsdl/InventoryService.wsdl b/rest-keycloak-soap-jms/src/main/resources/wsdl/InventoryService.wsdl new file mode 100644 index 00000000..41bc54c6 --- /dev/null +++ b/rest-keycloak-soap-jms/src/main/resources/wsdl/InventoryService.wsdl @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rest-keycloak-soap-jms/src/test/java/org/acme/AmqBrokerKeycloakIT.java b/rest-keycloak-soap-jms/src/test/java/org/acme/AmqBrokerKeycloakIT.java new file mode 100644 index 00000000..216f2219 --- /dev/null +++ b/rest-keycloak-soap-jms/src/test/java/org/acme/AmqBrokerKeycloakIT.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.acme; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class AmqBrokerKeycloakIT extends AmqBrokerKeycloakTest { + +} diff --git a/rest-keycloak-soap-jms/src/test/java/org/acme/AmqBrokerKeycloakTest.java b/rest-keycloak-soap-jms/src/test/java/org/acme/AmqBrokerKeycloakTest.java new file mode 100644 index 00000000..adb65d14 --- /dev/null +++ b/rest-keycloak-soap-jms/src/test/java/org/acme/AmqBrokerKeycloakTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.acme; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.keycloak.client.KeycloakTestClient; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.ConfigProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@QuarkusTest +public class AmqBrokerKeycloakTest { + + private static final String SAMPLE_ORDER_JSON = "{\"productId\":\"PRODUCT-001\",\"quantity\":5}"; + + KeycloakTestClient keycloakClient = new KeycloakTestClient(); + private static String clientId; + + @BeforeAll + public static void setUp() { + clientId = ConfigProvider.getConfig().getValue("quarkus.oidc.client-id", String.class); + } + + @Test + public void orderSubmissionShouldRequireAuthentication() { + // Without authentication token, should get 401 Unauthorized + given() + .header("Content-Type", "application/json") + .body(SAMPLE_ORDER_JSON) + .when().post("/api/orders/submit") + .then() + .statusCode(Response.Status.UNAUTHORIZED.getStatusCode()); + } + + @Test + public void orderSubmissionWithAuthenticationShouldSucceed() { + // With valid customer token, order submission should succeed + // This validates the complete flow: REST → Keycloak auth → SOAP (sync) + JMS (async) + String response = given() + .auth().oauth2(getCustomerAccessToken()) + .header("Content-Type", "application/json") + .body(SAMPLE_ORDER_JSON) + .when().post("/api/orders/submit") + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract().asString(); + + // Verify the SOAP response was received (synchronous response) + assertTrue( + response.contains("success") && response.contains("true"), + "Response should contain SOAP success result: " + response); + } + + @Test + public void adminEndpointShouldRequireAdminRole() { + // Customer user should get 403 Forbidden (authenticated but not authorized) + given() + .auth().oauth2(getCustomerAccessToken()) + .when().get("/api/admin/users") + .then() + .statusCode(Response.Status.FORBIDDEN.getStatusCode()); + } + + @Test + public void adminEndpointShouldWorkForAdminUser() { + // Admin user should get 200 OK with user list + String response = given() + .auth().oauth2(getAdminAccessToken()) + .when().get("/api/admin/users") + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract().asString(); + + // Verify the response contains user data (should be JSON array) + assertTrue( + response.contains("customer") || response.contains("admin"), + "Response should contain user list: " + response); + } + + private String getCustomerAccessToken() { + return keycloakClient.getAccessToken("customer", "customer-pass", clientId); + } + + private String getAdminAccessToken() { + return keycloakClient.getAccessToken("admin", "admin-pass", clientId); + } +}