Feature/import-export-application (#4553)

* Decryption for dbauth, basic and OAuth datasources added in exported file

* All authentications for datasources included while exporting as deserialization is done through json file otherwise cast is throwing error

* Content-Disposition header implemented 

* MongoEscapedWidget names segregated for published and unpublished layout, to prevent overwrite while exporting

* Published pages and actions explicitly handled in cases where unpublished resources are deleted
This commit is contained in:
Abhijeet 2021-06-01 17:38:26 +05:30 committed by GitHub
parent 771cf4a34f
commit 51addbc963
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1844 additions and 12 deletions

View File

@ -0,0 +1,39 @@
package com.appsmith.external.models;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
* This class hold sensitive information, and fields that have a `@JsonIgnore` on them, so that such information
* can be serialized when an application is exported.
*/
@ToString
@Getter
@Setter
@NoArgsConstructor
public class DecryptedSensitiveFields {
String password;
String token;
String refreshToken;
Object tokenResponse;
String authType;
DBAuth dbAuth;
BasicAuth basicAuth;
OAuth2 openAuth2;
public DecryptedSensitiveFields(AuthenticationResponse authResponse) {
this.token = authResponse.getToken();
this.refreshToken = authResponse.getRefreshToken();
this.tokenResponse = authResponse.getTokenResponse();
}
}

View File

@ -58,6 +58,7 @@ public class FieldName {
public static String ANONYMOUS_USER = "anonymousUser";
public static String USERNAMES = "usernames";
public static String ACTION = "action";
public static String ACTIONS = "actions";
public static String ASSET = "asset";
public static String APPLICATION = "application";
public static String COMMENT = "comment";

View File

@ -2,6 +2,7 @@ package com.appsmith.server.controllers;
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationJson;
import com.appsmith.server.dtos.ApplicationAccessDTO;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.dtos.UserHomepageDTO;
@ -11,9 +12,15 @@ import com.appsmith.server.services.ApplicationPageService;
import com.appsmith.server.services.ApplicationService;
import com.appsmith.server.solutions.ApplicationFetcher;
import com.appsmith.server.solutions.ApplicationForkingService;
import com.appsmith.server.solutions.ImportExportApplicationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.multipart.Part;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -22,12 +29,14 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
import java.nio.charset.StandardCharsets;
@RestController
@RequestMapping(Url.APPLICATION_URL)
@ -37,17 +46,20 @@ public class ApplicationController extends BaseController<ApplicationService, Ap
private final ApplicationPageService applicationPageService;
private final ApplicationFetcher applicationFetcher;
private final ApplicationForkingService applicationForkingService;
private final ImportExportApplicationService importExportApplicationService;
@Autowired
public ApplicationController(
ApplicationService service,
ApplicationPageService applicationPageService,
ApplicationFetcher applicationFetcher,
ApplicationForkingService applicationForkingService) {
ApplicationForkingService applicationForkingService,
ImportExportApplicationService importExportApplicationService) {
super(service);
this.applicationPageService = applicationPageService;
this.applicationFetcher = applicationFetcher;
this.applicationForkingService = applicationForkingService;
this.importExportApplicationService = importExportApplicationService;
}
@PostMapping
@ -117,4 +129,32 @@ public class ApplicationController extends BaseController<ApplicationService, Ap
.map(application -> new ResponseDTO<>(HttpStatus.OK.value(), application, null));
}
@GetMapping("/export/{id}")
public Mono<ResponseEntity<ApplicationJson>> getApplicationFile(@PathVariable String id) {
log.debug("Going to export application with id: {}", id);
return importExportApplicationService.exportApplicationById(id)
.map(fetchedResource -> {
HttpHeaders responseHeaders = new HttpHeaders();
ContentDisposition contentDisposition = ContentDisposition
.builder("attachment")
.filename("application-file.json", StandardCharsets.UTF_8)
.build();
responseHeaders.setContentDisposition(contentDisposition);
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
return new ResponseEntity(fetchedResource, responseHeaders, HttpStatus.OK);
});
}
@PostMapping(value = "/import/{orgId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<ResponseDTO<Application>> importApplicationFromFile(
@RequestPart("file") Mono<Part> fileMono, @PathVariable String orgId) {
log.debug("Going to import application in organization with id: {}", orgId);
return fileMono
.flatMap(file -> importExportApplicationService.extractFileAndSaveApplication(orgId, file))
.map(fetchedResource -> new ResponseDTO<>(HttpStatus.OK.value(), fetchedResource, null));
}
}

View File

@ -0,0 +1,38 @@
package com.appsmith.server.domains;
import com.appsmith.external.models.DecryptedSensitiveFields;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A DTO class to hold complete information about an application, which will then be serialized to a file so as to
* export that application into a file.
*/
@Getter
@Setter
public class ApplicationJson {
Application exportedApplication;
List<Datasource> datasourceList;
List<NewPage> pageList;
String publishedDefaultPageName;
String unpublishedDefaultPageName;
List<NewAction> actionList;
Map<String, DecryptedSensitiveFields> decryptedFields;
/**
* Mapping mongoEscapedWidgets with layoutId
*/
Map<String, Set<String>> publishedLayoutmongoEscapedWidgets;
Map<String, Set<String>> unpublishedLayoutmongoEscapedWidgets;
}

View File

