Skip to content

Commit 294317a

Browse files
authored
Merge pull request #3736 from akto-api-security/feature/bulk_apis_demerge
added support for bulk de-merge
2 parents fd386c7 + b933092 commit 294317a

File tree

4 files changed

+182
-2
lines changed

4 files changed

+182
-2
lines changed

apps/dashboard/src/main/java/com/akto/action/observe/InventoryAction.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,69 @@ public String undoDemergedApis() {
11701170
return SUCCESS.toUpperCase();
11711171
}
11721172

1173+
private List<ApiInfo.ApiInfoKey> apiInfoKeyList;
1174+
1175+
public String bulkDeMergeApis() {
1176+
if (apiInfoKeyList == null || apiInfoKeyList.isEmpty()) {
1177+
addActionError("API list cannot be null or empty");
1178+
return ERROR.toUpperCase();
1179+
}
1180+
1181+
int successCount = 0;
1182+
int failureCount = 0;
1183+
List<String> errors = new ArrayList<>();
1184+
1185+
for (ApiInfo.ApiInfoKey apiInfoKey : apiInfoKeyList) {
1186+
try {
1187+
String apiUrl = apiInfoKey.getUrl();
1188+
String apiMethod = apiInfoKey.getMethod().name();
1189+
int collectionId = apiInfoKey.getApiCollectionId();
1190+
1191+
// Validate that this is a merged URL
1192+
if (!APICatalog.isTemplateUrl(apiUrl)) {
1193+
errors.add("URL " + apiUrl + " is not a merged URL");
1194+
failureCount++;
1195+
continue;
1196+
}
1197+
1198+
// Set temporary values for single API de-merge
1199+
this.url = apiUrl;
1200+
this.method = apiMethod;
1201+
this.apiCollectionId = collectionId;
1202+
1203+
// Call the existing deMergeApi logic
1204+
String result = deMergeApi();
1205+
1206+
if (SUCCESS.toUpperCase().equals(result)) {
1207+
successCount++;
1208+
} else {
1209+
errors.add("Failed to de-merge: " + apiMethod + " " + apiUrl);
1210+
failureCount++;
1211+
}
1212+
} catch (Exception e) {
1213+
loggerMaker.errorAndAddToDb("Error de-merging API: " + e.getMessage(), LogDb.DASHBOARD);
1214+
errors.add("Error de-merging API: " + e.getMessage());
1215+
failureCount++;
1216+
}
1217+
}
1218+
1219+
loggerMaker.infoAndAddToDb("Bulk de-merge completed. Success: " + successCount + ", Failed: " + failureCount, LogDb.DASHBOARD);
1220+
1221+
if (failureCount > 0) {
1222+
addActionError("Some APIs failed to de-merge. Success: " + successCount + ", Failed: " + failureCount);
1223+
}
1224+
1225+
return SUCCESS.toUpperCase();
1226+
}
1227+
1228+
public void setApiInfoKeyList(List<ApiInfo.ApiInfoKey> apiInfoKeyList) {
1229+
this.apiInfoKeyList = apiInfoKeyList;
1230+
}
1231+
1232+
public List<ApiInfo.ApiInfoKey> getApiInfoKeyList() {
1233+
return apiInfoKeyList;
1234+
}
1235+
11731236
public String fetchNotTestedAPICount() {
11741237
Bson filterQ = UsageMetricCalculator.excludeDemosAndDeactivated(ApiInfo.ID_API_COLLECTION_ID);
11751238

apps/dashboard/src/main/resources/struts.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8026,6 +8026,31 @@
80268026
<param name="includeProperties">^actionErrors.*</param>
80278027
</result>
80288028
</action>
8029+
8030+
<action name="api/bulkDeMergeApis" class="com.akto.action.observe.InventoryAction" method="bulkDeMergeApis">
8031+
<interceptor-ref name="json"/>
8032+
<interceptor-ref name="defaultStack" />
8033+
<interceptor-ref name="roleAccessInterceptor">
8034+
<param name="featureLabel">API_COLLECTIONS</param>
8035+
<param name="accessType">READ_WRITE</param>
8036+
<param name="actionDescription">User bulk de-merged APIs</param>
8037+
</interceptor-ref>
8038+
8039+
<result name="FORBIDDEN" type="json">
8040+
<param name="statusCode">403</param>
8041+
<param name="ignoreHierarchy">false</param>
8042+
<param name="includeProperties">^actionErrors.*</param>
8043+
</result>
8044+
<result name="SUCCESS" type="json">
8045+
</result>
8046+
<result name="ERROR" type="json">
8047+
<param name="statusCode">422</param>
8048+
<param name="ignoreHierarchy">false</param>
8049+
<param name="includeProperties">^actionErrors.*</param>
8050+
</result>
8051+
</action>
8052+
8053+
80298054
<action name="tools/addLLmData" class="com.akto.action.ExportSampleDataAction" method="insertLlmData">
80308055
<interceptor-ref name="json"/>
80318056
<interceptor-ref name="defaultStack" />

apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/observe/api.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,13 @@ export default {
815815
data: {apiCollectionId, url, method}
816816
})
817817
},
818+
async bulkDeMergeApis(apiInfoKeyList){
819+
return await request({
820+
url: '/api/bulkDeMergeApis',
821+
method: 'post',
822+
data: {apiInfoKeyList}
823+
})
824+
},
818825
async getUserEndpoints(){
819826
return await request({
820827
url: '/api/getCustomerEndpoints',

apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/observe/api_collections/ApiEndpoints.jsx

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,8 +1259,10 @@ function ApiEndpoints(props) {
12591259

12601260
const [showDeleteApiModal, setShowDeleteApiModal] = useState(false)
12611261
const [showApiGroupModal, setShowApiGroupModal] = useState(false)
1262+
const [showBulkDeMergeModal, setShowBulkDeMergeModal] = useState(false)
12621263
const [apis, setApis] = useState([])
12631264
const [actionOperation, setActionOperation] = useState(Operation.ADD)
1265+
const [deMergingInProgress, setDeMergingInProgress] = useState(false)
12641266

12651267
function handleApiGroupAction(selectedResources, operation){
12661268

@@ -1273,6 +1275,49 @@ function ApiEndpoints(props) {
12731275
setShowApiGroupModal(false);
12741276
}
12751277

1278+
function handleBulkDeMerge(selectedResources){
1279+
// Filter only merged APIs (those containing INTEGER, STRING, OBJECT_ID, or VERSIONED)
1280+
const mergedApis = selectedResources.filter(resource => {
1281+
const parts = resource.split('###')
1282+
const endpoint = parts[1]
1283+
return endpoint && (endpoint.includes("STRING") || endpoint.includes("INTEGER") || endpoint.includes("FLOAT") || endpoint.includes("OBJECT_ID") || endpoint.includes("VERSIONED"))
1284+
})
1285+
1286+
if (mergedApis.length === 0) {
1287+
func.setToast(true, true, "No merged APIs selected. Only merged APIs can be de-merged.")
1288+
return
1289+
}
1290+
1291+
setApis(mergedApis)
1292+
setShowBulkDeMergeModal(true)
1293+
}
1294+
1295+
function deMergeBulkApisAction(){
1296+
setShowBulkDeMergeModal(false)
1297+
setDeMergingInProgress(true)
1298+
1299+
const apiObjects = apis.map((x) => {
1300+
let tmp = x.split("###")
1301+
return {
1302+
method: tmp[0],
1303+
url: tmp[1],
1304+
apiCollectionId: parseInt(tmp[2])
1305+
}
1306+
})
1307+
1308+
api.bulkDeMergeApis(apiObjects).then(resp => {
1309+
setDeMergingInProgress(false)
1310+
func.setToast(true, false, `Successfully de-merged ${apiObjects.length} API(s). Refresh to see the changes.`)
1311+
// Optionally refresh the data
1312+
setTimeout(() => {
1313+
fetchData()
1314+
}, 1000)
1315+
}).catch(err => {
1316+
setDeMergingInProgress(false)
1317+
func.setToast(true, true, "There was an error de-merging the APIs. Please try again or contact [email protected]")
1318+
})
1319+
}
1320+
12761321
const promotedBulkActions = (selectedResources) => {
12771322

12781323
let ret = [
@@ -1295,6 +1340,12 @@ function ApiEndpoints(props) {
12951340
})
12961341
}
12971342

1343+
// Add bulk de-merge option
1344+
ret.push({
1345+
content: 'De-merge ' + mapLabel('APIs', getDashboardCategory()),
1346+
onAction: () => handleBulkDeMerge(selectedResources)
1347+
})
1348+
12981349
if (window.USER_NAME && window.USER_NAME.endsWith("@akto.io")) {
12991350
ret.push({
13001351
content: 'Delete ' + mapLabel('APIs', getDashboardCategory()),
@@ -1325,7 +1376,7 @@ function ApiEndpoints(props) {
13251376
let deleteApiModal = (
13261377
<Modal
13271378
open={showDeleteApiModal}
1328-
onClose={() => setShowApiGroupModal(false)}
1379+
onClose={() => setShowDeleteApiModal(false)}
13291380
title="Confirm"
13301381
primaryAction={{
13311382
content: 'Yes',
@@ -1339,6 +1390,39 @@ function ApiEndpoints(props) {
13391390
</Modal>
13401391
)
13411392

1393+
let bulkDeMergeModal = (
1394+
<Modal
1395+
open={showBulkDeMergeModal}
1396+
onClose={() => setShowBulkDeMergeModal(false)}
1397+
title="Confirm Bulk De-merge"
1398+
primaryAction={{
1399+
content: 'De-merge',
1400+
onAction: deMergeBulkApisAction,
1401+
loading: deMergingInProgress
1402+
}}
1403+
secondaryActions={[
1404+
{
1405+
content: 'Cancel',
1406+
onAction: () => setShowBulkDeMergeModal(false)
1407+
}
1408+
]}
1409+
key="bulk-demerge-modal"
1410+
>
1411+
<Modal.Section>
1412+
<VerticalStack gap="4">
1413+
<Text>Are you sure you want to de-merge {(apis || []).length} merged API(s)?</Text>
1414+
<Text variant="bodyMd" color="subdued">
1415+
This will split the merged endpoints back into their original forms. For example,
1416+
<Text as="span" fontWeight="semibold"> /api/products/INTEGER/reviews </Text>
1417+
will be split into individual endpoints like
1418+
<Text as="span" fontWeight="semibold"> /api/products/24/reviews</Text>,
1419+
<Text as="span" fontWeight="semibold"> /api/products/53/reviews</Text>, etc.
1420+
</Text>
1421+
</VerticalStack>
1422+
</Modal.Section>
1423+
</Modal>
1424+
)
1425+
13421426
const canShowTags = () => {
13431427
return isApiGroup || isQueryPage;
13441428
}
@@ -1436,7 +1520,8 @@ function ApiEndpoints(props) {
14361520
fetchData={fetchData}
14371521
/>,
14381522
modal,
1439-
deleteApiModal
1523+
deleteApiModal,
1524+
bulkDeMergeModal
14401525
]
14411526
)
14421527
]

0 commit comments

Comments
 (0)