Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -74,10 +75,16 @@ public class MergeProcessorImpl implements MergeProcessor {

private static final Info MERGED_INFO;

private static final Set<String> EXTENSIONS_NOT_CHEKCED_FOR_DUPLICATES = new HashSet<>();

static {
MERGED_INFO = OASFactory.createInfo();
MERGED_INFO.setTitle(Constants.MERGED_OPENAPI_DOC_TITLE);
MERGED_INFO.setVersion(Constants.DEFAULT_OPENAPI_DOC_VERSION);

//if there are merge conflicts in x-ibm-zcon-roles-allowed they will be handled in the second pass
// Documentation for x-ibm-zcon-roles-allowed is at https://www.ibm.com/docs/en/zos-connect/3.0.0?topic=authorization-how-define-roles
EXTENSIONS_NOT_CHEKCED_FOR_DUPLICATES.add("x-ibm-zcon-roles-allowed");
}

@Reference
Expand Down Expand Up @@ -197,6 +204,7 @@ public void process() {
}
}

boolean zConRolesIdentical = isZConRolesAllowedIdentical(inProgressModels);
boolean securityIdentical = isSecurityIdentical(inProgressModels);
boolean infoIdentical = isInfoIdentical(inProgressModels);
boolean externalDocsIdentical = isExternalDocsIdentical(inProgressModels);
Expand Down Expand Up @@ -239,6 +247,10 @@ public void process() {
moveServersUnderPaths(inProgress.model);
}

if (!zConRolesIdentical) {
moveZConRolesAllow(inProgress.model);
}