@ -12,7 +12,7 @@ import java.util.Set;
public interface CustomDatasourceRepository extends AppsmithRepository<Datasource> {
Flux<Datasource> findAllByOrganizationId(String organizationId, AclPermission permission);
Mono<Datasource> findByName(String name, AclPermission aclPermission);
Mono<Datasource> findByNameAndOrganizationId(String name, String organizationId, AclPermission aclPermission);
Mono<Datasource> findById(String id, AclPermission aclPermission);

View File

@ -33,9 +33,10 @@ public class CustomDatasourceRepositoryImpl extends BaseAppsmithRepositoryImpl<D
}
@Override
public Mono<Datasource> findByName(String name, AclPermission aclPermission) {
public Mono<Datasource> findByNameAndOrganizationId(String name, String organizationId, AclPermission aclPermission) {
Criteria nameCriteria = where(fieldName(QDatasource.datasource.name)).is(name);
return queryOne(List.of(nameCriteria), aclPermission);
Criteria orgIdCriteria = where(fieldName(QDatasource.datasource.organizationId)).is(organizationId);
return queryOne(List.of(nameCriteria, orgIdCriteria), aclPermission);
}
@Override

View File

@ -34,4 +34,6 @@ public interface ApplicationPageService {
Mono<PageDTO> deleteUnpublishedPage(String id);
Mono<Boolean> publish(String applicationId);
void generateAndSetPagePolicies(Application application, PageDTO page);
}

View File

@ -277,7 +277,7 @@ public class ApplicationPageServiceImpl implements ApplicationPageService {
});
}
private void generateAndSetPagePolicies(Application application, PageDTO page) {
public void generateAndSetPagePolicies(Application application, PageDTO page) {
Set<Policy> documentPolicies = policyGenerator.getAllChildPolicies(application.getPolicies(), Application.class, Page.class);
page.setPolicies(documentPolicies);
}

View File

@ -13,7 +13,7 @@ public interface DatasourceService extends CrudService<Datasource, String> {
Mono<DatasourceTestResult> testDatasource(Datasource datasource);
Mono<Datasource> findByName(String name, AclPermission permission);
Mono<Datasource> findByNameAndOrganizationId(String name, String organizationId, AclPermission permission);
Mono<Datasource> findById(String id, AclPermission aclPermission);

View File

@ -343,8 +343,8 @@ public class DatasourceServiceImpl extends BaseService<DatasourceRepository, Dat
}
@Override
public Mono<Datasource> findByName(String name, AclPermission permission) {
return repository.findByName(name, permission);
public Mono<Datasource> findByNameAndOrganizationId(String name, String organizationId, AclPermission permission) {
return repository.findByNameAndOrganizationId(name, organizationId, permission);
}
@Override

View File

@ -459,7 +459,7 @@ public class ExamplesOrganizationCloner {
});
}
private void makePristine(BaseDomain domain) {
public void makePristine(BaseDomain domain) {
// Set the ID to null for this domain object so that it is saved a new document in the database (as opposed to
// updating an existing document). If it contains any policies, they are also reset.
domain.setId(null);

View File

@ -0,0 +1,640 @@
package com.appsmith.server.solutions;
import com.appsmith.external.helpers.BeanCopyUtils;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.AuthenticationResponse;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.external.models.BasicAuth;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DecryptedSensitiveFields;
import com.appsmith.external.models.OAuth2;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationJson;
import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.PluginType;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.dtos.PageDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.repositories.DatasourceRepository;
import com.appsmith.server.repositories.NewActionRepository;
import com.appsmith.server.repositories.NewPageRepository;
import com.appsmith.server.repositories.PluginRepository;
import com.appsmith.server.services.ApplicationPageService;
import com.appsmith.server.services.ApplicationService;
import com.appsmith.server.services.DatasourceService;
import com.appsmith.server.services.NewActionService;
import com.appsmith.server.services.NewPageService;
import com.appsmith.server.services.OrganizationService;
import com.appsmith.server.services.SequenceService;
import com.appsmith.server.services.SessionUserService;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.bson.types.ObjectId;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.Part;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.Type;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Slf4j
@Component
@RequiredArgsConstructor
public class ImportExportApplicationService {
private final DatasourceService datasourceService;
private final SessionUserService sessionUserService;
private final NewActionRepository newActionRepository;
private final DatasourceRepository datasourceRepository;
private final PluginRepository pluginRepository;
private final OrganizationService organizationService;
private final ApplicationService applicationService;
private final NewPageService newPageService;
private final ApplicationPageService applicationPageService;
private final NewPageRepository newPageRepository;
private final NewActionService newActionService;
private final SequenceService sequenceService;
private final ExamplesOrganizationCloner examplesOrganizationCloner;
private static final Set<MediaType> ALLOWED_CONTENT_TYPES = Set.of(MediaType.APPLICATION_JSON);
public final String INVALID_JSON_FILE = "invalid json file";
private enum PublishType {
UNPUBLISH, PUBLISH
}
public Mono<ApplicationJson> exportApplicationById(String applicationId) {
ApplicationJson applicationJson = new ApplicationJson();
Map<String, String> pluginMap = new HashMap<>();
Map<String, String> datasourceIdToNameMap = new HashMap<>();
Map<String, String> pageIdToNameMap = new HashMap<>();
if (applicationId == null || applicationId.isEmpty()) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.APPLICATION_ID));
}
return pluginRepository
.findAll()
.map(plugin -> {
pluginMap.put(plugin.getId(), plugin.getPackageName());
return plugin;
})
.then(applicationService.findById(applicationId, AclPermission.MANAGE_APPLICATIONS))
.switchIfEmpty(Mono.error(new AppsmithException(
AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId))
)
.flatMap(application -> {
ApplicationPage unpublishedDefaultPage = application.getPages()
.stream()
.filter(ApplicationPage::getIsDefault)
.findFirst()
.orElse(null);
if (unpublishedDefaultPage == null) {
return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.DEFAULT_PAGE_NAME));
} else {
applicationJson.setUnpublishedDefaultPageName(unpublishedDefaultPage.getId());
}
if (application.getPublishedPages() != null) {
ApplicationPage publishedDefaultPage = application.getPublishedPages()
.stream()
.filter(ApplicationPage::getIsDefault)
.findFirst()
.orElse(null);
if(publishedDefaultPage != null) {
applicationJson.setPublishedDefaultPageName(publishedDefaultPage.getId());
}
}
final String organizationId = application.getOrganizationId();
application.setOrganizationId(null);
application.setPages(null);
examplesOrganizationCloner.makePristine(application);
applicationJson.setExportedApplication(application);
return newPageRepository.findByApplicationId(applicationId, AclPermission.MANAGE_PAGES)
.collectList()
.flatMap(newPageList -> {
Map<String, Set<String>> publishedMongoEscapedWidgetsNames = new HashMap<>();
Map<String, Set<String>> unpublishedMongoEscapedWidgetsNames = new HashMap<>();
newPageList.forEach(newPage -> {
if (newPage.getUnpublishedPage() != null) {
pageIdToNameMap.put(
newPage.getId() + PublishType.UNPUBLISH, newPage.getUnpublishedPage().getName()
);
PageDTO unpublishedPageDTO = newPage.getUnpublishedPage();
if (StringUtils.equals(
applicationJson.getUnpublishedDefaultPageName(), newPage.getId())
) {
applicationJson.setUnpublishedDefaultPageName(unpublishedPageDTO.getName());
}
if (unpublishedPageDTO.getLayouts() != null) {
unpublishedPageDTO.getLayouts().forEach(layout ->
unpublishedMongoEscapedWidgetsNames
.put(layout.getId(), layout.getMongoEscapedWidgetNames())
);
}
}
if (newPage.getPublishedPage() != null) {
pageIdToNameMap.put(
newPage.getId() + PublishType.PUBLISH, newPage.getPublishedPage().getName()
);
PageDTO publishedPageDTO = newPage.getPublishedPage();
if (applicationJson.getPublishedDefaultPageName() != null &&
StringUtils.equals(
applicationJson.getPublishedDefaultPageName(), newPage.getId()
)
) {
applicationJson.setPublishedDefaultPageName(publishedPageDTO.getName());
}
if (publishedPageDTO.getLayouts() != null) {
newPage.getPublishedPage().getLayouts().forEach(layout ->
publishedMongoEscapedWidgetsNames
.put(layout.getId(), layout.getMongoEscapedWidgetNames())
);
}
}
newPage.setApplicationId(null);
examplesOrganizationCloner.makePristine(newPage);
});
applicationJson.setPageList(newPageList);
applicationJson.setPublishedLayoutmongoEscapedWidgets(publishedMongoEscapedWidgetsNames);
applicationJson.setUnpublishedLayoutmongoEscapedWidgets(unpublishedMongoEscapedWidgetsNames);
return datasourceRepository
.findAllByOrganizationId(organizationId, AclPermission.MANAGE_DATASOURCES)
.collectList();
})
.flatMapMany(datasourceList -> {
datasourceList.forEach(datasource ->
datasourceIdToNameMap.put(datasource.getId(), datasource.getName()));
applicationJson.setDatasourceList(datasourceList);
return newActionRepository
.findByApplicationId(applicationId, AclPermission.MANAGE_ACTIONS, null);
})
.collectList()
.map(newActionList -> {
Set<String> concernedDBNames = new HashSet<>();
newActionList.forEach(newAction -> {
newAction.setPluginId(pluginMap.get(newAction.getPluginId()));
newAction.setOrganizationId(null);
newAction.setPolicies(null);
newAction.setApplicationId(null);
//Collect Datasource names to filter only required datasources
if (newAction.getPluginType() == PluginType.DB || newAction.getPluginType() == PluginType.API) {
concernedDBNames.add(
mapDatasourceIdToNewAction(newAction.getPublishedAction(), datasourceIdToNameMap)
);
concernedDBNames.add(
mapDatasourceIdToNewAction(newAction.getUnpublishedAction(), datasourceIdToNameMap)
);
}
if (newAction.getUnpublishedAction() != null) {
ActionDTO actionDTO = newAction.getUnpublishedAction();
actionDTO.setPageId(pageIdToNameMap.get(actionDTO.getPageId() + PublishType.UNPUBLISH));
}
if (newAction.getPublishedAction() != null) {
ActionDTO actionDTO = newAction.getPublishedAction();
actionDTO.setPageId(pageIdToNameMap.get(actionDTO.getPageId() + PublishType.PUBLISH));
}
});
applicationJson
.getDatasourceList()
.removeIf(datasource -> !concernedDBNames.contains(datasource.getName()));
applicationJson.setActionList(newActionList);
//Only export those datasources which are used in the app instead of org level
Map<String, DecryptedSensitiveFields> decryptedFields = new HashMap<>();
applicationJson.getDatasourceList().forEach(datasource -> {
decryptedFields.put(datasource.getName(), getDecryptedFields(datasource));
datasource.setId(null);
datasource.setOrganizationId(null);
datasource.setPluginId(pluginMap.get(datasource.getPluginId()));
if (datasource.getDatasourceConfiguration() != null) {
datasource.getDatasourceConfiguration().setAuthentication(null);
}
});
applicationJson.setDecryptedFields(decryptedFields);
return applicationJson;
});
})
.then()
.thenReturn(applicationJson);
}
public Mono<Application> extractFileAndSaveApplication(String orgId, Part filePart) {
final MediaType contentType = filePart.headers().getContentType();
if (orgId == null || orgId.isEmpty()) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORGANIZATION_ID));
}
if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) {
return Mono.error(new AppsmithException(AppsmithError.VALIDATION_FAILURE, INVALID_JSON_FILE));
}
final Flux<DataBuffer> contentCache = filePart.content().cache();
Mono<String> stringifiedFile = DataBufferUtils.join(contentCache)
.map(dataBuffer -> {
byte[] data = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(data);
DataBufferUtils.release(dataBuffer);
return new String(data);
});
return stringifiedFile
.flatMap(data -> {
Gson gson = new Gson();
Type fileType = new TypeToken<ApplicationJson>() {}.getType();
ApplicationJson jsonFile = gson.fromJson(data, fileType);
return importApplicationInOrganization(orgId, jsonFile);
});
}
public Mono<Application> importApplicationInOrganization(String orgId, ApplicationJson importedDoc) {
Map<String, String> pluginMap = new HashMap<>();
Map<String, String> datasourceMap = new HashMap<>();
Map<String, NewPage> pageNameMap = new HashMap<>();
Map<String, String> actionIdMap = new HashMap<>();
Application importedApplication = importedDoc.getExportedApplication();
List<Datasource> importedDatasourceList = importedDoc.getDatasourceList();
List<NewPage> importedNewPageList = importedDoc.getPageList();
List<NewAction> importedNewActionList = importedDoc.getActionList();
Mono<User> currUserMono = sessionUserService.getCurrentUser();
final Flux<Datasource> existingDatasourceFlux = datasourceRepository.findAllByOrganizationId(orgId).cache();
String errorField = "";
if (importedNewPageList == null || importedNewPageList.isEmpty()) {
errorField = FieldName.PAGES;
} else if (importedApplication == null) {
errorField = FieldName.APPLICATION;
} else if (importedNewActionList == null) {
errorField = FieldName.ACTIONS;
} else if (importedDatasourceList == null) {
errorField = FieldName.DATASOURCE;
}
if(!errorField.isEmpty()) {
return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, errorField, INVALID_JSON_FILE));
}
return pluginRepository.findAll()
.map(plugin -> {
pluginMap.put(plugin.getPackageName(), plugin.getId());
return plugin;
})
.then(organizationService.findById(orgId, AclPermission.ORGANIZATION_MANAGE_APPLICATIONS))
.switchIfEmpty(Mono.error(
new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.ORGANIZATION, orgId))
)
.flatMap(organization -> Flux.fromIterable(importedDatasourceList)
//Check for duplicate datasources to avoid duplicates in target organization
.flatMap(datasource -> {
datasource.setPluginId(pluginMap.get(datasource.getPluginId()));
datasource.setOrganizationId(organization.getId());
//Check if any decrypted fields are present for datasource
if (importedDoc.getDecryptedFields().get(datasource.getName()) != null) {
DecryptedSensitiveFields decryptedFields =
importedDoc.getDecryptedFields().get(datasource.getName());
updateAuthenticationDTO(datasource, decryptedFields);
}
return createUniqueDatasourceIfNotPresent(existingDatasourceFlux, datasource, orgId);
})
.map(datasource -> {
datasourceMap.put(datasource.getName(), datasource.getId());
return datasource;
})
.collectList()
)
.then(
applicationPageService.setApplicationPolicies(currUserMono, orgId, importedApplication)
.flatMap(application -> applicationService
.findByOrganizationId(orgId, AclPermission.MANAGE_APPLICATIONS)
.collectList()
.flatMap(applicationList -> {
Application duplicateNameApp = applicationList
.stream()
.filter(application1 -> StringUtils.equals(application1.getName(), application.getName()))
.findAny()
.orElse(null);
return getUniqueSuffixForDuplicateNameEntity(duplicateNameApp, orgId)
.map(suffix -> {
importedApplication.setName(importedApplication.getName() + suffix);
return importedApplication;
});
})
.then(applicationService.save(importedApplication))
)
)
.flatMap(savedApp -> {
importedApplication.setId(savedApp.getId());
importedNewPageList.forEach(newPage -> newPage.setApplicationId(savedApp.getId()));
Map<PublishType, List<ApplicationPage>> applicationPages = Map.of(
PublishType.UNPUBLISH, new ArrayList<>(),
PublishType.PUBLISH, new ArrayList<>()
);
return importAndSavePages(
importedNewPageList,
importedApplication,
importedDoc.getPublishedLayoutmongoEscapedWidgets(),
importedDoc.getUnpublishedLayoutmongoEscapedWidgets()
)
.map(newPage -> {
ApplicationPage unpublishedAppPage = new ApplicationPage();
ApplicationPage publishedAppPage = new ApplicationPage();
if (newPage.getUnpublishedPage() != null && newPage.getUnpublishedPage().getName() != null) {
unpublishedAppPage.setIsDefault(
StringUtils.equals(
newPage.getUnpublishedPage().getName(), importedDoc.getUnpublishedDefaultPageName()
)
);
unpublishedAppPage.setId(newPage.getId());
pageNameMap.put(newPage.getUnpublishedPage().getName(), newPage);
}
if (newPage.getPublishedPage() != null && newPage.getPublishedPage().getName() != null) {
publishedAppPage.setIsDefault(
StringUtils.equals(
newPage.getPublishedPage().getName(), importedDoc.getPublishedDefaultPageName()
)
);
publishedAppPage.setId(newPage.getId());
pageNameMap.put(newPage.getPublishedPage().getName(), newPage);
}
applicationPages.get(PublishType.UNPUBLISH).add(unpublishedAppPage);
applicationPages.get(PublishType.PUBLISH).add(publishedAppPage);
return applicationPages;
})
.then()
.thenReturn(applicationPages);
})
.flatMap(applicationPageMap -> {
importedApplication.setPages(applicationPageMap.get(PublishType.UNPUBLISH));
importedApplication.setPublishedPages(applicationPageMap.get(PublishType.PUBLISH));
importedNewActionList.forEach(newAction -> {
NewPage parentPage = new NewPage();
if (newAction.getUnpublishedAction() != null && newAction.getUnpublishedAction().getName() != null) {
parentPage = pageNameMap.get(newAction.getUnpublishedAction().getPageId());
actionIdMap.put(newAction.getUnpublishedAction().getName(), newAction.getId());
newAction.getUnpublishedAction().setPageId(parentPage.getId());
mapDatasourceIdToNewAction(newAction.getUnpublishedAction(), datasourceMap);
}
if (newAction.getPublishedAction() != null && newAction.getPublishedAction().getName() != null) {
parentPage = pageNameMap.get(newAction.getPublishedAction().getPageId());
actionIdMap.put(newAction.getPublishedAction().getName(), newAction.getId());
newAction.getPublishedAction().setPageId(parentPage.getId());
mapDatasourceIdToNewAction(newAction.getPublishedAction(), datasourceMap);
}
examplesOrganizationCloner.makePristine(newAction);
newAction.setOrganizationId(orgId);
newAction.setApplicationId(importedApplication.getId());
newAction.setPluginId(pluginMap.get(newAction.getPluginId()));
newActionService.generateAndSetActionPolicies(parentPage, newAction);
});
return newActionService.saveAll(importedNewActionList)
.map(newAction -> {
if (newAction.getUnpublishedAction() != null) {
actionIdMap.put(
actionIdMap.get(newAction.getUnpublishedAction().getName()), newAction.getId()
);
}
if (newAction.getPublishedAction() != null) {
actionIdMap.put(
actionIdMap.get(newAction.getPublishedAction().getName()), newAction.getId()
);
}
return newAction;
})
.then(Mono.just(importedApplication));
})
.flatMap(ignored -> {
//Map layoutOnLoadActions ids with relevant actions
importedNewPageList.forEach(page -> mapActionIdWithPageLayout(page, actionIdMap));
return Flux.fromIterable(importedNewPageList)
.flatMap(newPageService::save)
.then(applicationService.update(importedApplication.getId(), importedApplication));
});
}
private Mono<String> getUniqueSuffixForDuplicateNameEntity(BaseDomain sourceEntity, String orgId) {
if (sourceEntity != null) {
return sequenceService
.getNextAsSuffix(sourceEntity.getClass(), " for organization with _id : " + orgId)
.flatMap(sequenceNumber -> Mono.just(" #" + sequenceNumber.trim()));
}
return Mono.just("");
}
private Flux<NewPage> importAndSavePages(List<NewPage> pages,
Application application,
Map<String, Set<String>> publishedMongoEscapedWidget,
Map<String, Set<String>> unpublishedMongoEscapedWidget
) {
pages.forEach(newPage -> {
String layoutId = new ObjectId().toString();
newPage.setApplicationId(application.getId());
if (newPage.getUnpublishedPage() != null) {
applicationPageService.generateAndSetPagePolicies(application, newPage.getUnpublishedPage());
newPage.setPolicies(newPage.getUnpublishedPage().getPolicies());
if (unpublishedMongoEscapedWidget != null) {
newPage.getUnpublishedPage().getLayouts().forEach(layout -> {
layout.setMongoEscapedWidgetNames(unpublishedMongoEscapedWidget.get(layout.getId()));
layout.setId(layoutId);
});
}
}
if (newPage.getPublishedPage() != null) {
applicationPageService.generateAndSetPagePolicies(application, newPage.getPublishedPage());
if (publishedMongoEscapedWidget != null) {
newPage.getPublishedPage().getLayouts().forEach(layout -> {
layout.setMongoEscapedWidgetNames(publishedMongoEscapedWidget.get(layout.getId()));
layout.setId(layoutId);
});
}
}
});
return Flux.fromIterable(pages)
.flatMap(newPageService::save);
}
private String mapDatasourceIdToNewAction(ActionDTO actionDTO, Map<String, String> datasourceMap) {
if (actionDTO != null && actionDTO.getDatasource() != null && actionDTO.getDatasource().getId() != null) {
Datasource ds = actionDTO.getDatasource();
//Mapping ds name in id field
ds.setId(datasourceMap.get(ds.getId()));
ds.setOrganizationId(null);
ds.setPluginId(null);
return ds.getId();
}
return "";
}
private void mapActionIdWithPageLayout(NewPage page, Map<String, String> actionIdMap) {
if (page.getUnpublishedPage().getLayouts() != null) {
page.getUnpublishedPage().getLayouts().forEach(layout -> {
if (layout.getLayoutOnLoadActions() != null) {
layout.getLayoutOnLoadActions().forEach(onLoadAction -> onLoadAction
.forEach(actionDTO -> actionDTO.setId(actionIdMap.get(actionDTO.getId()))));
}
});
}
if (page.getPublishedPage() != null && page.getPublishedPage().getLayouts() != null) {
page.getPublishedPage().getLayouts().forEach(layout -> {
if (layout.getLayoutOnLoadActions() != null) {
layout.getLayoutOnLoadActions().forEach(onLoadAction -> onLoadAction
.forEach(actionDTO -> actionDTO.setId(actionIdMap.get(actionDTO.getId()))));
}
});
}
}
private Mono<Datasource> createUniqueDatasourceIfNotPresent(Flux<Datasource> existingDatasourceFlux,
Datasource datasource,
String toOrgId) {
final DatasourceConfiguration datasourceConfig = datasource.getDatasourceConfiguration();
AuthenticationResponse authResponse = new AuthenticationResponse();
if (datasourceConfig != null && datasourceConfig.getAuthentication() != null) {
BeanCopyUtils.copyNestedNonNullProperties(
datasourceConfig.getAuthentication().getAuthenticationResponse(), authResponse);
datasourceConfig.getAuthentication().setAuthenticationResponse(null);
datasourceConfig.getAuthentication().setAuthenticationType(null);
}
return existingDatasourceFlux
.map(ds -> {
final DatasourceConfiguration dsAuthConfig = ds.getDatasourceConfiguration();
if (dsAuthConfig != null && dsAuthConfig.getAuthentication() != null) {
dsAuthConfig.getAuthentication().setAuthenticationResponse(null);
dsAuthConfig.getAuthentication().setAuthenticationType(null);
}
return ds;
})
.filter(ds -> ds.softEquals(datasource))
.next() // Get the first matching datasource, we don't need more than one here.
.switchIfEmpty(Mono.defer(() -> {
if (datasourceConfig != null && datasourceConfig.getAuthentication() != null) {
datasourceConfig.getAuthentication().setAuthenticationResponse(authResponse);
}
// No matching existing datasource found, so create a new one.
return datasourceService
.findByNameAndOrganizationId(datasource.getName(), toOrgId, AclPermission.MANAGE_DATASOURCES)
.flatMap(duplicateNameDatasource ->
getUniqueSuffixForDuplicateNameEntity(duplicateNameDatasource, toOrgId)
)
.map(suffix -> {
datasource.setName(datasource.getName() + suffix);
return datasource;
})
.then(datasourceService.create(datasource));
}));
}
private Datasource updateAuthenticationDTO(Datasource datasource, DecryptedSensitiveFields decryptedFields) {
final DatasourceConfiguration dsConfig = datasource.getDatasourceConfiguration();
String authType = decryptedFields.getAuthType();
if (dsConfig == null || authType == null) {
return datasource;
}
if (StringUtils.equals(authType, DBAuth.class.getName())) {
final DBAuth dbAuth = decryptedFields.getDbAuth();
dbAuth.setPassword(decryptedFields.getPassword());
datasource.getDatasourceConfiguration().setAuthentication(dbAuth);
} else if (StringUtils.equals(authType, BasicAuth.class.getName())) {
final BasicAuth basicAuth = decryptedFields.getBasicAuth();
basicAuth.setPassword(decryptedFields.getPassword());
datasource.getDatasourceConfiguration().setAuthentication(basicAuth);
} else if (StringUtils.equals(authType, OAuth2.class.getName())) {
OAuth2 auth2 = decryptedFields.getOpenAuth2();
AuthenticationResponse authResponse = new AuthenticationResponse();
auth2.setClientSecret(decryptedFields.getPassword());
authResponse.setToken(decryptedFields.getToken());
authResponse.setRefreshToken(decryptedFields.getRefreshToken());
authResponse.setTokenResponse(decryptedFields.getTokenResponse());
authResponse.setExpiresAt(Instant.now());
datasource.getDatasourceConfiguration().setAuthentication(auth2);
}
return datasource;
}
private DecryptedSensitiveFields getDecryptedFields(Datasource datasource) {
final AuthenticationDTO authentication = datasource.getDatasourceConfiguration() == null
? null : datasource.getDatasourceConfiguration().getAuthentication();
if (authentication != null) {
DecryptedSensitiveFields dsDecryptedFields =
authentication.getAuthenticationResponse() == null
? new DecryptedSensitiveFields()
: new DecryptedSensitiveFields(authentication.getAuthenticationResponse());
if (authentication instanceof DBAuth) {
DBAuth auth = (DBAuth) authentication;
dsDecryptedFields.setPassword(auth.getPassword());
dsDecryptedFields.setDbAuth(auth);
} else if (authentication instanceof OAuth2) {
OAuth2 auth = (OAuth2) authentication;
dsDecryptedFields.setPassword(auth.getClientSecret());
dsDecryptedFields.setOpenAuth2(auth);
} else if (authentication instanceof BasicAuth) {
BasicAuth auth = (BasicAuth) authentication;
dsDecryptedFields.setPassword(auth.getPassword());
dsDecryptedFields.setBasicAuth(auth);
}
dsDecryptedFields.setAuthType(authentication.getClass().getName());
return dsDecryptedFields;
}
return null;
}
}

