Skip to content

feat: add support for multiple user environment types and update rela… #2394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
123 changes: 98 additions & 25 deletions apps/dashboard/src/main/java/com/akto/action/ApiCollectionsAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.akto.dto.*;
import com.akto.dto.filter.MergedUrls;
import com.akto.util.Pair;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.bson.conversions.Bson;

import com.akto.action.observe.Utils;
Expand All @@ -17,6 +18,7 @@
import com.akto.dto.billing.FeatureAccess;
import com.akto.dto.usage.MetricTypes;
import com.akto.dto.testing.TestingEndpoints;
import com.akto.dto.traffic.CollectionTags;
import com.akto.dto.traffic.Key;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.Aggregates;
Expand Down Expand Up @@ -170,7 +172,7 @@ public String fetchAllCollectionsBasic() {
List<Bson> pipeLine = new ArrayList<>();
pipeLine.add(Aggregates.project(Projections.fields(
Projections.computed(ApiCollection.URLS_COUNT, new BasicDBObject("$size", new BasicDBObject("$ifNull", Arrays.asList("$urls", Collections.emptyList())))),
Projections.include(ApiCollection.ID, ApiCollection.NAME, ApiCollection.HOST_NAME, ApiCollection._TYPE, ApiCollection.USER_ENV_TYPE, ApiCollection._DEACTIVATED,ApiCollection.START_TS, ApiCollection.AUTOMATED, ApiCollection.DESCRIPTION)
Projections.include(ApiCollection.ID, ApiCollection.NAME, ApiCollection.HOST_NAME, ApiCollection._TYPE, ApiCollection.TAGS_STRING, ApiCollection._DEACTIVATED,ApiCollection.START_TS, ApiCollection.AUTOMATED, ApiCollection.DESCRIPTION, ApiCollection.USER_ENV_TYPE)
)));

try {
Expand All @@ -180,28 +182,20 @@ public String fetchAllCollectionsBasic() {
}
} catch(Exception e){
}
MongoCursor<BasicDBObject> cursor = ApiCollectionsDao.instance.getMCollection().aggregate(pipeLine, BasicDBObject.class).cursor();
MongoCursor<ApiCollection> cursor = ApiCollectionsDao.instance.getMCollection().aggregate(pipeLine, ApiCollection.class).cursor();
while(cursor.hasNext()){
try {
BasicDBObject collection = cursor.next();
ApiCollection apiCollection = new ApiCollection();
apiCollection.setId(collection.getInt(ApiCollection.ID));
apiCollection.setUrlsCount(collection.getInt(ApiCollection.URLS_COUNT));
apiCollection.setName(collection.getString(ApiCollection.NAME));
apiCollection.setHostName(collection.getString(ApiCollection.HOST_NAME));
apiCollection.setDeactivated(collection.getBoolean(ApiCollection._DEACTIVATED));
apiCollection.setStartTs(collection.getInt(ApiCollection.START_TS));
apiCollection.setAutomated(collection.getBoolean(ApiCollection.AUTOMATED));
apiCollection.setDescription(collection.getString(ApiCollection.DESCRIPTION));

String type = collection.getString(ApiCollection._TYPE);
if(type != null && type.length() > 0){
ApiCollection.Type typeEnum = ApiCollection.Type.valueOf(type);
apiCollection.setType(typeEnum);
ApiCollection apiCollection = cursor.next();
List<CollectionTags> tagsList = new ArrayList<>();
List<CollectionTags> value = apiCollection.getTagsList();
if(value != null) {
for (CollectionTags tag : value) {
CollectionTags collectionTags = new CollectionTags(tag.getLastUpdatedTs(), tag.getKeyName(), tag.getValue());
tagsList.add(collectionTags);
}
}
String userEnvType = collection.getString(ApiCollection.USER_ENV_TYPE);
if(userEnvType != null && userEnvType.length() > 0){
apiCollection.setUserSetEnvType(userEnvType);
if(!tagsList.isEmpty()){
apiCollection.setTagsList(tagsList);
}
this.apiCollections.add(apiCollection);
} catch (Exception e) {
Expand Down Expand Up @@ -739,9 +733,14 @@ public String fetchCustomerEndpoints(){

List<Integer> apiCollectionIds;

private String envType;
private List<CollectionTags> envType;
private boolean resetEnvTypes;

public String updateEnvType(){
if(!resetEnvTypes && (envType == null || envType.isEmpty())) {
addActionError("Please enter a valid ENV type.");
return Action.ERROR.toUpperCase();
}
try {
Bson filter = Filters.in("_id", apiCollectionIds);
FindOneAndUpdateOptions updateOptions = new FindOneAndUpdateOptions();
Expand All @@ -760,9 +759,79 @@ public String updateEnvType(){
} catch(Exception e){
}

UpdateResult result = ApiCollectionsDao.instance.getMCollection().updateMany(filter,
Updates.set(ApiCollection.USER_ENV_TYPE,envType)
);
if(resetEnvTypes) {
UpdateResult updateResult = ApiCollectionsDao.instance.getMCollection().updateOne(filter, Updates.unset(ApiCollection.TAGS_STRING));
if(updateResult == null) {
return Action.ERROR.toUpperCase();
}
return Action.SUCCESS.toUpperCase();
}

List<CollectionTags> tagsList = ApiCollectionsDao.instance.findOne(filter, Projections.include(ApiCollection.TAGS_STRING)).getTagsList();

List<CollectionTags> toPull = new ArrayList<>();
List<CollectionTags> toAdd = new ArrayList<>();
if(tagsList == null || tagsList.isEmpty()) {
toAdd.addAll(envType);
}
else {
for (CollectionTags env : envType) {
boolean found = tagsList.stream().anyMatch(tag ->
Objects.equals(tag.getKeyName(), env.getKeyName()) &&
Objects.equals(tag.getValue(), env.getValue())
);

if (found) {
toPull.add(env);
} else {
toAdd.add(env);
}
}

boolean isAddingStaging = toAdd.stream().anyMatch(tag ->
"envType".equalsIgnoreCase(tag.getKeyName()) &&
"staging".equalsIgnoreCase(tag.getValue())
);

boolean isAddingProduction = toAdd.stream().anyMatch(tag ->
"envType".equalsIgnoreCase(tag.getKeyName()) &&
"production".equalsIgnoreCase(tag.getValue())
);

if (isAddingStaging) {
tagsList.stream()
.filter(tag ->
"envType".equalsIgnoreCase(tag.getKeyName()) &&
"production".equalsIgnoreCase(tag.getValue())
)
.findFirst()
.ifPresent(toPull::add);
}

if (isAddingProduction) {
tagsList.stream()
.filter(tag ->
"envType".equalsIgnoreCase(tag.getKeyName()) &&
"staging".equalsIgnoreCase(tag.getValue())
)
.findFirst()
.ifPresent(toPull::add);
}
}

UpdateResult result = null;
if(!toPull.isEmpty()) {
result = ApiCollectionsDao.instance.getMCollection().updateOne(filter,
Updates.pullAll(ApiCollection.TAGS_STRING, toPull)
);
}

if(!toAdd.isEmpty()) {
result = ApiCollectionsDao.instance.getMCollection().updateOne(filter,
Updates.addEachToSet(ApiCollection.TAGS_STRING, toAdd)
);
}

if(result == null){
return Action.ERROR.toUpperCase();
}
Expand Down Expand Up @@ -946,7 +1015,7 @@ public void setRedacted(boolean redacted) {
this.redacted = redacted;
}

public void setEnvType(String envType) {
public void setEnvType(List<CollectionTags> envType) {
this.envType = envType;
}

Expand Down Expand Up @@ -985,4 +1054,8 @@ public Map<Integer, Integer> getDeactivatedHostnameCountMap() {
public void setDescription(String description) {
this.description = description;
}

public void setResetEnvTypes(boolean resetEnvTypes) {
this.resetEnvTypes = resetEnvTypes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
import com.akto.dto.testing.custom_groups.AllAPIsGroup;
import com.akto.dto.testing.custom_groups.UnauthenticatedEndpoint;
import com.akto.dto.testing.sources.AuthWithCond;
import com.akto.dto.traffic.CollectionTags;
import com.akto.dto.traffic.Key;
import com.akto.dto.traffic.SampleData;
import com.akto.dto.traffic.SuspectSampleData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,10 @@ public void testTagsForCollections(){
collection5.setId(5);
collection5.setHostName("kubernetes-121212-akto.io");

assertEquals(ApiCollection.ENV_TYPE.STAGING.name().toString(), collection1.getEnvType());
assertEquals(ApiCollection.ENV_TYPE.STAGING.name().toString(), collection2.getEnvType());
assertNotEquals(ApiCollection.ENV_TYPE.STAGING.name().toString(), collection3.getEnvType());
assertEquals(ApiCollection.ENV_TYPE.STAGING.name().toString(), collection4.getEnvType());
assertEquals(ApiCollection.ENV_TYPE.STAGING.name().toString(), collection5.getEnvType());
assertTrue(collection1.getEnvType() != null && collection1.getEnvType().contains(ApiCollection.ENV_TYPE.STAGING.name()));
assertTrue(collection2.getEnvType() != null && collection2.getEnvType().contains(ApiCollection.ENV_TYPE.STAGING.name()));
assertTrue(collection3.getEnvType() == null || !collection3.getEnvType().contains(ApiCollection.ENV_TYPE.STAGING.name()));
assertTrue(collection4.getEnvType() != null && collection4.getEnvType().contains(ApiCollection.ENV_TYPE.STAGING.name()));
assertTrue(collection5.getEnvType() != null && collection5.getEnvType().contains(ApiCollection.ENV_TYPE.STAGING.name()));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Badge, Box, HorizontalStack } from '@shopify/polaris'
import { Badge, Box, HorizontalStack, Tooltip } from '@shopify/polaris'
import React from 'react'
import TooltipText from './TooltipText'

function ShowListInBadge({itemsArr, maxWidth, status, maxItems, itemWidth, useBadge}) {
function ShowListInBadge({itemsArr, maxWidth, status, maxItems, itemWidth, useBadge, useTooltip}) {
return (
<Box maxWidth={maxWidth}>
<HorizontalStack gap={"1"} wrap={false}>
Expand All @@ -19,7 +19,18 @@ function ShowListInBadge({itemsArr, maxWidth, status, maxItems, itemWidth, useBa
</Box>
)
})}
{(itemsArr.length - maxItems) > 0 ? <Badge status={status} size="medium">+ {itemsArr.length - maxItems}</Badge> : null}

{(itemsArr.length - maxItems) > 0 ? (
useTooltip ?
<Tooltip borderRadius="2" padding="4" content={itemsArr?.slice(maxItems)?.map((item) => {
return (
<div>{item}</div>
)})
}>
<Badge status={status} size="medium">+ {itemsArr.length - maxItems}</Badge>
</Tooltip>
: <Badge status={status} size="medium">+ {itemsArr.length - maxItems}</Badge>
) : null}
</HorizontalStack>
</Box>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -803,11 +803,11 @@ export default {
data:{},
})
},
async updateEnvTypeOfCollection(envType, apiCollectionIds){
async updateEnvTypeOfCollection(envType, apiCollectionIds,resetEnvTypes){
return await request({
url: '/api/updateEnvType',
method: 'post',
data: {envType, apiCollectionIds}
data: {envType, apiCollectionIds,resetEnvTypes}
})
},
fetchEndpoint(apiInfoKey){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import ReactFlow, {

} from 'react-flow-renderer';
import { on } from "stream";
import SetUserEnvPopupComponent from "./component/SetUserEnvPopupComponent";

const CenterViewType = {
Table: 0,
Expand Down Expand Up @@ -127,6 +128,10 @@ const headers = [
showFilter: true,
textValue: 'envType',
tooltipContent: (<Text variant="bodySm">Environment type for an API collection, Staging or Production </Text>),
mergeType: (a, b) => {
return Math.max(a || 0, b || 0);
},
shouldMerge: true,
},
{
title: <HeadingWithTooltip content={<Text variant="bodySm">The most recent time an endpoint within collection was either discovered for the first time or seen again</Text>} title="Last traffic seen" />,
Expand Down Expand Up @@ -236,8 +241,6 @@ function ApiCollections() {
const [normalData, setNormalData] = useState([])
const [centerView, setCenterView] = useState(CenterViewType.Table);
const [moreActions, setMoreActions] = useState(false);
const [textFieldActive, setTextFieldActive] = useState(false);
const [customEnv,setCustomEnv] = useState('')

// const dummyData = dummyJson;

Expand Down Expand Up @@ -657,33 +660,20 @@ function ApiCollections() {
const toggleTypeContent = (
<Popover
activator={<div onClick={() => setPopover(!popover)}>Set ENV type</div>}
onClose={() => setPopover(false)}
onClose={() => {
setPopover(false)
}}
active={popover}
autofocusTarget="first-node"
>
<Popover.Pane>
{textFieldActive ?
<Box padding={"1"}>
<TextField onChange={setCustomEnv} value={customEnv} connectedRight={(
<Tooltip content="Save your Custom env type" dismissOnMouseOut>
<Button onClick={() => {
resetResourcesSelected();
updateEnvType(selectedResources, customEnv);
setTextFieldActive(false);
}} plain icon={FileFilledMinor}/>
</Tooltip>
)}/>
</Box>
:<ActionList
actionRole="menuitem"
items={[
{content: 'Staging', onAction: () => updateEnvType(selectedResources, "STAGING")},
{content: 'Production', onAction: () => updateEnvType(selectedResources, "PRODUCTION")},
{content: 'Reset', onAction: () => updateEnvType(selectedResources, null)},
{content: 'Add Custom', onAction: () => setTextFieldActive(!textFieldActive)}
]}

/>}
<SetUserEnvPopupComponent
popover={popover}
setPopover={setPopover}
tags={envTypeMap}
updateTags={updateTags}
apiCollectionIds={selectedResources}
/>
</Popover.Pane>
</Popover>
)
Expand All @@ -703,23 +693,38 @@ function ApiCollections() {
Object.keys(copyObj).forEach((key) => {
data[key].length > 0 && data[key].forEach((c) => {
c['envType'] = dataMap[c.id]
c['envTypeComp'] = dataMap[c.id] ? <Badge size="small" status="info">{dataMap[c.id]}</Badge> : null
c['envTypeComp'] = transform.getCollectionTypeList(dataMap[c.id])
})
})
setData(copyObj)
setRefreshData(!refreshData)
}

const updateEnvType = (apiCollectionIds,type) => {
const updateTags = (apiCollectionIds, tagObj) => {
let copyObj = JSON.parse(JSON.stringify(envTypeMap))
apiCollectionIds.forEach(id => copyObj[id] = type)
api.updateEnvTypeOfCollection(type,apiCollectionIds).then((resp) => {

apiCollectionIds.forEach(id => {
if(!copyObj[id]) {
copyObj[id] = []
}

if(tagObj === null) {
copyObj[id] = []
} else {
copyObj[id] = tagObj
}
})



api.updateEnvTypeOfCollection(tagObj, apiCollectionIds, tagObj === null).then((resp) => {
func.setToast(true, false, "ENV type updated successfully")
setEnvTypeMap(copyObj)
updateData(copyObj)
})
resetResourcesSelected();

resetResourcesSelected();
setPopover(false)
}

const modalComponent = <CreateNewCollectionModal
Expand Down
Loading
Loading