This is an automated email from the ASF dual-hosted git repository.
Lukas-Finster pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new 8839dbeeb7 Implemented: Support for independent rest-apis
(OFBIZ-13440) (#1790)
8839dbeeb7 is described below
commit 8839dbeeb70fde1cc067726e76d8ebd28c074399
Author: Lukas-Finster <[email protected]>
AuthorDate: Thu Sep 17 16:00:34 2026 +0200
Implemented: Support for independent rest-apis (OFBIZ-13440) (#1790)
---
.../manufacturing/api/bom-management.rest.xml | 4 +-
.../manufacturing/api/mrp-planning.rest.xml | 4 +-
.../manufacturing/api/production-runs.rest.xml | 4 +-
.../manufacturing/api/routing-cost.rest.xml | 4 +-
...mpleApiDefinition.rest.xml => testApi.rest.xml} | 22 +++----
framework/rest-api/config/rest-api.properties | 3 +-
...Data.xml => RestSecurityPermissionDemoData.xml} | 12 ++--
.../data/RestSecurityPermissionSeedData.xml | 15 ++---
framework/rest-api/dtd/rest-api.xsd | 11 +++-
framework/rest-api/ofbiz-component.xml | 4 +-
framework/rest-api/src/docs/asciidoc/rest-api.adoc | 40 +++++++++----
.../apache/ofbiz/ws/rs/core/OFBizApiConfig.java | 55 ++++++++++-------
.../org/apache/ofbiz/ws/rs/model/ModelApi.java | 18 ++++--
.../apache/ofbiz/ws/rs/model/ModelApiReader.java | 2 +-
.../ofbiz/ws/rs/openapi/OFBizOpenApiReader.java | 69 ++++++++++++++++++----
.../ofbiz/ws/rs/openapi/OFBizOpenApiUiFilter.java | 53 +++++++++++++++++
.../ws/rs/resources/AuthenticationResource.java | 33 ++++++++---
.../ofbiz/ws/rs/resources/OpenApiResource.java | 18 ++++--
.../ofbiz/ws/rs/security/auth/APIAuthFilter.java | 16 ++++-
.../ws/rs/security/auth/HttpBasicAuthFilter.java | 36 +++++++----
.../org/apache/ofbiz/ws/rs/util/RestApiUtil.java | 13 ++++
.../ofbiz/ws/rs/core/OFBizApiConfigTest.java | 24 ++++----
.../ofbiz/ws/rs/model/ModelApiReaderTest.java | 4 +-
.../ws/rs/openapi/OFBizOpenApiReaderTest.java | 11 +++-
.../ofbiz/ws/rs/resources/OpenApiResourceTest.java | 2 +-
.../ofbiz/ws/rs/test/RestTestHttpRequest.java | 64 ++++++++++++++------
framework/rest-api/webapp/docs/WEB-INF/web.xml | 14 +++++
framework/rest-api/webapp/docs/swagger-ui.html | 21 +++++--
.../apache/ofbiz/webapp/control/JWTManager.java | 15 ++---
29 files changed, 423 insertions(+), 168 deletions(-)
diff --git a/applications/manufacturing/api/bom-management.rest.xml
b/applications/manufacturing/api/bom-management.rest.xml
index 6dc0e51c29..edcb2adbf4 100644
--- a/applications/manufacturing/api/bom-management.rest.xml
+++ b/applications/manufacturing/api/bom-management.rest.xml
@@ -19,11 +19,11 @@ under the License.
-->
<api name="Manufacturing BOM Management API"
- path="bom-management"
+ apiGroupPath="api"
displayName="BOM Management"
description="REST API for manufacturing bill of materials management"
publish="true">
- <resource name="BomManagement" path="" auth="true" publish="true">
+ <resource name="BomManagement" path="bom-management" auth="true"
publish="true">
<operation verb="get" produces="application/json" auth="true"
description="Find products with bill of material component
associations">
<service name="findBomProducts"/>
diff --git a/applications/manufacturing/api/mrp-planning.rest.xml
b/applications/manufacturing/api/mrp-planning.rest.xml
index cb29159eb6..f08ea69b6b 100644
--- a/applications/manufacturing/api/mrp-planning.rest.xml
+++ b/applications/manufacturing/api/mrp-planning.rest.xml
@@ -19,11 +19,11 @@ under the License.
-->
<api name="Manufacturing MRP Planning API"
- path="mrp-planning"
+ apiGroupPath="api"
displayName="MRP and Planning"
description="REST API for manufacturing MRP event plans and MRP runs"
publish="true">
- <resource name="MrpPlanning" path="" auth="true" publish="true">
+ <resource name="MrpPlanning" path="mrp-planning" auth="true"
publish="true">
<operation verb="get" path="/event-plans" produces="application/json"
auth="true"
description="Find product-centric MRP event plans">
<service name="findMrpEventPlans"/>
diff --git a/applications/manufacturing/api/production-runs.rest.xml
b/applications/manufacturing/api/production-runs.rest.xml
index 0fd9e8d223..4c2eddafd5 100644
--- a/applications/manufacturing/api/production-runs.rest.xml
+++ b/applications/manufacturing/api/production-runs.rest.xml
@@ -19,11 +19,11 @@ under the License.
-->
<api name="Manufacturing Production Run API"
- path="production-runs"
+ apiGroupPath="api"
displayName="Production Runs"
description="REST API for manufacturing production runs"
publish="true">
- <resource name="ProductionRuns" path="" auth="true" publish="true">
+ <resource name="ProductionRuns" path="production-runs" auth="true"
publish="true">
<operation verb="get" produces="application/json" auth="true"
description="Find production runs">
<service name="findProductionRuns"/>
diff --git a/applications/manufacturing/api/routing-cost.rest.xml
b/applications/manufacturing/api/routing-cost.rest.xml
index f3ab8274ba..643a748567 100644
--- a/applications/manufacturing/api/routing-cost.rest.xml
+++ b/applications/manufacturing/api/routing-cost.rest.xml
@@ -19,11 +19,11 @@ under the License.
-->
<api name="Manufacturing Routing and Cost API"
- path="routing-cost"
+ apiGroupPath="api"
displayName="Routing and Cost Management"
description="REST API for manufacturing routings, operations,
products, and operation setup data"
publish="true">
- <resource name="RoutingCost" path="" auth="true" publish="true">
+ <resource name="RoutingCost" path="routing-cost" auth="true"
publish="true">
<operation verb="get" produces="application/json" auth="true"
description="Find product routing templates">
<service name="findProductRoutings"/>
diff --git a/framework/rest-api/api/exampleApiDefinition.rest.xml
b/framework/rest-api/api/testApi.rest.xml
similarity index 82%
rename from framework/rest-api/api/exampleApiDefinition.rest.xml
rename to framework/rest-api/api/testApi.rest.xml
index 7d4266f748..66b0427999 100644
--- a/framework/rest-api/api/exampleApiDefinition.rest.xml
+++ b/framework/rest-api/api/testApi.rest.xml
@@ -18,14 +18,14 @@ specific language governing permissions and limitations
under the License.
-->
-<api name="ExampleApiDefinition"
- displayName="Example REST API"
- path="/exampleApi"
- description="This API exposes example product services as REST endpoints."
+<api name="TestApi"
+ displayName="REST Test API"
+ apiGroupPath="test-api"
+ description="This API exposes REST endpoints for testing purposes."
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/rest-api.xsd">
- <resource name="orders" path="/createRestOrderExample">
+ <resource name="example" path="/createRestOrderExample">
<operation verb="post" consumes="application/json"
produces="application/json">
<service name="createRestOrderExample"/>
<example type="response" code="200">
@@ -46,37 +46,37 @@ under the License.
<!-- The exposed endpoints are neccessary for tests defined in
RestTestHttpRequest.java to run.-->
<!-- If you decide to remove / not publish this rest.xml, those tests need
to be commented out. -->
- <resource name="orders" path="/returnSuccess">
+ <resource name="test" path="/returnSuccess">
<operation verb="post" consumes="application/json"
produces="application/json">
<service name="returnSuccess"/>
</operation>
</resource>
- <resource name="orders" path="/returnSuccessButOverwriteStatusCode">
+ <resource name="test" path="/returnSuccessButOverwriteStatusCode">
<operation verb="post" consumes="application/json"
produces="application/json">
<service name="returnSuccessButOverwriteStatusCode"/>
</operation>
</resource>
- <resource name="orders" path="/useCustomHeaderAsServiceParameter">
+ <resource name="test" path="/useCustomHeaderAsServiceParameter">
<operation verb="post" consumes="application/json"
produces="application/json" customHeaders="x-custom-header">
<service name="useCustomHeaderAsServiceParameter"/>
</operation>
</resource>
- <resource name="orders" path="/useLocaleSetInRequestHeader">
+ <resource name="test" path="/useLocaleSetInRequestHeader">
<operation verb="post" consumes="application/json"
produces="application/json">
<service name="useLocaleSetInRequestHeader"/>
</operation>
</resource>
- <resource name="orders"
path="/{myInput}/testServiceInputParametersAsPath/">
+ <resource name="test" path="/{myInput}/testServiceInputParametersAsPath/">
<operation verb="get" consumes="application/json"
produces="application/json">
<service name="testServiceInputParameters"/>
</operation>
</resource>
- <resource name="orders" path="/testServiceInputParametersAsQueryParam/">
+ <resource name="test" path="/testServiceInputParametersAsQueryParam/">
<operation verb="get" consumes="application/json"
produces="application/json">
<service name="testServiceInputParameters"/>
<queryParam name="myInput" type="string" description="example
input" />
diff --git a/framework/rest-api/config/rest-api.properties
b/framework/rest-api/config/rest-api.properties
index 4e8e4b5aaa..e354984aec 100644
--- a/framework/rest-api/config/rest-api.properties
+++ b/framework/rest-api/config/rest-api.properties
@@ -16,7 +16,8 @@
# specific language governing permissions and limitations
# under the License.
###############################################################################
-rest-api.auth.baseSecurityGroupPermission = REST-API_ADMIN
+rest-api.auth.api-group.api.baseSecurityGroupPermission = REST-API_ADMIN
+rest-api.auth.api-group.test-api.baseSecurityGroupPermission
REST-TEST-API_ADMIN
### Configures jackson serialization inclusion. Possible values are:
# ALWAYS,NON_NULL,NON_ABSENT,NON_EMPTY,NON_DEFAULT,CUSTOM,USE_DEFAULTS
diff --git a/framework/rest-api/data/RestSecurityPermissionSeedData.xml
b/framework/rest-api/data/RestSecurityPermissionDemoData.xml
similarity index 75%
copy from framework/rest-api/data/RestSecurityPermissionSeedData.xml
copy to framework/rest-api/data/RestSecurityPermissionDemoData.xml
index eeffa900c8..4ec86417b5 100644
--- a/framework/rest-api/data/RestSecurityPermissionSeedData.xml
+++ b/framework/rest-api/data/RestSecurityPermissionDemoData.xml
@@ -18,20 +18,16 @@
under the License.
-->
<entity-engine-xml>
-
- <!-- REST-API base permission -->
- <SecurityPermission description="REST-API base permission for basic auth."
permissionId="REST-API_ADMIN"/>
-
- <!-- REST-API security group and permission -->
- <SecurityGroup groupId="REST-API" groupName="REST-API group"
description="Group for accessing the REST-API"/>
- <SecurityGroupPermission fromDate="2026-01-01 12:00:00.0"
groupId="REST-API" permissionId="REST-API_ADMIN"/>
-
<!-- REST-API Test-User -->
<Party partyId="REST_API_TEST_USER" statusId="PARTY_ENABLED" />
<UserLogin userLoginId="REST_API_TEST_USER"
currentPassword="{SHA}47b56994cbc2b6d10aa1be30f70165adb305a41a"
partyId="REST_API_TEST_USER" hasLoggedOut="N" enabled="Y" />
+
+ <!-- TestUser -->
<UserLoginSecurityGroup groupId="REST-API"
userLoginId="REST_API_TEST_USER" fromDate="2026-01-01 12:00:00.0" />
+ <UserLoginSecurityGroup groupId="REST-TEST-API"
userLoginId="REST_API_TEST_USER" fromDate="2026-01-01 12:00:00.0" />
<!-- DemoAdmin -->
<UserLoginSecurityGroup groupId="REST-API" userLoginId="admin"
fromDate="2026-01-01 12:00:00.0" />
+ <UserLoginSecurityGroup groupId="REST-TEST-API" userLoginId="admin"
fromDate="2026-01-01 12:00:00.0" />
</entity-engine-xml>
\ No newline at end of file
diff --git a/framework/rest-api/data/RestSecurityPermissionSeedData.xml
b/framework/rest-api/data/RestSecurityPermissionSeedData.xml
index eeffa900c8..08d7e87446 100644
--- a/framework/rest-api/data/RestSecurityPermissionSeedData.xml
+++ b/framework/rest-api/data/RestSecurityPermissionSeedData.xml
@@ -19,19 +19,14 @@
-->
<entity-engine-xml>
- <!-- REST-API base permission -->
+ <!-- REST-API: "api" - security group and permission -->
<SecurityPermission description="REST-API base permission for basic auth."
permissionId="REST-API_ADMIN"/>
-
- <!-- REST-API security group and permission -->
<SecurityGroup groupId="REST-API" groupName="REST-API group"
description="Group for accessing the REST-API"/>
<SecurityGroupPermission fromDate="2026-01-01 12:00:00.0"
groupId="REST-API" permissionId="REST-API_ADMIN"/>
- <!-- REST-API Test-User -->
- <Party partyId="REST_API_TEST_USER" statusId="PARTY_ENABLED" />
- <UserLogin userLoginId="REST_API_TEST_USER"
currentPassword="{SHA}47b56994cbc2b6d10aa1be30f70165adb305a41a"
partyId="REST_API_TEST_USER" hasLoggedOut="N" enabled="Y" />
- <UserLoginSecurityGroup groupId="REST-API"
userLoginId="REST_API_TEST_USER" fromDate="2026-01-01 12:00:00.0" />
-
- <!-- DemoAdmin -->
- <UserLoginSecurityGroup groupId="REST-API" userLoginId="admin"
fromDate="2026-01-01 12:00:00.0" />
+ <!-- REST-APi: "test-api" - security group and permission -->
+ <SecurityPermission description="REST-API base permission for test-api"
permissionId="REST-TEST-API_ADMIN"/>
+ <SecurityGroup groupId="REST-TEST-API" groupName="REST-API group"
description="Group for accessing rest test-api"/>
+ <SecurityGroupPermission fromDate="2026-01-01 12:00:00.0"
groupId="REST-TEST-API" permissionId="REST-TEST-API_ADMIN"/>
</entity-engine-xml>
\ No newline at end of file
diff --git a/framework/rest-api/dtd/rest-api.xsd
b/framework/rest-api/dtd/rest-api.xsd
index de3fb320c8..1621ed4d21 100644
--- a/framework/rest-api/dtd/rest-api.xsd
+++ b/framework/rest-api/dtd/rest-api.xsd
@@ -26,7 +26,16 @@ under the License.
<xs:element minOccurs="0" maxOccurs="unbounded" ref="mapping"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
- <xs:attribute name="path" type="xs:string" use="required"/>
+ <xs:attribute name="apiGroupPath" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>
+ Defines a common path variable, that will be shared
among different APIs.
+ APIs belonging to the same group define the same
apiGroupPath.
+ The apiGroupName is the first path segment after the
mountpoint.
+ If unset, will default to api.
+ </xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
<xs:attribute name="displayName" type="xs:string"/>
<xs:attribute name="description" type="xs:string"/>
<xs:attribute name="publish" type="xs:boolean" default="true"/>
diff --git a/framework/rest-api/ofbiz-component.xml
b/framework/rest-api/ofbiz-component.xml
index 3b89960bcc..2a12948e0a 100644
--- a/framework/rest-api/ofbiz-component.xml
+++ b/framework/rest-api/ofbiz-component.xml
@@ -30,7 +30,9 @@ under the License.
<classpath type="dir" location="dtd"/>
<!-- entity resources: model(s), eca(s), group, and data definitions -->
- <entity-resource type="data" reader-name="demo" loader="main"
location="data/RestSecurityPermissionSeedData.xml"/>
+ <entity-resource type="data" reader-name="seed" loader="main"
location="data/RestSecurityPermissionSeedData.xml"/>
+
+ <entity-resource type="data" reader-name="demo" loader="main"
location="data/RestSecurityPermissionDemoData.xml"/>
<service-resource type="model" loader="main"
location="servicedef/services.xml"/>
diff --git a/framework/rest-api/src/docs/asciidoc/rest-api.adoc
b/framework/rest-api/src/docs/asciidoc/rest-api.adoc
index 97a34530e4..c11cafee2b 100644
--- a/framework/rest-api/src/docs/asciidoc/rest-api.adoc
+++ b/framework/rest-api/src/docs/asciidoc/rest-api.adoc
@@ -8,21 +8,26 @@ paths and HTTP verbs to OFBiz services.
== Important URLs
* API https://localhost:8443/rest
* WADL https://localhost:8443/rest/application.wadl
-* OpenAPI docs https://localhost:8443/docs/swagger-ui.html
+* OpenAPI docs https://localhost:8443/docs/<api-group-path>/swagger-ui.html
+Note: SwaggerUi expects an apiGroupPath as a path parameter. Visiting swaggerUi
+without specifying an apiGroup (.../docs/swagger-ui.html or .../docs/) will
forward to
+to OFBiz framework apis (.../docs/api/swagger-ui.html)
== Declaring endpoints
Create a file ending in `.rest.xml` in the `api` directory of a component
-(see `framework/rest-api/api/exampleApiDefinition.rest.xml` for a working
example
+(see `framework/rest-api/api/testApi.rest.xml` for a working example
and `framework/rest-api/dtd/rest-api.xsd` for the schema):
[source, xml]
----
-<api name="ExampleApiDefinition"
- displayName="Example REST API"
- path="/exampleApi"
- description="This API exposes example product services as REST
endpoints.">
+<api name="TestApi"
+ displayName="REST Test API"
+ apiGroupPath="test-api"
+ description="This API exposes REST endpoints for testing purposes."
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+
xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/rest-api.xsd">
- <resource name="orders" path="/createRestOrderExample">
+ <resource name="example" path="/createRestOrderExample">
<operation verb="post" consumes="application/json"
produces="application/json">
<service name="createRestOrderExample"/>
</operation>
@@ -31,8 +36,11 @@ and `framework/rest-api/dtd/rest-api.xsd` for the schema):
</api>
----
-Once deployed, each declared operation is available under the API path, e.g.
-`POST /rest/exampleApi/createRestOrderExample`, and is documented in the
generated
+Once deployed, each declared operation is available under the API path, with
apiGroupPath
+defining the first path segment after the mountpoint. Additional path segments
are set via
+path attribute on child resource elements. For the example above we have a
single child path element,
+resulting in our endpoint beeing deployed at
.../<mountpoint>/<apiGroupPath>/<path>,
+e.g. `POST /rest/test-api/createRestOrderExample`, and is documented in the
generated
OpenAPI specification. Operations declared with `auth="true"` require a valid
JWT
(see below).
@@ -40,9 +48,14 @@ OpenAPI specification. Operations declared with
`auth="true"` require a valid JW
API is protected by JWT tokens, although other forms of access tokens may be
supported in future.
An API client must first needs to authenticate itself using Basic Auth using
username and password.
Username is nothing but OFBiz 'userLogin'. Token can also be generated using
Swagger UI.
+
+Authorization works on a per-api-group basis. Api groups are defined as sets
of one or more apis sharing their
+first path segment, defined via xml as apiGroupPath. When creating an auth
token, the apiGroupPath common to
+that group needs to be send as a path parameter (e.g.
https://localhost:8443/rest/auth/<apiGroupPath>/token)
+The default api group for OFBiz native apis is "api".
[source, json]
----
-curl -X POST "https://localhost:8443/rest/auth/token" -H "accept:
application/json" -H "Authorization: Basic YWRtaW46b2ZiaXo="
+curl -X POST "https://localhost:8443/rest/auth/test-api/token" -H "accept:
application/json" -H "Authorization: Basic YWRtaW46b2ZiaXo="
----
If successfully validated, generated token should be received in response
[source, json]
@@ -52,7 +65,8 @@ If successfully validated, generated token should be received
in response
"statusDescription": "OK",
"successMessage": "Token granted.",
"data": {
- "access_token":
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJ1c2VyTG9naW5JZCI6ImFkbWluIiwiaXNzIjoiQXBhY2hlT0ZCaXoiLCJleHAiOjE1OTY3MDk4MjAsImlhdCI6MTU5NjcwODAyMH0.9Hj4pkkeQowAMxPLrI_To0WTohxxgVR6FoViyx5HoboTACQZ4iqDyqiIBodkuCVsZwOTPT1RSAQJ0L_oSVMqBA",
+ "access_token":
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3ODc4MjIxMzEsImV4cCI6MTc4NzgyMzkzMSwiaXNzIjoiQXBhY2hlT0ZCaXoiLCJ1c2VyTG9naW5JZCI6ImFkbWluIiwiYXBpR3JvdXBQYXRoIjoiYXBpIn0.t4ns8p2_sE8A4xlLBAzLy1Jv1kLPbnZMA6YmsSq_1rb_ISG0gUwyA67mFdL8kbbIk9TGP8drZHyCr6JzRPjQcg",
+ "refresh_token":
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3ODc4MjIxMzEsImV4cCI6MTc4Nzg1MDkzMSwiaXNzIjoiQXBhY2hlT0ZCaXoiLCJ1c2VyTG9naW5JZCI6ImFkbWluIiwiYXBpR3JvdXBQYXRoIjoiYXBpIiwidHlwZSI6InJlZnJlc2gifQ.tZxL_62perZaHpQu4GVM1dVj8kx4Q84j6O8IZaDxLl-_TCvqDv7OpuO5K3xa1NURY0_knj1BJl5ocglTyTg5lw",
"token_type": "Bearer",
"expires_in": "1800"
}
@@ -63,7 +77,7 @@ Subsequent API calls must use received token using Bearer
Authentication scheme
[source, json]
----
-POST /rest/exampleApi/createRestOrderExample HTTP/1.1
+POST /rest/test-api/createRestOrderExample HTTP/1.1
Authorization: Bearer
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJ1c2VyTG9naW5JZCI6ImFkbWluIiwiaXNzIjoiQXBhY2hlT0ZCaXoiLCJleHAiOjE1OTY3MDk4MjAsImlhdCI6MTU5NjcwODAyMH0.9Hj4pkkeQowAMxPLrI_To0WTohxxgVR6FoViyx5HoboTACQZ4iqDyqiIBodkuCVsZwOTPT1RSAQJ0L_oSVMqBA
----
@@ -114,7 +128,7 @@ Response :
* Call OFBiz Service via POST +
[source, json]
----
-POST rest/exampleApi/createRestOrderExample HTTP/1.1
+POST rest/test-api/createRestOrderExample HTTP/1.1
Content-Type: application/json
Authorization: Bearer
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJBcGFjaGVPRkJpeiIsImlhdCI6MTU0NzczOTM0OCwiZXhwIjoxNjc5Mjc1MzQ4LCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiR2l2ZW5OYW1lIjoiSm9obm55IiwiU3VybmFtZSI6IlJvY2tldCIsIkVtYWlsIjoianJvY2tldEBleGFtcGxlLmNvbSIsInVzZXJMb2dpbklkIjoiYWRtaW4iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.fwafgrgpodBJcXxNTQdZknKeWKb3sDOsQrcR2vcRw97FznD6mkE79p10Tu7cqpUx7LiXuROUAnXEgqDice-BSg
{
diff --git
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfig.java
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfig.java
index 6f3a4bf328..4a07488133 100644
---
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfig.java
+++
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfig.java
@@ -19,9 +19,13 @@
package org.apache.ofbiz.ws.rs.core;
import java.io.File;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -51,7 +55,7 @@ import jakarta.ws.rs.core.MediaType;
public class OFBizApiConfig extends ResourceConfig {
private static final String MODULE = OFBizApiConfig.class.getName();
- private static final Map<String, ModelApi> MICRO_APIS = new HashMap<>();
+ private static final Map<String, List<ModelApi>> MICRO_APIS = new
HashMap<>();
/**
* Configures the OFBiz JAX-RS application by registering built-in resource
@@ -87,7 +91,7 @@ public class OFBizApiConfig extends ResourceConfig {
*
* @return registered API definitions
*/
- public static Map<String, ModelApi> getModelApis() {
+ public static Map<String, List<ModelApi>> getModelApis() {
return MICRO_APIS;
}
@@ -113,17 +117,12 @@ public class OFBizApiConfig extends ResourceConfig {
for (File restXmlFile : restXmlFiles) {
ModelApi api = ModelApiReader.getModelApi(restXmlFile);
if (!api.isPublish()) {
- Debug.logInfo("API %s[%s] is declared non-publish,
ignoring...", api.getName(), api.getPath(), MODULE);
+ Debug.logInfo("API %s[%s] is declared non-publish,
ignoring...", api.getName(), api.getApiGroupPath(), MODULE);
continue;
}
- String path = api.getPath();
- if (MICRO_APIS.containsKey(path)) {
- Debug.logWarning("Duplicate REST API definition for
path: %s at: %s, overriding component: %s",
- path, restXmlFile, componentName, MODULE);
- } else {
- Debug.logInfo("Processing REST API path: %s from
component: %s", path, componentName, MODULE);
- }
- MICRO_APIS.put(path, api);
+ String path = api.getApiGroupPath();
+ Debug.logInfo("Adding API %s to group: %s from component:
%s", api.getName(), path, componentName, MODULE);
+ MICRO_APIS.computeIfAbsent(path, k -> new
ArrayList<>()).add(api);
}
} catch (ComponentException | RuntimeException e) {
Debug.logError(e, MODULE);
@@ -137,15 +136,20 @@ public class OFBizApiConfig extends ResourceConfig {
return;
}
- MICRO_APIS.forEach((apiPath, modelApi) -> {
- Debug.logInfo("Registering Resource Definitions from API - " +
apiPath, MODULE);
- for (ModelResource resource : modelApi.getResources()) {
- String resourcePath = buildCleanPath(apiPath,
resource.getPath());
- registerModelResource(resource, resourcePath);
+ MICRO_APIS.forEach((groupPath, apis) -> {
+ Set<String> registeredSignatures = new HashSet<>();
+ for (ModelApi modelApi : apis) {
+ Debug.logInfo("Registering Resource Definitions from API - " +
modelApi.getName()
+ + " in group: " + groupPath, MODULE);
+ for (ModelResource resource : modelApi.getResources()) {
+ String resourcePath = buildCleanPath(groupPath,
resource.getPath());
+ registerModelResource(resource, resourcePath,
registeredSignatures, modelApi.getName());
+ }
}
});
}
- private void registerModelResource(ModelResource modelResource, String
basePath) {
+ private void registerModelResource(ModelResource modelResource, String
basePath,
+ Set<String> registeredSignatures, String owningApiName) {
if (!modelResource.isPublish()) return;
Resource.Builder resourceBuilder = Resource.builder("/" + basePath)
@@ -153,11 +157,20 @@ public class OFBizApiConfig extends ResourceConfig {
for (ModelOperation op : modelResource.getOperations()) {
String serviceName = op.getService();
- ServiceRequestHandler requestHandler = new
ServiceRequestHandler(serviceName, op.getPrimaryPermission(),
op.getMainAction());
-
String verb = op.getVerb().toUpperCase();
- boolean isOtherThanGet = verb.matches(HttpMethod.POST + "|" +
HttpMethod.PUT + "|" + HttpMethod.PATCH);
String opPath = op.getPath();
+ String fullPath = UtilValidate.isEmpty(opPath) ? basePath :
buildCleanPath(basePath, opPath);
+ String signature = verb + " " + fullPath;
+
+ // Fail fast if api groups define identical endpoints
+ if (!registeredSignatures.add(signature)) {
+ throw new IllegalStateException("REST path collision in group
'" + basePath
+ + "': " + signature + " is already registered by
another API"
+ + " (conflict while processing " + owningApiName +
")");
+ }
+
+ ServiceRequestHandler requestHandler = new
ServiceRequestHandler(serviceName, op.getPrimaryPermission(),
op.getMainAction());
+ boolean isOtherThanGet = verb.matches(HttpMethod.POST + "|" +
HttpMethod.PUT + "|" + HttpMethod.PATCH);
ResourceMethod.Builder methodBuilder;
if (UtilValidate.isEmpty(opPath)) {
@@ -184,7 +197,7 @@ public class OFBizApiConfig extends ResourceConfig {
if (UtilValidate.isNotEmpty(modelResource.getSubResources())) {
for (ModelResource sub : modelResource.getSubResources()) {
String subPath = buildCleanPath(basePath, sub.getPath());
- registerModelResource(sub, subPath);
+ registerModelResource(sub, subPath, registeredSignatures,
owningApiName);
}
}
}
diff --git
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java
index b3341c26be..963987fcd0 100644
---
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java
+++
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java
@@ -21,12 +21,15 @@ package org.apache.ofbiz.ws.rs.model;
import java.util.ArrayList;
import java.util.List;
+import org.apache.ofbiz.base.util.UtilValidate;
+
public class ModelApi {
+ private final String defaultApiGroupPath = "api";
private List<ModelResource> resources;
private List<ModelMapping> mappings;
private String name;
- private String path;
+ private String apiGroupPath;
private String displayName;
private String description;
private boolean publish;
@@ -97,8 +100,11 @@ public class ModelApi {
*
* @return the path value
*/
- public String getPath() {
- return path;
+ public String getApiGroupPath() {
+ if (UtilValidate.isEmpty(apiGroupPath)) {
+ apiGroupPath = defaultApiGroupPath;
+ }
+ return apiGroupPath;
}
/**
@@ -113,10 +119,10 @@ public class ModelApi {
/**
* Sets the path.
*
- * @param path the path value to set
+ * @param apiGroupPath the path value to set
*/
- public void setPath(String path) {
- this.path = path;
+ public void setApiGroupPath(String apiGroupPath) {
+ this.apiGroupPath = apiGroupPath;
}
/**
diff --git
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
index bec41a8739..6674c7b437 100644
---
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
+++
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
@@ -60,7 +60,7 @@ public final class ModelApiReader {
api.setDisplayName(UtilXml.checkEmpty(docElement.getAttribute("displayName")).intern());
api.setName(UtilXml.checkEmpty(docElement.getAttribute("name")).intern());
api.setDescription(UtilXml.checkEmpty(docElement.getAttribute("description")).intern());
-
api.setPath(UtilXml.checkEmpty(docElement.getAttribute("path")).intern());
+
api.setApiGroupPath(UtilXml.checkEmpty(docElement.getAttribute("apiGroupPath")).intern());
api.setPublish(parseBooleanDefaultTrue(docElement.getAttribute("publish")));
for (Element resourceEle : UtilXml.childElementList(docElement,
"resource")) {
createModelResource(resourceEle, api);
diff --git
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
index 3f0a6568ae..6e578f3edf 100644
---
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
+++
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
@@ -72,6 +72,7 @@ import jakarta.ws.rs.core.Response.StatusType;
public final class OFBizOpenApiReader extends Reader implements OpenApiReader {
private static final String MODULE = OFBizOpenApiReader.class.getName();
+
private Components components;
private Paths paths;
@SuppressWarnings("rawtypes")
@@ -98,28 +99,35 @@ public final class OFBizOpenApiReader extends Reader
implements OpenApiReader {
initializeStdOpenApiComponents();
addPredefinedSchemas();
addApiResources();
+ injectApiGroupPathIntoExamples();
openApi.setPaths(paths);
openApi.setComponents(components);
return openApi;
}
private void addApiResources() {
- Map<String, ModelApi> apis = OFBizApiConfig.getModelApis();
+ Map<String, Object> userDefinedOptions =
config.getUserDefinedOptions();
+ String apiGroupPath = userDefinedOptions != null ? (String)
userDefinedOptions.get("apiGroupPath") : null;
+
+ Map<String, List<ModelApi>> apis = OFBizApiConfig.getModelApis();
SecurityRequirement security = new SecurityRequirement();
security.addList("jwtToken");
- apis.forEach((k, api) -> {
- if (!api.isPublish()) return;
+ apis.forEach((k, apiList) -> {
+ for (ModelApi api : apiList) {
+ if (!api.isPublish()) continue;
+ if (UtilValidate.isEmpty(apiGroupPath) ||
!k.equals(apiGroupPath)) continue;
- List<ModelMapping> mappings = api.getMappings();
- mappings.forEach(modelMapping -> {
- OpenApiUtil.getListTypes().put(modelMapping.getName(),
modelMapping.getClassName());
- });
- List<String> baseSegments = new ArrayList<>();
- baseSegments.add(api.getPath());
+ List<ModelMapping> mappings = api.getMappings();
+ mappings.forEach(modelMapping -> {
+ OpenApiUtil.getListTypes().put(modelMapping.getName(),
modelMapping.getClassName());
+ });
+ List<String> baseSegments = new ArrayList<>();
+ baseSegments.add(api.getApiGroupPath());
- for (ModelResource resource : api.getResources()) {
- processResourceRecursive(resource, baseSegments, security);
+ for (ModelResource resource : api.getResources()) {
+ processResourceRecursive(resource, baseSegments, security);
+ }
}
});
}
@@ -367,4 +375,43 @@ public final class OFBizOpenApiReader extends Reader
implements OpenApiReader {
operation.addParametersItem(new
HeaderParameter().name(headerName).schema(new StringSchema()).required(true));
});
}
+
+ /**
+ * Injects the current apiGroupPath into the example for any given endpoint
+ */
+ private void injectApiGroupPathIntoExamples() {
+ Map<String, Object> userDefinedOptions =
config.getUserDefinedOptions();
+ String apiGroupPath = userDefinedOptions != null ? (String)
userDefinedOptions.get("apiGroupPath") : null;
+
+ if (paths == null || paths.isEmpty() || apiGroupPath == null ||
apiGroupPath.isEmpty()) {
+ return;
+ }
+
+ for (Map.Entry<String, PathItem> pathEntry : paths.entrySet()) {
+ PathItem pathItem = pathEntry.getValue();
+ List<Operation> operations = pathItem.readOperations();
+ if (operations == null || operations.isEmpty()) {
+ continue;
+ }
+
+ for (Operation operation : operations) {
+ List<Parameter> parameters = operation.getParameters();
+ if (parameters == null || parameters.isEmpty()) {
+ continue;
+ }
+
+ for (Parameter param : parameters) {
+ String paramName = param.getName();
+ if (!"apiGroupPath".equals(paramName)) {
+ continue;
+ }
+ param.setExample(apiGroupPath);
+ Schema<?> schema = param.getSchema();
+ if (schema != null) {
+ schema.setExample(apiGroupPath);
+ }
+ }
+ }
+ }
+ }
}
diff --git
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiUiFilter.java
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiUiFilter.java
new file mode 100644
index 0000000000..14f9cb48f6
--- /dev/null
+++
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiUiFilter.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * 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.apache.ofbiz.ws.rs.openapi;
+
+import java.io.IOException;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class OFBizOpenApiUiFilter implements Filter {
+
+ @Override
+ public void doFilter(ServletRequest servletRequest,
+ ServletResponse servletResponse,
+ FilterChain chain)
+ throws IOException, ServletException {
+
+ HttpServletRequest request = (HttpServletRequest) servletRequest;
+ HttpServletResponse response = (HttpServletResponse) servletResponse;
+
+ String path =
request.getRequestURI().substring(request.getContextPath().length());
+
+ // /orderApi/swagger-ui.html
+ if (path.matches("/[^/]+/swagger-ui\\.html")) {
+ request.getRequestDispatcher("/swagger-ui.html")
+ .forward(request, response);
+ return;
+ }
+
+ chain.doFilter(request, response);
+ }
+}
diff --git
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/resources/AuthenticationResource.java
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/resources/AuthenticationResource.java
index 5fa0c35d8d..8a69d7c9a4 100644
---
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/resources/AuthenticationResource.java
+++
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/resources/AuthenticationResource.java
@@ -48,6 +48,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
@@ -96,7 +97,7 @@ public class AuthenticationResource {
*/
@POST
@Produces(MediaType.APPLICATION_JSON)
- @Path("/token")
+ @Path("/{apiGroupPath}/token")
@AuthToken
@Operation(security = @SecurityRequirement(name = "basicAuth"),
operationId = "getAuthToken",
description = "Generates JWT token for subsequent API calls.",
@@ -113,9 +114,15 @@ public class AuthenticationResource {
@ApiResponse(responseCode = "403", description =
"Forbidden.",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{
\"statusCode\" : 403, \"statusDescription\" : \"Forbidden\", "
- + "\"errorMessage\" : \"Access Denied: User not
found.\" }")))
+ + "\"errorMessage\" : \"Access Denied: The
specified user has no base permission to use api: [api]\" }")))
})
- public Response getAuthToken(@Parameter(in = ParameterIn.HEADER, name =
"Authorization",
+ public Response getAuthToken(
+ @Parameter(in = ParameterIn.PATH, name = "apiGroupPath",
+ description = "The common path parameter identifying this API
group."
+ + "The token issued will be valid exclusively for APIs
belonging to this group",
+ example = "api")
+ @PathParam("apiGroupPath") String apiGroupPath,
+ @Parameter(in = ParameterIn.HEADER, name = "Authorization",
description = "Authorization header using Basic Authentication",
example = HttpHeaders.AUTHORIZATION + ": Basic YWRtaW46b2ZiaXo=")
@HeaderParam(HttpHeaders.AUTHORIZATION) String creds) {
Delegator delegator = (Delegator)
servletContext.getAttribute("delegator");
@@ -123,9 +130,10 @@ public class AuthenticationResource {
httpRequest.setAttribute("dispatcher",
servletContext.getAttribute("dispatcher"));
GenericValue userLogin = (GenericValue)
httpRequest.getAttribute("userLogin");
//TODO : Move this into an OFBiz service. All such implementations
should be inside an OFBiz service.
- String jwtToken = JWTManager.createJwt(delegator,
- UtilMisc.toMap("userLoginId",
userLogin.getString("userLoginId")));
- String refreshToken = JWTManager.createRefreshToken(delegator,
userLogin.getString("userLoginId"));
+ Map<String, String> claims = UtilMisc.toMap("userLoginId",
userLogin.getString("userLoginId"),
+ "apiGroupPath", apiGroupPath);
+ String jwtToken = JWTManager.createJwt(delegator, claims);
+ String refreshToken = JWTManager.createRefreshToken(delegator, claims);
Map<String, Object> tokenPayload = UtilMisc.toMap("access_token",
jwtToken, "refresh_token", refreshToken,
"expires_in",
EntityUtilProperties.getPropertyValue("security",
"security.jwt.token.expireTime", "1800", delegator),
@@ -146,7 +154,8 @@ public class AuthenticationResource {
@POST
@Produces(MediaType.APPLICATION_JSON)
@Path("/refresh-token")
- @Operation(description = "Generates a new access token using a refresh
token.")
+ @Operation(description = "Generates a new access token using a refresh
token. The token issued will be"
+ + " valid exclusivly for the same apiGroup as the provided token")
public Response refreshToken(@HeaderParam("Refresh-Token") String
refreshToken) {
Delegator delegator = (Delegator)
servletContext.getAttribute("delegator");
@@ -159,6 +168,10 @@ public class AuthenticationResource {
"Unauthorized: " + claims.get(ModelService.ERROR_MESSAGE));
}
+ if (!claims.containsKey("apiGroupPath")) {
+ return
RestApiUtil.error(Response.Status.UNAUTHORIZED.getStatusCode(),
Response.Status.UNAUTHORIZED.getReasonPhrase(),
+ "Unauthorized: RefreshToken does not specify api");
+ }
String userLoginId = (String) claims.get("userLoginId");
GenericValue userLogin = getActiveUserLogin(delegator, userLoginId);
if (userLogin == null) {
@@ -166,8 +179,10 @@ public class AuthenticationResource {
"Unauthorized: Invalid refresh token.");
}
- String newAccessToken = JWTManager.createJwt(delegator,
UtilMisc.toMap("userLoginId", userLoginId));
- String newRefreshToken = JWTManager.createRefreshToken(delegator,
userLoginId);
+ Map<String, String> newClaims = UtilMisc.toMap("userLoginId",
userLoginId,
+ "apiGroupPath", claims.get("apiGroupPath"));
+ String newAccessToken = JWTManager.createJwt(delegator, newClaims);
+ String newRefreshToken = JWTManager.createRefreshToken(delegator,
newClaims);
Map<String, Object> tokenPayload = UtilMisc.toMap("access_token",
newAccessToken, "refresh_token", newRefreshToken, "expires_in",
EntityUtilProperties.getPropertyValue("security",
"security.jwt.token.expireTime", "1800", delegator), "token_type", "Bearer");
diff --git
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/resources/OpenApiResource.java
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/resources/OpenApiResource.java
index 1ad95f3560..864590d574 100644
---
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/resources/OpenApiResource.java
+++
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/resources/OpenApiResource.java
@@ -18,9 +18,11 @@
*******************************************************************************/
package org.apache.ofbiz.ws.rs.resources;
+import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.apache.ofbiz.base.util.UtilMisc;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.ws.rs.openapi.OFBizOpenApiReader;
@@ -52,7 +54,7 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
-@Path("/openapi.{type:json|yaml}")
+@Path("/{apiGroupPath}/openapi.{type:json|yaml}")
public final class OpenApiResource {
@Context
private ServletConfig config;
@@ -94,7 +96,8 @@ public final class OpenApiResource {
@GET
@Produces({MediaType.APPLICATION_JSON, "application/yaml"})
@Operation(hidden = true)
- public Response getOpenApi(@Context HttpHeaders headers, @Context UriInfo
uriInfo, @PathParam("type") String type)
+ public Response getOpenApi(@Context HttpHeaders headers, @Context UriInfo
uriInfo, @PathParam("type") String type,
+ @PathParam("apiGroupPath") String apiGroupPath)
throws Exception {
OpenAPI openApi = new OpenAPI();
openApi.addServersItem(buildOpenApiServer());
@@ -112,14 +115,19 @@ public final class OpenApiResource {
basicAuthScheme.setScheme("basic");
openApi.schemaRequirement(basicAuthScheme.getName(), basicAuthScheme);
+ Map<String, Object> userDefinedOptions =
UtilMisc.toMap("apiGroupPath", apiGroupPath);
- SwaggerConfiguration config = new
SwaggerConfiguration().openAPI(openApi.info(buildOpenApiInfo()))
+ SwaggerConfiguration config = new SwaggerConfiguration()
+ .openAPI(openApi.info(buildOpenApiInfo()))
.readerClass(OFBizOpenApiReader.class.getName())
.resourcePackages(Stream.of("org.apache.ofbiz.ws.rs.resources").collect(Collectors.toSet()))
- .scannerClass(OFBizResourceScanner.class.getName());
+ .scannerClass(OFBizResourceScanner.class.getName())
+ .userDefinedOptions(userDefinedOptions);
- OpenApiContext ctx = new
GenericOpenApiContextBuilder<>().openApiConfiguration(config)
+ OpenApiContext ctx = new GenericOpenApiContextBuilder<>()
+ .openApiConfiguration(config)
+ .ctxId(apiGroupPath)
.buildContext(true);
openApi = ctx.read();
diff --git
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/security/auth/APIAuthFilter.java
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/security/auth/APIAuthFilter.java
index abbaefafb3..7620350c9c 100644
---
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/security/auth/APIAuthFilter.java
+++
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/security/auth/APIAuthFilter.java
@@ -83,10 +83,15 @@ public class APIAuthFilter implements
ContainerRequestFilter {
Map<String, Object> claims = JWTManager.validateAccessToken(delegator,
jwtToken);
if (claims.containsKey(ModelService.ERROR_MESSAGE)) {
abortWithUnauthorized(requestContext, true, "Unauthorized: " +
(String) claims.get(ModelService.ERROR_MESSAGE));
- } else {
- GenericValue userLogin = extractUserLoginFromJwtClaim(delegator,
claims);
- httpRequest.setAttribute("userLogin", userLogin);
+ return;
}
+ if (!isTokenIssuedForRequestedApi(requestContext, claims)) {
+ abortWithUnauthorized(requestContext, false, "Unauthorized: Access
is denied due to the token"
+ + " presented beeing issued for a different api");
+ return;
+ }
+ GenericValue userLogin = extractUserLoginFromJwtClaim(delegator,
claims);
+ httpRequest.setAttribute("userLogin", userLogin);
}
private boolean isTokenBasedAuthentication(String authorizationHeader) {
@@ -125,4 +130,9 @@ public class APIAuthFilter implements
ContainerRequestFilter {
return userLogin;
}
+ private boolean isTokenIssuedForRequestedApi(ContainerRequestContext
requestContext, Map<String, Object> claims) {
+ String currentApiGroupPath =
requestContext.getUriInfo().getPathSegments().get(0).getPath();
+ String issuedApiGroupPath = (String)
claims.getOrDefault("apiGroupPath", "");
+ return currentApiGroupPath.equals(issuedApiGroupPath);
+ }
}
diff --git
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/security/auth/HttpBasicAuthFilter.java
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/security/auth/HttpBasicAuthFilter.java
index 9de425f6e8..68f558ec24 100644
---
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/security/auth/HttpBasicAuthFilter.java
+++
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/security/auth/HttpBasicAuthFilter.java
@@ -87,8 +87,14 @@ public class HttpBasicAuthFilter implements
ContainerRequestFilter {
String[] tokens = (new
String(Base64.getDecoder().decode(authorizationHeader.split(" ")[1]),
"UTF-8")).split(":");
final String username = tokens[0];
final String password = tokens[1];
+
+ String apiGroupPath =
requestContext.getUriInfo().getPathParameters().getFirst("apiGroupPath");
+ String validatedApiGroupPath =
RestApiUtil.validateApiGroupPath(apiGroupPath);
+ if (UtilValidate.isEmpty(validatedApiGroupPath)) {
+ abortWithUnauthorized(requestContext, true, "Access Denied:
Unknown api.");
+ }
try {
- authenticate(username, password);
+ authenticate(username, password, validatedApiGroupPath);
} catch (ForbiddenException fe) {
abortWithUnauthorized(requestContext, true, "Access Denied: " +
fe.getMessage());
}
@@ -111,7 +117,7 @@ public class HttpBasicAuthFilter implements
ContainerRequestFilter {
}
}
- private void authenticate(String userName, String password) throws
ForbiddenException {
+ private void authenticate(String userName, String password, String
validatedApiGroupPath) throws ForbiddenException {
Map<String, Object> result = null;
LocalDispatcher dispatcher = (LocalDispatcher)
servletContext.getAttribute("dispatcher");
try {
@@ -126,19 +132,29 @@ public class HttpBasicAuthFilter implements
ContainerRequestFilter {
throw new ForbiddenException(ServiceUtil.getErrorMessage(result));
}
- String securityGroupPermission =
EntityUtilProperties.getPropertyValue("rest-api",
"rest-api.auth.baseSecurityGroupPermission",
- (Delegator) servletContext.getAttribute("delegator"));
- if (UtilValidate.isNotEmpty(securityGroupPermission)) {
- Security security = (Security)
servletContext.getAttribute("security");
- if (security != null &&
!security.hasPermission(securityGroupPermission, (GenericValue)
result.get("userLogin"))) {
- Debug.logInfo("The specified user has no base permission to
use the REST-API.", MODULE);
- throw new
ForbiddenException(ServiceUtil.getErrorMessage(result));
- }
+ String securityGroupPermission =
determineSecurityGroupPermission(validatedApiGroupPath);
+
+ if (UtilValidate.isEmpty(securityGroupPermission)) {
+ String errorMsg = String.format("No SecurityGroup configured for
api: [%s]. Access denied.", validatedApiGroupPath);
+ Debug.logInfo(errorMsg, MODULE);
+ throw new ForbiddenException(errorMsg);
}
+ Security security = (Security) servletContext.getAttribute("security");
+ if (security != null &&
!security.hasPermission(securityGroupPermission, (GenericValue)
result.get("userLogin"))) {
+ String errorMsg = String.format("The specified user has no base
permission to use api: [%s].", validatedApiGroupPath);
+ Debug.logInfo(errorMsg, MODULE);
+ throw new ForbiddenException(errorMsg);
+ }
GenericValue userLogin = (GenericValue) result.get("userLogin");
httpRequest.setAttribute("userLogin", userLogin);
}
+ private String determineSecurityGroupPermission(String
validatedApiGroupPath) {
+ String keyPrefix = "rest-api.auth.api-group.";
+ String keyPostfix = ".baseSecurityGroupPermission";
+ String fullKey = keyPrefix + validatedApiGroupPath + keyPostfix;
+ return EntityUtilProperties.getPropertyValue("rest-api", fullKey,
(Delegator) servletContext.getAttribute("delegator"));
+ }
}
diff --git
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java
index cb8c6600c6..a2894fa8c0 100644
---
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java
+++
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java
@@ -41,6 +41,7 @@ import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.base.util.collections.MapComparator;
import org.apache.ofbiz.service.ModelService;
+import org.apache.ofbiz.ws.rs.core.OFBizApiConfig;
import org.apache.ofbiz.ws.rs.core.ResponseStatus;
import org.apache.ofbiz.ws.rs.response.Error;
import org.apache.ofbiz.ws.rs.response.Success;
@@ -780,4 +781,16 @@ public final class RestApiUtil {
}
return statusType;
}
+
+ public static String validateApiGroupPath(String providedApiGroupPath) {
+ if (providedApiGroupPath == null) {
+ return null;
+ }
+ for (String knownApiGroupPath :
OFBizApiConfig.getModelApis().keySet()) {
+ if (knownApiGroupPath.equals(providedApiGroupPath)) {
+ return knownApiGroupPath;
+ }
+ }
+ return null;
+ }
}
diff --git
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfigTest.java
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfigTest.java
index 2e9dd22b26..940dbafd2f 100644
---
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfigTest.java
+++
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/core/OFBizApiConfigTest.java
@@ -22,21 +22,27 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import java.lang.reflect.Method;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
class OFBizApiConfigTest {
+ private static OFBizApiConfig ofbizApiConfig;
+
+ @BeforeAll
+ public static void initOFBizApiConfig() {
+ ofbizApiConfig = new OFBizApiConfig();
+ }
+
@Test
void testBuildCleanPathRemovesDuplicateSlashes() throws Exception {
- OFBizApiConfig config = new OFBizApiConfig();
-
Method method = OFBizApiConfig.class.getDeclaredMethod(
"buildCleanPath",
String[].class);
method.setAccessible(true);
String path = (String) method.invoke(
- config,
+ ofbizApiConfig,
(Object) new String[] {
"/party/",
"/customers/",
@@ -48,15 +54,13 @@ class OFBizApiConfigTest {
@Test
void testBuildCleanPathIgnoresNullParts() throws Exception {
- OFBizApiConfig config = new OFBizApiConfig();
-
Method method = OFBizApiConfig.class.getDeclaredMethod(
"buildCleanPath",
String[].class);
method.setAccessible(true);
String path = (String) method.invoke(
- config,
+ ofbizApiConfig,
(Object) new String[] {
null,
"",
@@ -69,15 +73,13 @@ class OFBizApiConfigTest {
@Test
void testBuildCleanPathReturnsEmptyStringWhenNoValidParts() throws
Exception {
- OFBizApiConfig config = new OFBizApiConfig();
-
Method method = OFBizApiConfig.class.getDeclaredMethod(
"buildCleanPath",
String[].class);
method.setAccessible(true);
String path = (String) method.invoke(
- config,
+ ofbizApiConfig,
(Object) new String[] {
"",
null,
@@ -89,15 +91,13 @@ class OFBizApiConfigTest {
@Test
void testBuildCleanPathWithSinglePart() throws Exception {
- OFBizApiConfig config = new OFBizApiConfig();
-
Method method = OFBizApiConfig.class.getDeclaredMethod(
"buildCleanPath",
String[].class);
method.setAccessible(true);
String path = (String) method.invoke(
- config,
+ ofbizApiConfig,
(Object) new String[] {
"/services/"
});
diff --git
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/model/ModelApiReaderTest.java
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/model/ModelApiReaderTest.java
index 6d21ed7e6f..b0502d7468 100644
---
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/model/ModelApiReaderTest.java
+++
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/model/ModelApiReaderTest.java
@@ -53,7 +53,7 @@ class ModelApiReaderTest {
@Test
void testGetModelApiParsesTopLevelAttributes() throws IOException {
File file = writeXml("""
- <api displayName="My API" name="myApi" description="desc"
path="/api" publish="true"/>
+ <api displayName="My API" name="myApi" description="desc"
apiGroupPath="/api" publish="true"/>
""");
ModelApi api = ModelApiReader.getModelApi(file);
@@ -61,7 +61,7 @@ class ModelApiReaderTest {
assertEquals("My API", api.getDisplayName());
assertEquals("myApi", api.getName());
assertEquals("desc", api.getDescription());
- assertEquals("/api", api.getPath());
+ assertEquals("/api", api.getApiGroupPath());
assertTrue(api.isPublish());
}
diff --git
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReaderTest.java
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReaderTest.java
index 8fadbb25ae..6cf5851cc9 100644
---
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReaderTest.java
+++
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReaderTest.java
@@ -39,6 +39,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
+import io.swagger.v3.oas.integration.SwaggerConfiguration;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
@@ -153,11 +154,13 @@ class OFBizOpenApiReaderTest {
ModelApi api = mock(ModelApi.class);
when(api.isPublish()).thenReturn(true);
- when(api.getPath()).thenReturn("api");
+ when(api.getApiGroupPath()).thenReturn("api");
when(api.getResources()).thenReturn(List.of(resource));
+ List<ModelApi> apiGroup = new ArrayList<>();
+ apiGroup.add(api);
apiMock.when(OFBizApiConfig::getModelApis)
- .thenReturn(Map.of("test", api));
+ .thenReturn(Map.of("test", apiGroup));
org.apache.ofbiz.service.ModelService service =
mock(org.apache.ofbiz.service.ModelService.class);
@@ -165,6 +168,10 @@ class OFBizOpenApiReaderTest {
when(service.getInModelParamList()).thenReturn(List.of());
when(dctx.getModelService("testService")).thenReturn(service);
+ SwaggerConfiguration config = new SwaggerConfiguration();
+ config.setUserDefinedOptions(Map.of("apiGroupPath", "test"));
+ reader.setConfiguration(config);
+
return reader.read(Set.of(), Map.of());
}
}
diff --git
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/resources/OpenApiResourceTest.java
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/resources/OpenApiResourceTest.java
index 83c4d43ac0..729becf5ac 100644
---
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/resources/OpenApiResourceTest.java
+++
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/resources/OpenApiResourceTest.java
@@ -65,7 +65,7 @@ class OpenApiResourceTest {
Mockito.mockConstruction(OFBizOpenApiReader.class, (mock,
context) -> { });
MockedConstruction<OFBizResourceScanner> scannerMock =
Mockito.mockConstruction(OFBizResourceScanner.class)) {
- Response response = resource.getOpenApi(mock(HttpHeaders.class),
mock(UriInfo.class), type);
+ Response response = resource.getOpenApi(mock(HttpHeaders.class),
mock(UriInfo.class), type, "api");
assertEquals(200, response.getStatus());
assertEquals(expectedType, response.getMediaType().toString());
diff --git
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestHttpRequest.java
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestHttpRequest.java
index b43013054a..f570448950 100644
---
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestHttpRequest.java
+++
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestHttpRequest.java
@@ -70,14 +70,18 @@ class RestTestHttpRequest implements JupiterTestHelper {
return http;
}
- private static HttpClient createAuthorizedClient(String path) {
+ private static HttpClient createAuthorizedClient(String path, String
token) {
HttpClient client = initHttpClient();
client.setHeader("Content-Type", "application/json");
- client.setHeader("Authorization", "Bearer " + accessToken);
+ client.setHeader("Authorization", "Bearer " + token);
client.setUrl(BASE_URL + path);
return client;
}
+ private static HttpClient createAuthorizedClient(String path) {
+ return createAuthorizedClient(path, accessToken);
+ }
+
private static JsonNode requestForJson(HttpClient client, String
errorContext, String verb) {
String response = "";
try {
@@ -141,15 +145,18 @@ class RestTestHttpRequest implements JupiterTestHelper {
}
}
- private static JsonNode requestAuthToken(String username, String password)
{
+ private static JsonNode requestAuthToken(String username, String password,
String apiGroupPath) {
String creds = Base64.getEncoder().encodeToString((username + ":" +
password).getBytes(StandardCharsets.UTF_8));
HttpClient client = initHttpClient();
- client.setUrl(BASE_URL + "/auth/token");
+ client.setUrl(BASE_URL + "/auth/" + apiGroupPath + "/token");
client.setHeader("Authorization", "Basic " + creds);
client.setHeader("Accept", "application/json");
+ return requestForJson(client, "/auth/" + apiGroupPath + "/token (" +
username + ")", HttpMethod.POST);
- return requestForJson(client, "/auth/token (" + username + ")",
HttpMethod.POST);
+ }
+ private static JsonNode requestAuthToken(String username, String password)
{
+ return requestAuthToken(username, password, "test-api");
}
private static JsonNode requestAuthTokenAsAdmin() {
@@ -245,43 +252,62 @@ class RestTestHttpRequest implements JupiterTestHelper {
assertFalse(statusCode == 200, "Invalid credentials should not return
a success status code");
}
+ @Test
+ @Order(9)
+ void testGenerateAuthTokenRejectsUnknownApiGroup() {
+ JsonNode root = requestAuthToken("admin", "ofbiz", "non-existing-api");
+ int statusCode = root.path("statusCode").asInt(200);
+ assertFalse(statusCode == 200, "Invalid apiGroupPath should be
rejected");
+ }
+
+ @Test
+ @Order(10)
+ void testTokenForDifferentApiGetsRejected() {
+ // Create token for apiGroup "api"
+ JsonNode authRoot = requestAuthToken("admin", "ofbiz", "api");
+ String apiAccessToken =
authRoot.path("data").path("access_token").asText();
+ // Request to apiGroup "test-api"
+ HttpClient client = createAuthorizedClient("/test-api/returnSuccess",
apiAccessToken);
+ JsonNode root = requestForJson(client, "Reject access token from
different api.", HttpMethod.POST);
+ assertTrue(401 == root.path("statusCode").asInt());
+ }
/* ========= generall tests - these rely on the Endpoints
specified via exampleApiDefinition.rest.xml ============ */
@Test
void returnSuccessReturnsExpectedStatusCode() throws Exception {
- HttpClient client =
createAuthorizedClient("/exampleApi/returnSuccess");
- JsonNode root = requestForJson(client, "/exampleApi/returnSuccess",
HttpMethod.POST);
+ HttpClient client = createAuthorizedClient("/test-api/returnSuccess");
+ JsonNode root = requestForJson(client, "/test-api/returnSuccess",
HttpMethod.POST);
assertEquals(200, root.path("statusCode").asInt());
}
// Corresponding service overwrites statusCode with 201
@Test
void returnSuccessOverwriteStatusCode() throws Exception {
- HttpClient client =
createAuthorizedClient("/exampleApi/returnSuccessButOverwriteStatusCode");
- JsonNode root = requestForJson(client,
"/exampleApi/returnSuccessButOverwriteStatusCode", HttpMethod.POST);
+ HttpClient client =
createAuthorizedClient("/test-api/returnSuccessButOverwriteStatusCode");
+ JsonNode root = requestForJson(client,
"/test-api/returnSuccessButOverwriteStatusCode", HttpMethod.POST);
assertEquals(201, root.path("statusCode").asInt());
}
@Test
void useCustomHeaderAsServiceParameter() throws Exception {
- HttpClient client =
createAuthorizedClient("/exampleApi/useCustomHeaderAsServiceParameter");
+ HttpClient client =
createAuthorizedClient("/test-api/useCustomHeaderAsServiceParameter");
client.setHeader("x-custom-header", "Foo");
- JsonNode root = requestForJson(client,
"/exampleApi/useCustomHeaderAsServiceParameter", HttpMethod.POST);
+ JsonNode root = requestForJson(client,
"/test-api/useCustomHeaderAsServiceParameter", HttpMethod.POST);
assertEquals("Foo",
root.path("data").path("x-custom-header").asText());
}
@Test
void testUseLocaleSetInRequestHeader() throws Exception {
- HttpClient client =
createAuthorizedClient("/exampleApi/useLocaleSetInRequestHeader");
+ HttpClient client =
createAuthorizedClient("/test-api/useLocaleSetInRequestHeader");
client.setHeader("Accept-Language", "fr");
- JsonNode root = requestForJson(client,
"/exampleApi/useLocaleSetInRequestHeader", HttpMethod.POST);
+ JsonNode root = requestForJson(client,
"/test-api/useLocaleSetInRequestHeader", HttpMethod.POST);
assertEquals("fr", root.path("data").path("localeAsString").asText());
}
@Test
void testGZipCompression() throws Exception {
- HttpClient client =
createAuthorizedClient("/exampleApi/returnSuccess");
+ HttpClient client = createAuthorizedClient("/test-api/returnSuccess");
client.setHeader("Accept-Encoding", "gzip");
byte[] rawBytes = postRawBytes(client);
@@ -296,7 +322,7 @@ class RestTestHttpRequest implements JupiterTestHelper {
@Test
void testDeflateCompression() throws Exception {
- HttpClient client =
createAuthorizedClient("/exampleApi/returnSuccess");
+ HttpClient client = createAuthorizedClient("/test-api/returnSuccess");
client.setHeader("Accept-Encoding", "deflate");
byte[] rawBytes = postRawBytes(client);
@@ -307,18 +333,18 @@ class RestTestHttpRequest implements JupiterTestHelper {
@Test
void testServiceInputParametersAsPath() throws Exception {
- HttpClient client =
createAuthorizedClient("/exampleApi/foo/testServiceInputParametersAsPath/");
+ HttpClient client =
createAuthorizedClient("/test-api/foo/testServiceInputParametersAsPath/");
- JsonNode root = requestForJson(client,
"/exampleApi/foo/testServiceInputParametersAsPath/", HttpMethod.GET);
+ JsonNode root = requestForJson(client,
"/test-api/foo/testServiceInputParametersAsPath/", HttpMethod.GET);
assertEquals("foo", root.path("data").path("myInput").asText());
}
@Test
void testServiceInputParametersAsQueryParam() throws Exception {
- HttpClient client =
createAuthorizedClient("/exampleApi/testServiceInputParametersAsQueryParam/");
+ HttpClient client =
createAuthorizedClient("/test-api/testServiceInputParametersAsQueryParam/");
client.setParameter("myInput", "foo");
- JsonNode root = requestForJson(client,
"/exampleApi/testServiceInputParametersAsQueryParam/", HttpMethod.GET);
+ JsonNode root = requestForJson(client,
"/test-api/testServiceInputParametersAsQueryParam/", HttpMethod.GET);
assertEquals("foo", root.path("data").path("myInput").asText());
}
}
diff --git a/framework/rest-api/webapp/docs/WEB-INF/web.xml
b/framework/rest-api/webapp/docs/WEB-INF/web.xml
index d5a9404e62..dc2b671a82 100644
--- a/framework/rest-api/webapp/docs/WEB-INF/web.xml
+++ b/framework/rest-api/webapp/docs/WEB-INF/web.xml
@@ -21,6 +21,20 @@ under the License.
<web-app version="4.0" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd">
<display-name>Apache OFBiz - REST API Swagger Component</display-name>
<description>REST API Swagger Component of the Apache OFBiz
Project</description>
+
+
+ <filter>
+ <filter-name>swaggerUiFilter</filter-name>
+ <filter-class>
+ org.apache.ofbiz.ws.rs.openapi.OFBizOpenApiUiFilter
+ </filter-class>
+ </filter>
+
+ <filter-mapping>
+ <filter-name>swaggerUiFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+
<welcome-file-list>
<welcome-file>swagger-ui.html</welcome-file>
</welcome-file-list>
diff --git a/framework/rest-api/webapp/docs/swagger-ui.html
b/framework/rest-api/webapp/docs/swagger-ui.html
index 800db725e9..50a5411053 100644
--- a/framework/rest-api/webapp/docs/swagger-ui.html
+++ b/framework/rest-api/webapp/docs/swagger-ui.html
@@ -22,9 +22,9 @@ under the License.
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
- <link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
- <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32"
/>
- <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16"
/>
+ <link rel="stylesheet" type="text/css" href="/docs/swagger-ui.css" >
+ <link rel="icon" type="image/png" href="/docs/favicon-32x32.png"
sizes="32x32" />
+ <link rel="icon" type="image/png" href="/docs/favicon-16x16.png"
sizes="16x16" />
<style>
html
{
@@ -51,16 +51,25 @@ under the License.
<body>
<div id="swagger-ui"></div>
- <script src="./swagger-ui-bundle.js"> </script>
- <script src="./swagger-ui-standalone-preset.js"> </script>
+ <script src="/docs/swagger-ui-bundle.js"> </script>
+ <script src="/docs/swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
if (!window.location.origin) {
window.location.origin = window.location.protocol + "//" +
window.location.hostname + (window.location.port ? ':' + window.location.port:
'');
}
+
+ const pathSegments = window.location.pathname.split('/').filter(Boolean);
+
+ const docsIndex = pathSegments.indexOf('docs');
+
+ const apiName = pathSegments.length >= 3 ?
pathSegments[pathSegments.length - 2] : null;
+ const apiGroupPath = apiName ? encodeURIComponent(apiName) + "/" :
"api/";
+ //const apiGroupPath = apiName ? encodeURIComponent(apiName) + "/" : "";
+
const ui = SwaggerUIBundle({
- url: window.location.origin + "/rest/openapi.json",
+ url: window.location.origin + "/rest/" + apiGroupPath +
"openapi.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
diff --git
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/JWTManager.java
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/JWTManager.java
index 2a61465f8f..843942c0dd 100644
---
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/JWTManager.java
+++
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/JWTManager.java
@@ -28,11 +28,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
-import jakarta.servlet.ServletContext;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.ws.rs.core.HttpHeaders;
-
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.StringUtil;
import org.apache.ofbiz.base.util.UtilDateTime;
@@ -63,6 +58,11 @@ import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.ws.rs.core.HttpHeaders;
+
/**
* This class manages the single sign-on authentication through JWT tokens
between OFBiz applications.
@@ -482,10 +482,11 @@ public class JWTManager {
return result;
}
- public static String createRefreshToken(Delegator delegator, String
userLoginId) {
+ public static String createRefreshToken(Delegator delegator, Map<String,
String> claims) {
int refreshTokenExpireTime =
Integer.parseInt(EntityUtilProperties.getPropertyValue("security",
"security.jwt.refresh.token.expireTime", "86400", delegator));
- return createJwt(delegator, UtilMisc.toMap("userLoginId", userLoginId,
"type", "refresh"), refreshTokenExpireTime);
+ claims.put("type", "refresh");
+ return createJwt(delegator, claims, refreshTokenExpireTime);
}
public static Map<String, Object> validateRefreshToken(Delegator
delegator, String refreshToken) {