View File

@ -607,8 +607,10 @@ public class OrganizationServiceTest {
Mono<Organization> readOrganizationByNameMono = organizationRepository.findByName("Member Management Admin Test Organization")
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "organization by name")));
Mono<Datasource> readDatasourceByNameMono = datasourceRepository.findByName("test datasource", READ_DATASOURCES)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "Datasource")));
Mono<Datasource> readDatasourceByNameMono = organizationMono.flatMap(organization1 ->
datasourceRepository.findByNameAndOrganizationId("test datasource", organization1.getId(),READ_DATASOURCES)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "Datasource")))
);
Mono<Tuple3<Application, Organization, Datasource>> testMono = organizationMono
// create application and datasource

View File

@ -0,0 +1,534 @@
package com.appsmith.server.solutions;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DecryptedSensitiveFields;
import com.appsmith.external.models.Policy;
import com.appsmith.external.models.Property;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationJson;
import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.dtos.PageDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.MockPluginExecutor;
import com.appsmith.server.helpers.PluginExecutorHelper;
import com.appsmith.server.repositories.NewPageRepository;
import com.appsmith.server.repositories.PluginRepository;
import com.appsmith.server.services.ApplicationPageService;
import com.appsmith.server.services.ApplicationService;
import com.appsmith.server.services.DatasourceService;
import com.appsmith.server.services.LayoutActionService;
import com.appsmith.server.services.NewActionService;
import com.appsmith.server.services.NewPageService;
import com.appsmith.server.services.OrganizationService;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.UserService;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.LinkedMultiValueMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES;
import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES;
import static com.appsmith.server.acl.AclPermission.READ_ACTIONS;
import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.READ_PAGES;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
@DirtiesContext
public class ImportExportApplicationServiceTests {
@Autowired
private ImportExportApplicationService importExportApplicationService;
@Autowired
private ApplicationPageService applicationPageService;
@Autowired
private UserService userService;
@Autowired
private PluginRepository pluginRepository;
@Autowired
private ApplicationService applicationService;
@Autowired
private DatasourceService datasourceService;
@Autowired
private NewPageService newPageService;
@Autowired
private NewActionService newActionService;
@Autowired
private OrganizationService organizationService;
@Autowired
private SessionUserService sessionUserService;
@Autowired
private LayoutActionService layoutActionService;
@Autowired
private NewPageRepository newPageRepository;
@MockBean
private PluginExecutorHelper pluginExecutorHelper;
private String invalid_json_file;
private Plugin installedPlugin;
private String orgId;
private String testAppId;
private Map<String, Datasource> datasourceMap = new HashMap<>();
private Flux<ActionDTO> getActionsInApplication(Application application) {
return newPageService
// fetch the unpublished pages
.findByApplicationId(application.getId(), READ_PAGES, false)
.flatMap(page -> newActionService.getUnpublishedActions(new LinkedMultiValueMap<>(
Map.of(FieldName.PAGE_ID, Collections.singletonList(page.getId())))));
}
@Before
@WithUserDetails(value = "api_user")
public void setup() {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
installedPlugin = pluginRepository.findByPackageName("installed-plugin").block();
User apiUser = userService.findByEmail("api_user").block();
orgId = apiUser.getOrganizationIds().iterator().next();
invalid_json_file = importExportApplicationService.INVALID_JSON_FILE;
Datasource ds1 = new Datasource();
ds1.setName("DS1");
ds1.setOrganizationId(orgId);
ds1.setPluginId(installedPlugin.getId());
final DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
ds1.setDatasourceConfiguration(datasourceConfiguration);
datasourceConfiguration.setUrl("http://httpbin.org/get");
datasourceConfiguration.setHeaders(List.of(
new Property("X-Answer", "42")
));
final Datasource ds2 = new Datasource();
ds2.setName("DS2");
ds2.setPluginId(installedPlugin.getId());
ds2.setDatasourceConfiguration(new DatasourceConfiguration());
DBAuth auth = new DBAuth();
auth.setPassword("awesome-password");
ds2.getDatasourceConfiguration().setAuthentication(auth);
datasourceMap.put("DS1", ds1);
datasourceMap.put("DS2", ds2);
}
@Test
public void exportApplicationWithNullApplicationIdTest() {
Mono<ApplicationJson> resultMono = importExportApplicationService.exportApplicationById(null);
StepVerifier
.create(resultMono)
.expectErrorMatches(throwable -> throwable instanceof AppsmithException &&
throwable.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.APPLICATION_ID)))
.verify();
}
@Test
@WithUserDetails(value = "api_user")
public void createExportAppJsonWithoutActionsAndDatasourceTest() {
Application testApplication = new Application();
testApplication.setName("Export Application TestApp");
final Mono<ApplicationJson> resultMono = applicationPageService.createApplication(testApplication, orgId)
.flatMap(application -> importExportApplicationService.exportApplicationById(application.getId()));
StepVerifier.create(resultMono)
.assertNext(applicationJson -> {
Application exportedApp = applicationJson.getExportedApplication();
List<NewPage> pageList = applicationJson.getPageList();
List<NewAction> actionList = applicationJson.getActionList();
List<Datasource> datasourceList = applicationJson.getDatasourceList();
NewPage defaultPage = pageList.get(0);
assertThat(exportedApp.getName()).isEqualTo(testApplication.getName());
assertThat(exportedApp.getOrganizationId()).isNull();
assertThat(exportedApp.getPages()).isNull();
assertThat(exportedApp.getPolicies().size()).isEqualTo(0);
assertThat(pageList.isEmpty()).isFalse();
assertThat(defaultPage.getApplicationId()).isNull();
assertThat(defaultPage.getUnpublishedPage().getLayouts().get(0).getLayoutOnLoadActions()).isNull();
assertThat(actionList.isEmpty()).isTrue();
assertThat(datasourceList.isEmpty()).isTrue();
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void createExportAppJsonWithDatasourceButWithoutActionsTest() {
Application testApplication = new Application();
testApplication.setName("Another Export Application");
final Mono<ApplicationJson> resultMono = organizationService.getById(orgId)
.flatMap(organization -> {
final Datasource ds1 = datasourceMap.get("DS1");
ds1.setOrganizationId(organization.getId());
final Datasource ds2 = datasourceMap.get("DS2");
ds2.setOrganizationId(organization.getId());
return Mono.zip(
datasourceService.create(ds1),
datasourceService.create(ds2),
applicationPageService.createApplication(testApplication, orgId)
);
})
.flatMap(tuple -> importExportApplicationService.exportApplicationById(tuple.getT3().getId()));
StepVerifier.create(resultMono)
.assertNext(applicationJson -> {
assertThat(applicationJson.getPageList()).hasSize(1);
assertThat(applicationJson.getActionList()).isEmpty();
assertThat(applicationJson.getDatasourceList()).isEmpty();
assertThat(applicationJson.getDecryptedFields()).isEmpty();
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void createExportAppJsonWithActionsAndDatasourceTest() {
Organization newOrganization = new Organization();
newOrganization.setName("template-org-with-ds");
Application testApplication = new Application();
testApplication.setName("ApplicationWithActionsAndDatasource");
final Mono<ApplicationJson> resultMono = organizationService.create(newOrganization)
.zipWhen(org -> applicationPageService.createApplication(testApplication, org.getId()))
.flatMap(tuple -> {
Organization organization = tuple.getT1();
Application testApp = tuple.getT2();
final Datasource ds1 = datasourceMap.get("DS1");
ds1.setOrganizationId(organization.getId());
final Datasource ds2 = datasourceMap.get("DS2");
ds2.setOrganizationId(organization.getId());
final String pageId = testApp.getPages().get(0).getId();
return Mono.zip(
datasourceService.create(ds1),
datasourceService.create(ds2),
Mono.just(testApp),
newPageService.findPageById(pageId, READ_PAGES, false)
);
})
.flatMap(tuple -> {
Datasource ds1 = tuple.getT1();
Datasource ds2 = tuple.getT2();
Application testApp = tuple.getT3();
PageDTO testPage = tuple.getT4();
Layout layout = testPage.getLayouts().get(0);
JSONObject dsl = new JSONObject(Map.of("text", "{{ query1.data }}"));
JSONObject dsl2 = new JSONObject();
dsl2.put("widgetName", "Table1");
dsl2.put("type", "TABLE_WIDGET");
Map<String, Object> primaryColumns = new HashMap<>();
JSONObject jsonObject = new JSONObject(Map.of("key", "value"));
primaryColumns.put("_id", "{{ query1.data }}");
primaryColumns.put("_class", jsonObject);
dsl2.put("primaryColumns", primaryColumns);
final ArrayList<Object> objects = new ArrayList<>();
JSONArray temp2 = new JSONArray();
temp2.addAll(List.of(new JSONObject(Map.of("key", "primaryColumns._id"))));
dsl2.put("dynamicBindingPathList", temp2);
objects.add(dsl2);
dsl.put("children", objects);
layout.setDsl(dsl);
layout.setPublishedDsl(dsl);
ActionDTO action = new ActionDTO();
action.setName("validAction");
action.setPageId(testPage.getId());
action.setExecuteOnLoad(true);
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setHttpMethod(HttpMethod.GET);
action.setActionConfiguration(actionConfiguration);
action.setDatasource(ds2);
return layoutActionService.createAction(action)
.flatMap(createdAction -> newActionService.findById(createdAction.getId(), READ_ACTIONS))
.flatMap(newAction -> newActionService.generateActionByViewMode(newAction, false))
.then(importExportApplicationService.exportApplicationById(testApp.getId()));
});
StepVerifier
.create(resultMono)
.assertNext(applicationJson -> {
Application exportedApp = applicationJson.getExportedApplication();
List<NewPage> pageList = applicationJson.getPageList();
List<NewAction> actionList = applicationJson.getActionList();
List<Datasource> datasourceList = applicationJson.getDatasourceList();
NewPage defaultPage = pageList.get(0);
assertThat(exportedApp.getName()).isEqualTo(testApplication.getName());
assertThat(exportedApp.getOrganizationId()).isNull();
assertThat(exportedApp.getPages()).isNull();
assertThat(exportedApp.getPolicies()).hasSize(0);
assertThat(pageList).hasSize(1);
assertThat(defaultPage.getApplicationId()).isNull();
assertThat(defaultPage.getUnpublishedPage().getLayouts().get(0).getDsl()).isNotNull();
assertThat(defaultPage.getId()).isNull();
assertThat(defaultPage.getPolicies()).isEmpty();
assertThat(actionList.isEmpty()).isFalse();
NewAction validAction = actionList.get(0);
assertThat(validAction.getApplicationId()).isNull();
assertThat(validAction.getPluginId()).isEqualTo(installedPlugin.getPackageName());
assertThat(validAction.getPluginType()).isEqualTo(PluginType.API);
assertThat(validAction.getOrganizationId()).isNull();
assertThat(validAction.getPolicies()).isNull();
assertThat(validAction.getId()).isNotNull();
assertThat(validAction.getUnpublishedAction().getPageId())
.isEqualTo(defaultPage.getUnpublishedPage().getName());
assertThat(datasourceList).hasSize(1);
Datasource datasource = datasourceList.get(0);
assertThat(datasource.getOrganizationId()).isNull();
assertThat(datasource.getId()).isNull();
assertThat(datasource.getPluginId()).isEqualTo(installedPlugin.getPackageName());
assertThat(datasource.getDatasourceConfiguration().getAuthentication()).isNull();
DecryptedSensitiveFields decryptedFields =
applicationJson.getDecryptedFields().get(datasource.getName());
DBAuth auth = (DBAuth) datasourceMap.get("DS2").getDatasourceConfiguration().getAuthentication();
assertThat(decryptedFields.getAuthType()).isEqualTo(auth.getClass().getName());
assertThat(decryptedFields.getPassword()).isEqualTo("awesome-password");
assertThat(applicationJson.getUnpublishedLayoutmongoEscapedWidgets()).isNotEmpty();
assertThat(applicationJson.getPublishedLayoutmongoEscapedWidgets()).isNotEmpty();
})
.verifyComplete();
}
@Test
public void importApplicationFromInvalidFileTest() {
FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS);
Flux<DataBuffer> dataBufferFlux = DataBufferUtils
.read(new ClassPathResource("test_assets/OrganizationServiceTest/my_organization_logo.png"), new DefaultDataBufferFactory(), 4096)
.cache();
Mockito.when(filepart.content()).thenReturn(dataBufferFlux);
Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.IMAGE_PNG);
Mono<Application> resultMono = importExportApplicationService.extractFileAndSaveApplication(orgId, filepart);
StepVerifier
.create(resultMono)
.expectErrorMatches(error -> error instanceof AppsmithException)
.verify();
}
@Test
public void importApplicationWithNullOrganizationIdTest() {
FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS);
Mono<Application> resultMono = importExportApplicationService
.extractFileAndSaveApplication(null, filepart);
StepVerifier
.create(resultMono)
.expectErrorMatches(throwable -> throwable instanceof AppsmithException &&
throwable.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.ORGANIZATION_ID)))
.verify();
}
@Test
@WithUserDetails(value = "api_user")
public void importApplicationFromInvalidJsonFileWithoutPagesTest() {
FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/invalid-json-without-pages.json");
Mono<Application> resultMono = importExportApplicationService.extractFileAndSaveApplication(orgId,filePart);
StepVerifier
.create(resultMono)
.expectErrorMatches(throwable -> throwable instanceof AppsmithException &&
throwable.getMessage().equals(AppsmithError.NO_RESOURCE_FOUND.getMessage(FieldName.PAGES, invalid_json_file)))
.verify();
}
@Test
@WithUserDetails(value = "api_user")
public void importApplicationFromInvalidJsonFileWithoutApplicationTest() {
FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/invalid-json-without-app.json");
Mono<Application> resultMono = importExportApplicationService.extractFileAndSaveApplication(orgId,filePart);
StepVerifier
.create(resultMono)
.expectErrorMatches(throwable -> throwable instanceof AppsmithException &&
throwable.getMessage().equals(AppsmithError.NO_RESOURCE_FOUND.getMessage(FieldName.APPLICATION, invalid_json_file)))
.verify();
}
@Test
@WithUserDetails(value = "api_user")
public void importApplicationFromValidJsonFileTest() {
FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application.json");
Organization newOrganization = new Organization();
newOrganization.setName("Template Organization");
Policy manageAppPolicy = Policy.builder().permission(MANAGE_APPLICATIONS.getValue())
.users(Set.of("api_user"))
.build();
Policy readAppPolicy = Policy.builder().permission(READ_APPLICATIONS.getValue())
.users(Set.of("api_user"))
.build();
final Mono<Application> resultMono = organizationService
.create(newOrganization)
.flatMap(organization -> importExportApplicationService
.extractFileAndSaveApplication(organization.getId(), filePart)
);
StepVerifier
.create(resultMono
.flatMap(application -> Mono.zip(
Mono.just(application),
datasourceService.findAllByOrganizationId(application.getOrganizationId(), MANAGE_DATASOURCES).collectList(),
getActionsInApplication(application).collectList(),
newPageService.findByApplicationId(application.getId(), MANAGE_PAGES, false).collectList()
)))
.assertNext(tuple -> {
final Application application = tuple.getT1();
final List<Datasource> datasourceList = tuple.getT2();
final List<ActionDTO> actionDTOS = tuple.getT3();
final List<PageDTO> pageList = tuple.getT4();
assertThat(application.getName()).isEqualTo("valid_application");
assertThat(application.getOrganizationId()).isNotNull();
assertThat(application.getPages()).isNotEmpty();
assertThat(application.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy));
assertThat(datasourceList).isNotEmpty();
datasourceList.forEach(datasource -> {
assertThat(datasource.getOrganizationId()).isEqualTo(application.getOrganizationId());
if (datasource.getName().contains("wo-auth")) {
assertThat(datasource.getDatasourceConfiguration().getAuthentication()).isNull();
} else if (datasource.getName().contains("db")) {
DBAuth auth = (DBAuth) datasource.getDatasourceConfiguration().getAuthentication();
assertThat(auth).isNotNull();
assertThat(auth.getPassword()).isNotNull();
assertThat(auth.getUsername()).isNotNull();
}
});
assertThat(actionDTOS).isNotEmpty();
actionDTOS.forEach(actionDTO -> {
assertThat(actionDTO.getPageId()).isNotEqualTo(pageList.get(0).getName());
});
assertThat(pageList).isNotEmpty();
ApplicationPage defaultAppPage = application.getPages()
.stream()
.filter(ApplicationPage::getIsDefault)
.findFirst()
.orElse(null);
assertThat(defaultAppPage).isNotNull();
PageDTO defaultPageDTO = pageList.stream()
.filter(pageDTO -> pageDTO.getId().equals(defaultAppPage.getId())).findFirst().orElse(null);
assertThat(defaultPageDTO).isNotNull();
assertThat(defaultPageDTO.getLayouts().get(0).getLayoutOnLoadActions()).isNotEmpty();
})
.verifyComplete();
}
private FilePart createFilePart(String filePath) {
FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS);
Flux<DataBuffer> dataBufferFlux = DataBufferUtils
.read(
new ClassPathResource(filePath),
new DefaultDataBufferFactory(),
4096)
.cache();
Mockito.when(filepart.content()).thenReturn(dataBufferFlux);
Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.APPLICATION_JSON);
return filepart;
}
}