if (inProgress.documentNameProcessor.hasRenames()) {
if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) {
Tr.event(this, tc, "Updating references to renamed elements");
Expand Down Expand Up @@ -341,6 +353,11 @@ private boolean findAndRecordExtensionClashes(OpenAPI model, OpenAPIProvider pro
OpenAPIProvider otherProvider = entry.getKey();
Map<String, Object> otherExtensions = entry.getValue();
for (Entry<String, Object> extensionEntry : extensions.entrySet()) {

if (EXTENSIONS_NOT_CHEKCED_FOR_DUPLICATES.contains(extensionEntry.getKey())) {
continue;
}

String key = extensionEntry.getKey();
Object value = extensionEntry.getValue();
Object otherValue = otherExtensions.get(key);
Expand Down Expand Up @@ -437,6 +454,48 @@ private void moveSecurityRequirements(OpenAPI document) {

document.setSecurity(null);
}

//See https://www.ibm.com/docs/en/zos-connect/3.0.0?topic=authorization-how-define-roles for details on x-ibm-zcon-roles-allowed
//Like security, it can be defined on the top level or on specific operations
private void moveZConRolesAllow(OpenAPI document) {

final String MAP_KEY = "x-ibm-zcon-roles-allowed";

Map<String, Object> oldExtensions = document.getExtensions();
if (oldExtensions == null || oldExtensions.isEmpty() || !oldExtensions.containsKey(MAP_KEY)) {
return;
}

Map<String, Object> extensions = new HashMap<>(document.getExtensions());

if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) {
Tr.event(this, tc, "Moving ibm-zcon-roles-allowed from the top level to under paths");
}

Paths paths = document.getPaths();
if (paths == null) {
return;
}

for (PathItem item : notNull(paths.getPathItems()).values()) {
for (Operation op : notNull(item.getOperations()).values()) {
if (op.getExtensions() == null) {
//Hopefully OpenAPI can handle the same map being passed in
//to multiple operations, but why risk it?
Map<String, Object> zConMap = new HashMap<>();
zConMap.put(MAP_KEY, extensions.get(MAP_KEY));
op.setExtensions(zConMap);
} else {
Map<String, Object> localExtenionsMap = new HashMap<>(op.getExtensions());
localExtenionsMap.putIfAbsent(MAP_KEY, extensions.get(MAP_KEY)); //A more specific setting will override the default
op.setExtensions(localExtenionsMap);
}
}
}

extensions.remove(MAP_KEY);
document.setExtensions(extensions);
}
}

/**
Expand Down Expand Up @@ -618,6 +677,12 @@ private static boolean serverEndsWithContextRoot(Server server, String contextRo
return server.getUrl().endsWith(contextRoot) || server.getUrl().endsWith(contextRoot + "/");
}

private static boolean isZConRolesAllowedIdentical(List<InProgressModel> models) {
List<Object> zconExtensions = models.stream().map(d -> d.model.getExtensions())
.filter(Objects::nonNull).map(e -> e.get("x-ibm-zcon-roles-allowed")).filter(Objects::nonNull).collect(toList());
return zconExtensions.isEmpty() || (models.size() == zconExtensions.size() && allEqual(zconExtensions, ModelEquality::equals));
}

private static boolean isSecurityIdentical(List<InProgressModel> models) {
List<List<SecurityRequirement>> reqs = models.stream().map(d -> d.model.getSecurity()).collect(toList());
return allEqual(reqs, ModelEquality::equals);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ tested.features=\
io.openliberty.org.eclipse.microprofile.openapi.2.0;version=latest,\
io.openliberty.org.eclipse.microprofile.config.2.0;version=latest,\
com.ibm.ws.kernel.service;version=latest, \
io.openliberty.com.fasterxml.jackson;version=latest
io.openliberty.com.fasterxml.jackson;version=latest, \
com.ibm.ws.org.apache.commons.lang3;version=latest
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
addRequiredLibraries.dependsOn addJakartaTransformer

dependencies {
requiredLibs project(path: ':io.openliberty.org.eclipse.microprofile', configuration: 'openapi20')
requiredLibs project(path: ':io.openliberty.org.eclipse.microprofile', configuration: 'openapi20'), project(':com.ibm.ws.org.apache.commons.lang3')
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
* - Scenarios involving context root, host/port, servers
* - Make a pure JAX-RS app with the ApplicationPath annotation and ensure that the annotations are scanned and a document is generated
* - Complete flow: model, static, annotation, filter in order
*
* Most of these are tested in com.ibm.ws.microprofile.openapi_fat
*/
@RunWith(FATRunner.class)
public class ApplicationProcessorTest extends FATServletClient {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import io.openliberty.microprofile.openapi20.fat.deployments.MergeTest;
import io.openliberty.microprofile.openapi20.fat.deployments.MergeWithServletTest;
import io.openliberty.microprofile.openapi20.fat.deployments.StartupWarningMessagesTest;
import io.openliberty.microprofile.openapi20.fat.deployments.ZOSConnectExtensionTest;
import io.openliberty.microprofile.openapi20.fat.shutdown.ShutdownTest;
import io.openliberty.microprofile.openapi20.fat.version.OpenAPIVersionTest;

Expand All @@ -41,7 +42,8 @@
MergeWithServletTest.class,
OpenAPIVersionTest.class,
ShutdownTest.class,
StartupWarningMessagesTest.class
StartupWarningMessagesTest.class,
ZOSConnectExtensionTest.class
})
public class FATSuite {
public static RepeatTests repeatDefault(String serverName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*******************************************************************************
* Copyright (c) 2025 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package io.openliberty.microprofile.openapi20.fat.deployments;

import static com.ibm.websphere.simplicity.ShrinkHelper.DeployOptions.SERVER_ONLY;
import static org.junit.Assert.assertEquals;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;

import com.fasterxml.jackson.databind.JsonNode;
import com.ibm.websphere.simplicity.PropertiesAsset;
import com.ibm.websphere.simplicity.ShrinkHelper;
import com.ibm.websphere.simplicity.config.MpOpenAPIElement;
import com.ibm.websphere.simplicity.config.MpOpenAPIInfoElement;
import com.ibm.websphere.simplicity.config.ServerConfiguration;

import componenttest.annotation.Server;
import componenttest.custom.junit.runner.FATRunner;
import componenttest.custom.junit.runner.Mode;
import componenttest.custom.junit.runner.Mode.TestMode;
import componenttest.rules.repeater.RepeatTests;
import componenttest.topology.impl.LibertyFileManager;
import componenttest.topology.impl.LibertyServer;
import io.openliberty.microprofile.openapi20.fat.FATSuite;
import io.openliberty.microprofile.openapi20.fat.deployments.zosconnect.app.ZOSConnectTestApp;
import io.openliberty.microprofile.openapi20.fat.deployments.zosconnect.app.ZOSConnectTestResource;
import io.openliberty.microprofile.openapi20.fat.utils.OpenAPIConnection;
import io.openliberty.microprofile.openapi20.fat.utils.OpenAPITestUtil;

@RunWith(FATRunner.class)
@Mode(TestMode.FULL)
public class ZOSConnectExtensionTest {

private static final String SERVER_NAME = "OpenAPIMergeWithServerXMLTestServer";

@Server(SERVER_NAME)
public static LibertyServer server;

@ClassRule
public static RepeatTests r = FATSuite.repeatDefault(SERVER_NAME);

@BeforeClass
public static void startup() throws Exception {
server.saveServerConfiguration();
server.startServer();
}

@AfterClass
public static void shutdown() throws Exception {
server.stopServer("CWWKO1683W", // Invalid info element
"CWWKO1678W", // Invalid application name
"CWWKO1679W" // Invalid module name
);
}

@After
public void cleanup() throws Exception {
server.setMarkToEndOfLog();
server.restoreServerConfiguration(); // Will stop all apps deployed via server.xml and clear openapi config
server.waitForConfigUpdateInLogUsingMark(null);

server.deleteAllDropinApplications(); // Will stop all dropin apps
server.removeAllInstalledAppsForValidation(); // Validates that all apps stop

// Delete everything from the apps directory
server.deleteDirectoryFromLibertyServerRoot("apps");
LibertyFileManager.createRemoteFile(server.getMachine(), server.getServerRoot() + "/apps").mkdir();
}

//This test creates an openAPI doc with a map containing values from two apps, and checks they preserve their ordering
@Test
public void testMergePreservesMapOrdering() throws Exception {
setMergeConfig(list("test1", "test2"), null, null);

PropertiesAsset scanConfig = new PropertiesAsset().addProperty("mp.openapi.scan.disable",
"true");

WebArchive war1 = ShrinkWrap.create(WebArchive.class, "test1.war")
.addClasses(ZOSConnectTestApp.class, ZOSConnectTestResource.class)
.addAsResource(scanConfig, "META-INF/microprofile-config.properties")
.addAsManifestResource(ZOSConnectTestApp.class.getPackage(), "static-file-foo.json", "openapi.json");
deployApp(war1);

WebArchive war2 = ShrinkWrap.create(WebArchive.class, "test2.war")
.addClasses(ZOSConnectTestApp.class, ZOSConnectTestResource.class)
.addAsResource(scanConfig, "META-INF/microprofile-config.properties")
.addAsManifestResource(ZOSConnectTestApp.class.getPackage(), "static-file-bar.json", "openapi.json");
deployApp(war2);

// check that documentation includes all paths in the right order
String doc = OpenAPIConnection.openAPIDocsConnection(server, false).download();
JsonNode openapiNode = OpenAPITestUtil.readYamlTree(doc).get("paths");

List<String> pathNames = new ArrayList<>();
openapiNode.fieldNames().forEachRemaining(pathNames::add);

assertEquals("There should be exactly six instances of [x-ibm-zcon-roles-allowed] (one per operation) in " + doc,
6, StringUtils.countMatches(openapiNode.toPrettyString(), "x-ibm-zcon-roles-allowed"));

assertEquals("Bobs", openapiNode.path("/test1/foo1").path("get").path("x-ibm-zcon-roles-allowed").get(0).asText());
assertEquals("Bobs", openapiNode.path("/test1/foo2").path("get").path("x-ibm-zcon-roles-allowed").get(0).asText());
assertEquals("Robs", openapiNode.path("/test1/foo3").path("get").path("x-ibm-zcon-roles-allowed").get(0).asText());
assertEquals("Staff", openapiNode.path("/test2/bar1").path("get").path("x-ibm-zcon-roles-allowed").get(0).asText());
assertEquals("Staff", openapiNode.path("/test2/bar2").path("get").path("x-ibm-zcon-roles-allowed").get(0).asText());
assertEquals("Guests", openapiNode.path("/test2/bar3").path("get").path("x-ibm-zcon-roles-allowed").get(0).asText());

}

private void setMergeConfig(List<String> included, List<String> excluded, MpOpenAPIInfoElement info) throws Exception {
ServerConfiguration config = server.getServerConfiguration();
MpOpenAPIElement mpOpenAPI = config.getMpOpenAPIElement();

clearMergeConfig(mpOpenAPI);

mpOpenAPI.getIncludedApplications().addAll(applications(included));
mpOpenAPI.getIncludedModules().addAll(modules(included));
mpOpenAPI.getExcludedApplications().addAll(applications(excluded));
mpOpenAPI.getExcludedModules().addAll(modules(excluded));

mpOpenAPI.setInfo(info);

server.setMarkToEndOfLog();
server.updateServerConfiguration(config);
server.waitForConfigUpdateInLogUsingMark(null);
}

private static void clearMergeConfig(MpOpenAPIElement mpOpenAPI) {
List<String> includedApplications = mpOpenAPI.getIncludedApplications();
includedApplications.clear();

List<String> includedModules = mpOpenAPI.getIncludedModules();
includedModules.clear();

List<String> excludedApplications = mpOpenAPI.getExcludedApplications();
excludedApplications.clear();

List<String> excludedModules = mpOpenAPI.getExcludedModules();
excludedModules.clear();

mpOpenAPI.setInfo(null);
}

private static List<String> list(String... values) {
return new ArrayList<>(Arrays.asList(values));
}

private static List<String> applications(List<String> values) {
if (values == null) {
return Collections.emptyList();
}
return values.stream()
.filter(v -> !v.contains("/"))
.collect(Collectors.toList());
}

private static List<String> modules(List<String> values) {
if (values == null) {
return Collections.emptyList();
}
return values.stream()
.filter(v -> v.contains("/"))
.collect(Collectors.toList());
}

private void deployApp(Archive<?> app) throws Exception {
ShrinkHelper.exportDropinAppToServer(server, app, SERVER_ONLY);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*******************************************************************************
* Copyright (c) 2025 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package io.openliberty.microprofile.openapi20.fat.deployments.zosconnect.app;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/")
public class ZOSConnectTestApp extends Application {}
Loading