This is an automated email from the ASF dual-hosted git repository. jamesnetherton pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-quarkus-examples.git
commit b7d8219e4d8cff9537f49bbcdc9adb242dea4e40 Author: JinyuChen97 <[email protected]> AuthorDate: Tue Jun 9 15:05:31 2026 +0100 Add AMQ Broker and Keycloak integration example This example demonstrates integration of Apache ActiveMQ Artemis (AMQ Broker) with Keycloak authentication in a Camel Quarkus application. Features: - REST endpoint for order submission with Keycloak authentication - SOAP endpoint for inventory management with Keycloak security policy - JMS integration using AMQ Broker for asynchronous order processing - Admin endpoint with role-based access control (RBAC) - Keycloak component for user management The example supports: - Development mode with Dev Services (auto-start Keycloak and Artemis) - JVM mode - Native mode - Kubernetes deployment with complete YAML manifests and testing guide On-Behalf-Of: Jinyu Chen <[email protected]> Co-authored-by: Claude Sonnet 4.5 <[email protected]> --- docs/modules/ROOT/attachments/examples.json | 5 + pom.xml | 1 + rest-keycloak-soap-jms/.gitignore | 18 + rest-keycloak-soap-jms/README.adoc | 602 +++++++++++++++++++++ .../eclipse-formatter-config.xml | 276 ++++++++++ rest-keycloak-soap-jms/pom.xml | 443 +++++++++++++++ .../main/java/org/acme/config/KeycloakConfig.java | 102 ++++ .../src/main/java/org/acme/model/Order.java | 49 ++ .../org/acme/routes/JmsOrderProcessorRoute.java | 64 +++ .../java/org/acme/routes/KeycloakAdminRoute.java | 69 +++ .../main/java/org/acme/routes/RestOrderRoute.java | 75 +++ .../java/org/acme/routes/SoapInventoryRoute.java | 33 ++ .../org/acme/service/InventoryServiceImpl.java | 76 +++ .../src/main/resources/application.properties | 67 +++ .../src/main/resources/wsdl/InventoryService.wsdl | 81 +++ .../test/java/org/acme/AmqBrokerKeycloakIT.java | 24 + .../test/java/org/acme/AmqBrokerKeycloakTest.java | 105 ++++ 17 files changed, 2090 insertions(+) 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 @@ <module>platform-http-security-keycloak</module> <module>quarkus-rest-json</module> <module>rest-json</module> + <module>rest-keycloak-soap-jms</module> <module>saga</module> <module>spring-redis</module> <module>timer-log</module> 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 <<playground>> 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 '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> + <soap:Body> + <tns:UpdateStockRequest xmlns:tns="http://acme.org/inventory"> + <productId>PRODUCT-001</productId> + <quantity>5</quantity> + </tns:UpdateStockRequest> + </soap:Body> + </soap:Envelope>' \ + $APP_URL/services/inventory +---- + +Expected response: +[source,xml] +---- +<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> + <soap:Body> + <ns2:UpdateStockResponse xmlns:ns2="http://acme.org/inventory"> + <success>true</success> + <message>Stock updated successfully</message> + <newStock>95</newStock> + </ns2:UpdateStockResponse> + </soap:Body> +</soap:Envelope> +---- + +[[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: `[email protected]`, Role: `customer-role` +- Username: `admin`, Password: `admin-pass`, Email: `[email protected]`, 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 <<playground>> 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 <<playground>> 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 - <<EOF +apiVersion: v1 +kind: Service +metadata: + name: keycloak + labels: + app: keycloak +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: keycloak + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak + labels: + app: keycloak +spec: + replicas: 1 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + spec: + containers: + - name: keycloak + image: quay.io/keycloak/keycloak:26.6.1 + args: ["start-dev"] + env: + - name: KEYCLOAK_ADMIN + value: "admin" + - name: KEYCLOAK_ADMIN_PASSWORD + value: "admin" + - name: KC_PROXY + value: "edge" + ports: + - name: http + containerPort: 8080 + readinessProbe: + httpGet: + path: /realms/master + port: 8080 +EOF +---- + +For production use, follow https://www.keycloak.org/getting-started/getting-started-kube[Keycloak Kubernetes guide]. + +=== Deploy AMQ Broker to Kubernetes + +For a simple AMQ Broker deployment on Kubernetes: + +[source,shell] +---- +$ kubectl apply -f - <<EOF +apiVersion: v1 +kind: Service +metadata: + name: artemis-broker + labels: + app: artemis +spec: + ports: + - name: tcp + port: 61616 + targetPort: 61616 + - name: console + port: 8161 + targetPort: 8161 + selector: + app: artemis + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: artemis-broker + labels: + app: artemis +spec: + replicas: 1 + selector: + matchLabels: + app: artemis + template: + metadata: + labels: + app: artemis + spec: + containers: + - name: artemis + image: quay.io/arkmq-org/activemq-artemis-broker:artemis.2.52.0 + env: + - name: AMQ_USER + value: "admin" + - name: AMQ_PASSWORD + value: "admin" + - name: AMQ_ROLE + value: "admin" + ports: + - name: tcp + containerPort: 61616 + - name: console + containerPort: 8161 +EOF +---- + +For production use with operators, follow the https://artemiscloud.io/[ArtemisCloud Operator] documentation. + +=== Configure Keycloak on Kubernetes + +Use the same configuration as in <<external-instances-configuration>> 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=<your-registry> \ + -Dquarkus.container-image.group=<your-group> \ + -Dquarkus.container-image.push=true \ + -Dquarkus.kubernetes.namespace=<your-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":"[email protected]","username":"customer"},{"role":"admin-role","email":"[email protected]","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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<profiles version="8"> + <profile name="Camel Java Conventions" version="8" kind="CodeFormatterProfile"> + <setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/> + <setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/> + <setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/> + <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/> + <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/> + <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/> + <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/> + <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/> + <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/> + <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/> + <setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/> + <setting id="org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.comment.format_comments" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.comment.indent_return_description" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="120"/> + <setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/> + <setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/> + <setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.indentation.size" value="8"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/> + <setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.lineSplit" value="128"/> + <setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/> + <setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/> + <setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/> + <setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/> + <setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/> + <setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/> + <setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="CHECKSTYLE:OFF"/> + <setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="CHECKSTYLE:ON"/> + </profile> +</profiles> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <artifactId>camel-quarkus-examples-rest-keycloak-soap-jms</artifactId> + <groupId>org.apache.camel.quarkus.examples</groupId> + <version>3.37.0-SNAPSHOT</version> + + <name>Camel Quarkus :: Examples :: REST Keycloak SOAP JMS</name> + <description>Camel Quarkus Example :: REST to SOAP bridge with Keycloak security and JMS async events</description> + + <properties> + <quarkus.platform.version>3.36.1</quarkus.platform.version> + <camel-quarkus.platform.version>3.37.0-SNAPSHOT</camel-quarkus.platform.version> + + <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id> + <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id> + <camel-quarkus.platform.group-id>org.apache.camel.quarkus</camel-quarkus.platform.group-id> + <camel-quarkus.platform.artifact-id>camel-quarkus-bom</camel-quarkus.platform.artifact-id> + + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> + <maven.compiler.release>17</maven.compiler.release> + + <formatter-maven-plugin.version>2.29.0</formatter-maven-plugin.version> + <impsort-maven-plugin.version>1.13.0</impsort-maven-plugin.version> + <license-maven-plugin.version>5.0.0</license-maven-plugin.version> + <maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version> + <maven-jar-plugin.version>3.5.0</maven-jar-plugin.version> + <maven-resources-plugin.version>3.3.1</maven-resources-plugin.version> + <maven-surefire-plugin.version>3.5.6</maven-surefire-plugin.version> + + <quarkiverse-artemis.version>3.14.3</quarkiverse-artemis.version> + </properties> + + <dependencyManagement> + <dependencies> + <!-- Import BOM --> + <dependency> + <groupId>${quarkus.platform.group-id}</groupId> + <artifactId>${quarkus.platform.artifact-id}</artifactId> + <version>${quarkus.platform.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + <dependency> + <groupId>${camel-quarkus.platform.group-id}</groupId> + <artifactId>${camel-quarkus.platform.artifact-id}</artifactId> + <version>${camel-quarkus.platform.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <!-- Camel Core --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-direct</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-log</artifactId> + </dependency> + + <!-- REST endpoint --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-rest</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-jackson</artifactId> + </dependency> + + <!-- JMS/Artemis --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-jms</artifactId> + </dependency> + <dependency> + <groupId>io.quarkiverse.artemis</groupId> + <artifactId>quarkus-artemis-jms</artifactId> + <version>${quarkiverse-artemis.version}</version> + </dependency> + + <!-- workaround to fix jaxb deep scanning issue --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-support-jaxb</artifactId> + </dependency> + + <!-- SOAP/CXF --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-cxf-soap</artifactId> + </dependency> + + <!-- Bean processing --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-bean</artifactId> + </dependency> + + <!-- HTTP client for outbound calls --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-http</artifactId> + </dependency> + + <!-- Security/Keycloak --> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-oidc</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-keycloak</artifactId> + </dependency> + + <!-- Test dependencies --> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-junit5</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-test-keycloak-server</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <pluginManagement> + <plugins> + + <plugin> + <groupId>net.revelc.code.formatter</groupId> + <artifactId>formatter-maven-plugin</artifactId> + <version>${formatter-maven-plugin.version}</version> + <configuration> + <configFile>${maven.multiModuleProjectDirectory}/eclipse-formatter-config.xml</configFile> + <lineEnding>LF</lineEnding> + </configuration> + </plugin> + + <plugin> + <groupId>net.revelc.code</groupId> + <artifactId>impsort-maven-plugin</artifactId> + <version>${impsort-maven-plugin.version}</version> + <configuration> + <groups>java.,javax.,org.w3c.,org.xml.,junit.</groups> + <removeUnused>true</removeUnused> + <staticAfter>true</staticAfter> + <staticGroups>java.,javax.,org.w3c.,org.xml.,junit.</staticGroups> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>${maven-compiler-plugin.version}</version> + <configuration> + <showDeprecation>true</showDeprecation> + <showWarnings>true</showWarnings> + <compilerArgs> + <arg>-Xlint:unchecked</arg> + </compilerArgs> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>${maven-surefire-plugin.version}</version> + <configuration> + <failIfNoTests>false</failIfNoTests> + <systemPropertyVariables> + <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> + </systemPropertyVariables> + </configuration> + </plugin> + + <plugin> + <groupId>${quarkus.platform.group-id}</groupId> + <artifactId>quarkus-maven-plugin</artifactId> + <version>${quarkus.platform.version}</version> + <extensions>true</extensions> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + <version>${maven-surefire-plugin.version}</version> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>${maven-jar-plugin.version}</version> + </plugin> + + <plugin> + <groupId>com.mycila</groupId> + <artifactId>license-maven-plugin</artifactId> + <version>${license-maven-plugin.version}</version> + <configuration> + <failIfUnknown>true</failIfUnknown> + <header>${maven.multiModuleProjectDirectory}/header.txt</header> + <excludes> + <exclude>**/*.adoc</exclude> + <exclude>**/*.txt</exclude> + <exclude>**/LICENSE.txt</exclude> + <exclude>**/LICENSE</exclude> + <exclude>**/NOTICE.txt</exclude> + <exclude>**/NOTICE</exclude> + <exclude>**/README</exclude> + <exclude>**/pom.xml.versionsBackup</exclude> + <exclude>**/quarkus.log*</exclude> + <exclude>**/target/generated-sources/**</exclude> + </excludes> + <mapping> + <java>SLASHSTAR_STYLE</java> + <properties>CAMEL_PROPERTIES_STYLE</properties> + <wsdl>XML_STYLE</wsdl> + </mapping> + <headerDefinitions> + <headerDefinition>${maven.multiModuleProjectDirectory}/license-properties-headerdefinition.xml + </headerDefinition> + </headerDefinitions> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.cxf</groupId> + <artifactId>cxf-codegen-plugin</artifactId> + <version>4.0.5</version> + </plugin> + </plugins> + </pluginManagement> + + <plugins> + <plugin> + <groupId>org.apache.cxf</groupId> + <artifactId>cxf-codegen-plugin</artifactId> + <executions> + <execution> + <id>generate-sources</id> + <phase>generate-sources</phase> + <goals> + <goal>wsdl2java</goal> + </goals> + <configuration> + <wsdlOptions> + <wsdlOption> + <wsdl>${project.basedir}/src/main/resources/wsdl/InventoryService.wsdl</wsdl> + <packagenames> + <packagename>org.acme.inventory</packagename> + </packagenames> + </wsdlOption> + </wsdlOptions> + </configuration> + </execution> + </executions> + </plugin> + + <plugin> + <groupId>${quarkus.platform.group-id}</groupId> + <artifactId>quarkus-maven-plugin</artifactId> + <executions> + <execution> + <id>build</id> + <goals> + <goal>build</goal> + </goals> + </execution> + </executions> + </plugin> + + <plugin> + <groupId>net.revelc.code.formatter</groupId> + <artifactId>formatter-maven-plugin</artifactId> + <executions> + <execution> + <id>format</id> + <goals> + <goal>format</goal> + </goals> + <phase>process-sources</phase> + </execution> + </executions> + </plugin> + + <plugin> + <groupId>net.revelc.code</groupId> + <artifactId>impsort-maven-plugin</artifactId> + <executions> + <execution> + <id>sort-imports</id> + <goals> + <goal>sort</goal> + </goals> + <phase>process-sources</phase> + </execution> + </executions> + </plugin> + + <plugin> + <groupId>com.mycila</groupId> + <artifactId>license-maven-plugin</artifactId> + <executions> + <execution> + <id>license-format</id> + <goals> + <goal>format</goal> + </goals> + <phase>process-sources</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <profiles> + <profile> + <id>native</id> + <activation> + <property> + <name>native</name> + </property> + </activation> + <properties> + <quarkus.native.enabled>true</quarkus.native.enabled> + </properties> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>integration-test</goal> + <goal>verify</goal> + </goals> + <configuration> + <systemPropertyVariables> + <quarkus.native.enabled>${quarkus.native.enabled}</quarkus.native.enabled> + </systemPropertyVariables> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>kubernetes</id> + <activation> + <property> + <name>kubernetes</name> + </property> + </activation> + <dependencies> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-kubernetes</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-container-image-jib</artifactId> + </dependency> + </dependencies> + </profile> + <profile> + <id>openshift</id> + <activation> + <property> + <name>openshift</name> + </property> + </activation> + <dependencies> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-openshift</artifactId> + </dependency> + </dependencies> + </profile> + <profile> + <id>skip-testcontainers-tests</id> + <activation> + <property> + <name>skip-testcontainers-tests</name> + </property> + </activation> + <properties> + <skipTests>true</skipTests> + </properties> + </profile> + </profiles> + +</project> 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: <server-url>/realms/<realm-name>, " + + "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<Map<String, Object>> users = new ArrayList<>(); + + Map<String, Object> customer = new HashMap<>(); + customer.put("username", "customer"); + customer.put("email", "[email protected]"); + customer.put("role", "customer-role"); + users.add(customer); + + Map<String, Object> admin = new HashMap<>(); + admin.put("username", "admin"); + admin.put("email", "[email protected]"); + 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<String, Integer> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<definitions name="InventoryService" + targetNamespace="http://acme.org/inventory" + xmlns="http://schemas.xmlsoap.org/wsdl/" + xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" + xmlns:tns="http://acme.org/inventory" + xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + + <types> + <xsd:schema targetNamespace="http://acme.org/inventory"> + <xsd:element name="UpdateStockRequest"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="productId" type="xsd:string"/> + <xsd:element name="quantity" type="xsd:int"/> + </xsd:sequence> + </xsd:complexType> + </xsd:element> + <xsd:element name="UpdateStockResponse"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="success" type="xsd:boolean"/> + <xsd:element name="message" type="xsd:string"/> + <xsd:element name="newStock" type="xsd:int"/> + </xsd:sequence> + </xsd:complexType> + </xsd:element> + </xsd:schema> + </types> + + <message name="UpdateStockRequestMessage"> + <part name="parameters" element="tns:UpdateStockRequest"/> + </message> + <message name="UpdateStockResponseMessage"> + <part name="parameters" element="tns:UpdateStockResponse"/> + </message> + + <portType name="InventoryServicePortType"> + <operation name="updateStock"> + <input message="tns:UpdateStockRequestMessage"/> + <output message="tns:UpdateStockResponseMessage"/> + </operation> + </portType> + + <binding name="InventoryServiceSoapBinding" type="tns:InventoryServicePortType"> + <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> + <operation name="updateStock"> + <soap:operation soapAction=""/> + <input> + <soap:body use="literal"/> + </input> + <output> + <soap:body use="literal"/> + </output> + </operation> + </binding> + + <service name="InventoryService"> + <port name="InventoryServicePort" binding="tns:InventoryServiceSoapBinding"> + <soap:address location="http://localhost:8080/services/inventory"/> + </port> + </service> +</definitions> 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); + } +}