View File

@ -0,0 +1,18 @@
{
"datasourceList": [],
"pageList": [
{
"applicationId": "test_app"
}
],
"actionList": [
{
"id": "randomId",
"userPermissions": [],
"unpublishedAction": {},
"publishedAction": {}
}
],
"decryptedFields": {},
"mongoEscapedWidgets": {}
}

View File

@ -0,0 +1,12 @@
{
"exportedApplication": {},
"datasourceList": [],
"pageList": [],
"actionList": [
{
"id": "randomId"
}
],
"decryptedFields": {},
"mongoEscapedWidgets": {}
}

View File

@ -0,0 +1,505 @@
{
"exportedApplication": {
"userPermissions": [
"canComment:applications",
"manage:applications",
"read:applications",
"publish:applications",
"makePublic:applications"
],
"name": "valid_application",
"isPublic": false,
"appIsExample": false,
"color": "#EA6179",
"icon": "medical",
"new": true
},
"datasourceList": [
{
"userPermissions": [
"execute:datasources",
"manage:datasources",
"read:datasources"
],
"name": "db-auth",
"pluginId": "mongo-plugin",
"datasourceConfiguration": {
"connection": {
"mode": "READ_WRITE",
"type": "REPLICA_SET",
"ssl": {
"authType": "DEFAULT"
}
},
"endpoints": [
{
"host": "db-auth-uri.net"
}
],
"sshProxyEnabled": false,
"properties": [
{
"key": "Use Mongo Connection String URI",
"value": "No"
}
]
},
"invalids": [],
"isValid": true,
"new": true
},
{
"userPermissions": [
"execute:datasources",
"manage:datasources",
"read:datasources"
],
"name": "api_ds_wo_auth",
"pluginId": "restapi-plugin",
"datasourceConfiguration": {
"sshProxyEnabled": false,
"properties": [
{
"key": "isSendSessionEnabled",
"value": "N"
},
{
"key": "sessionSignatureKey",
"value": ""
}
],
"url": "https://api-ds-wo-auth-uri.com",
"headers": []
},
"invalids": [],
"isValid": true,
"new": true
}
],
"pageList": [
{
"userPermissions": [
"read:pages",
"manage:pages"
],
"applicationId": "valid_application",
"unpublishedPage": {
"name": "Page1",
"layouts": [
{
"id": "60aca056136c4b7178f67906",
"userPermissions": [],
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 1280,
"snapColumns": 16,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
"bottomRow": 800,
"containerStyle": "none",
"snapRows": 33,
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 4,
"minHeight": 840,
"parentColumnSpace": 1,
"dynamicTriggerPathList": [],
"dynamicBindingPathList": [],
"leftColumn": 0,
"children": [
{
"widgetName": "Table1",
"columnOrder": [
"_id",
"username",
"active"
],
"dynamicPropertyPathList": [],
"topRow": 4,
"bottomRow": 15,
"parentRowSpace": 40,
"type": "TABLE_WIDGET",
"parentColumnSpace": 77.5,
"dynamicTriggerPathList": [],
"dynamicBindingPathList": [
{
"key": "tableData"
},
{
"key": "primaryColumns._id.computedValue"
},
{
"key": "primaryColumns.username.computedValue"
},
{
"key": "primaryColumns.active.computedValue"
}
],
"leftColumn": 0,
"primaryColumns": {
"appsmith_mongo_escape_id": {
"isDerived": false,
"computedValue": "{{Table1.sanitizedTableData.map((currentRow) => { return currentRow._id})}}",
"textSize": "PARAGRAPH",
"index": 4,
"isVisible": true,
"label": "_id",
"columnType": "text",
"horizontalAlignment": "LEFT",
"width": 150,
"enableFilter": true,
"enableSort": true,
"id": "_id",
"verticalAlignment": "CENTER"
},
"active": {
"isDerived": false,
"computedValue": "{{Table1.sanitizedTableData.map((currentRow) => { return currentRow.active})}}",
"textSize": "PARAGRAPH",
"index": 8,
"isVisible": true,
"label": "active",
"columnType": "text",
"horizontalAlignment": "LEFT",
"width": 150,
"enableFilter": true,
"enableSort": true,
"id": "active",
"verticalAlignment": "CENTER"
},
"username": {
"isDerived": false,
"computedValue": "{{Table1.sanitizedTableData.map((currentRow) => { return currentRow.username})}}",
"textSize": "PARAGRAPH",
"index": 7,
"isVisible": true,
"label": "username",
"columnType": "text",
"horizontalAlignment": "LEFT",
"width": 150,
"enableFilter": true,
"enableSort": true,
"id": "username",
"verticalAlignment": "CENTER"
}
},
"derivedColumns": {},
"rightColumn": 8,
"textSize": "PARAGRAPH",
"widgetId": "aisibaxwhb",
"tableData": "{{get_users.data}}",
"isVisible": true,
"label": "Data",
"searchKey": "",
"version": 1,
"parentId": "0",
"isLoading": false,
"horizontalAlignment": "LEFT",
"verticalAlignment": "CENTER",
"columnSizeMap": {
"task": 245,
"step": 62,
"status": 75
}
},
{
"widgetName": "Form1",
"backgroundColor": "white",
"rightColumn": 16,
"widgetId": "ut3l54pzqw",
"topRow": 4,
"bottomRow": 11,
"parentRowSpace": 40,
"isVisible": true,
"type": "FORM_WIDGET",
"parentId": "0",
"isLoading": false,
"parentColumnSpace": 77.5,
"leftColumn": 9,
"children": [
{
"widgetName": "Canvas1",
"rightColumn": 542.5,
"detachFromLayout": true,
"widgetId": "mcsltg1l0j",
"containerStyle": "none",
"topRow": 0,
"bottomRow": 320,
"parentRowSpace": 1,
"isVisible": true,
"canExtend": false,
"type": "CANVAS_WIDGET",
"version": 1,
"parentId": "ut3l54pzqw",
"minHeight": 520,
"isLoading": false,
"parentColumnSpace": 1,
"leftColumn": 0,
"children": [
{
"widgetName": "Text1",
"rightColumn": 6,
"textAlign": "LEFT",
"widgetId": "7b4x786lxp",
"topRow": 0,
"bottomRow": 1,
"isVisible": true,
"fontStyle": "BOLD",
"type": "TEXT_WIDGET",
"textColor": "#231F20",
"version": 1,
"parentId": "mcsltg1l0j",
"isLoading": false,
"leftColumn": 0,
"fontSize": "HEADING1",
"text": "Form"
},
{
"widgetName": "Text2",
"rightColumn": 16,
"textAlign": "LEFT",
"widgetId": "d0axuxiosp",
"topRow": 3,
"bottomRow": 6,
"parentRowSpace": 40,
"isVisible": true,
"fontStyle": "BOLD",
"type": "TEXT_WIDGET",
"textColor": "#231F20",
"version": 1,
"parentId": "mcsltg1l0j",
"isLoading": false,
"parentColumnSpace": 31.40625,
"dynamicTriggerPathList": [],
"leftColumn": 0,
"dynamicBindingPathList": [
{
"key": "text"
}
],
"fontSize": "PARAGRAPH2",
"text": "{{api_wo_auth.data.body}}"
},
{
"widgetName": "Text3",
"rightColumn": 4,
"textAlign": "LEFT",
"widgetId": "lmfer0622c",
"topRow": 2,
"bottomRow": 3,
"parentRowSpace": 40,
"isVisible": true,
"fontStyle": "BOLD",
"type": "TEXT_WIDGET",
"textColor": "#231F20",
"version": 1,
"parentId": "mcsltg1l0j",
"isLoading": false,
"parentColumnSpace": 31.40625,
"dynamicTriggerPathList": [],
"leftColumn": 0,
"dynamicBindingPathList": [
{
"key": "text"
}
],
"fontSize": "PARAGRAPH",
"text": "{{api_wo_auth.data.id}}"
}
]
}
]
}
]
},
"layoutOnLoadActions": [
[
{
"id": "60aca24c136c4b7178f6790d",
"name": "api_wo_auth",
"pluginType": "API",
"jsonPathKeys": [],
"timeoutInMillisecond": 10000
},
{
"id": "60aca092136c4b7178f6790a",
"name": "get_users",
"pluginType": "DB",
"jsonPathKeys": [],
"timeoutInMillisecond": 10000
}
]
],
"new": false
}
],
"userPermissions": []
},
"publishedPage": {
"name": "Page1",
"layouts": [
{
"id": "60aca056136c4b7178f67906",
"userPermissions": [],
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 1224,
"snapColumns": 16,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
"bottomRow": 1254,
"containerStyle": "none",
"snapRows": 33,
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 4,
"minHeight": 1292,
"parentColumnSpace": 1,
"dynamicBindingPathList": [],
"leftColumn": 0,
"children": []
},
"new": false
}
],
"userPermissions": []
},
"new": true
}
],
"actionList": [
{
"id": "60aca092136c4b7178f6790a",
"userPermissions": [],
"applicationId": "valid_application",
"pluginType": "DB",
"pluginId": "mongo-plugin",
"unpublishedAction": {
"name": "get_users",
"datasource": {
"id": "db-auth",
"userPermissions": [],
"isValid": true,
"new": false
},
"pageId": "Page1",
"actionConfiguration": {
"timeoutInMillisecond": 10000,
"paginationType": "NONE",
"encodeParamsToggle": true,
"body": "{\n \"find\": \"users\",\n \"sort\": {\n \"id\": 1\n },\n \"limit\": 10\n}",
"pluginSpecifiedTemplates": [
{
"value": false
}
]
},
"executeOnLoad": true,
"dynamicBindingPathList": [],
"isValid": true,
"invalids": [],
"jsonPathKeys": [],
"confirmBeforeExecute": false,
"userPermissions": []
},
"publishedAction": {
"datasource": {
"userPermissions": [],
"isValid": true,
"new": true
},
"confirmBeforeExecute": false,
"userPermissions": []
},
"new": false
},
{
"id": "60aca24c136c4b7178f6790d",
"userPermissions": [],
"applicationId": "valid_application",
"pluginType": "API",
"pluginId": "restapi-plugin",
"unpublishedAction": {
"name": "api_wo_auth",
"datasource": {
"id": "api_ds_wo_auth",
"userPermissions": [],
"isValid": true,
"new": false
},
"pageId": "Page1",
"actionConfiguration": {
"timeoutInMillisecond": 10000,
"paginationType": "NONE",
"path": "/params",
"headers": [
{
"key": "",
"value": ""
},
{
"key": "",
"value": ""
}
],
"encodeParamsToggle": true,
"queryParameters": [],
"body": "",
"httpMethod": "GET",
"pluginSpecifiedTemplates": [
{
"value": false
}
]
},
"executeOnLoad": true,
"dynamicBindingPathList": [],
"isValid": true,
"invalids": [],
"jsonPathKeys": [],
"confirmBeforeExecute": false,
"userPermissions": []
},
"publishedAction": {
"datasource": {
"userPermissions": [],
"isValid": true,
"new": true
},
"confirmBeforeExecute": false,
"userPermissions": []
},
"new": false
}
],
"decryptedFields": {
"db-auth": {
"password": "CreativePassword",
"authType": "com.appsmith.external.models.DBAuth",
"dbAuth": {
"authenticationType": "dbAuth",
"authType": "SCRAM_SHA_1",
"username": "CreativeUser",
"databaseName": "db-name"
}
}
},
"publishedDefaultPageName": "Page1",
"unpublishedDefaultPageName": "Page1",
"publishedLayoutmongoEscapedWidgets": {
"60aca056136c4b7178f67906": [
"Table1"
]
},
"unpublishedLayoutmongoEscapedWidgets": {
"60aca056136c4b7178f67906": [
"Table1"
]
}
}

View File

@ -23,4 +23,4 @@ APPSMITH_CODEC_SIZE=10
#APPSMITH_SENTRY_ENVIRONMENT=
#APPSMITH_RECAPTCHA_SITE_KEY=""
#APPSMITH_RECAPTCHA_SECRET_KEY=""
#APPSMITH_RECAPTCHA_SECRET_KEY=""