Merge pull request #2320 from appsmithorg/release

Release
This commit is contained in:
Arpit Mohan 2020-12-23 09:10:53 +05:30 committed by GitHub
commit 0a16e29a80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1442 additions and 405 deletions

View File

@ -23,7 +23,7 @@ describe("Test Create Api and Bind to Table widget", function() {
/**Bind Table with Textwidget with selected row */
cy.SearchEntityandOpen("Text1");
cy.testJsontext("text", "{{Table1.selectedRow.url}}");
cy.get(commonlocators.editPropCrossButton).click();
cy.SearchEntityandOpen("Table1");
cy.readTabledata("0", "0").then(tabData => {
const tableData = tabData;
localStorage.setItem("tableDataPage1", tableData);

View File

@ -0,0 +1,19 @@
const homePage = require("../../../locators/HomePage.json");
describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() {
it("Duplicating an application", function()
{
// Navigate to home Page
// Click on any application action icon (Three dots)
// Click on "Duplicate" option
// Ensure the application gets copied
// Click on "Appsmith" to navigate to homepage
// Click on action icon
// Click on Delete option
// Click on "Are You Sure?" option
// Ensure the App gets deleted
}
)
}
)

View File

@ -0,0 +1,15 @@
const homePage = require("../../../locators/HomePage.json");
describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() {
it("Duplicating an application", function()
{
// Navigate to home Page
// Click on any application action icon (Three dots)
// Click on "Duplicate" option
// Ensure the application gets copied
// Ensure the name is appended with the word "Copy"
}
)
}
)

View File

@ -0,0 +1,28 @@
const homePage = require("../../../locators/HomePage.json");
describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() {
it("Duplicating an application", function()
{
// Navigate to home Page
// Click on any application action icon (Three dots)
// Click on "Duplicate" option
// Ensure the application gets copied
// Ensure the name is appended with the word "Copy"
}
)
it("Deleting the duplicated Application ", function()
{
// Navigate to home Page
// Click on any application action icon (Three dots)
// Click on "Duplicate" option
// Ensure the application gets copied
// Click on "Appsmith" to navigate to homepage
// Click on action icon
// Click on Delete option
// Click on "Are You Sure?" option
// Ensure the App gets deleted
}
)
}
)

View File

@ -0,0 +1,19 @@
const homePage = require("../../../locators/HomePage.json");
describe("Deletion of organisational Logo ", function() {
it(" org logo upload ", function()
{
//Click on the dropdown next to organisational Name
// Navigate between tabs
// Naviagte to General Tab
// Add an Organisational Logo
// Wait until it loads
// Switch between Tabs
// Click on the remove Icon
//Ensure the organisational Logo is deleted
}
)
}
)

View File

@ -0,0 +1,18 @@
const homePage = require("../../../locators/HomePage.json");
describe("insert organisational Logo ", function() {
it(" org logo upload ", function()
{
//Click on the dropdown next to organisational Name
// Navigate between tabs
// Naviagte to General Tab
// Add an Organisational Logo
//Wait until it loads
// Switch between Tabs
// Navigate to General Tab and ensure the logo exsits
//navigate back to Homepage
}
)
}
)

View File

@ -0,0 +1,15 @@
const homePage = require("../../../locators/HomePage.json");
describe("Checking for error message on Organisation Name ", function() {
it("Ensure of Inactive Submit button ", function()
{
// Navigate to home Page
// Click on Create Organisation
// Type "Space" as first character
// Ensure "Submit" button does not get Active
// Now click on "X" (Close icon) ensure the pop up closes
}
)
}
)

View File

@ -0,0 +1,36 @@
const homePage = require("../../../locators/HomePage.json");
describe("Checking for error message on Organisation Name ", function() {
it("Ensure of Inactive Submit button ", function()
{
// Navigate to home Page
// Click on Create Organisation
// Type "Space" as first character
// Ensure "Submit" button does not get Active
// Now click on "X" (Close icon) ensure the pop up closes
}
)
it("Reuse the name of the deleted application name ", function()
{
// Navigate to home Page
// Create an Application by name "XYZ"
// Add some widgets
// Navigate back to the application
// Delete the Application
// Click on "Create New" option under samee organisation
// Enter the name "XYZ"
// Ensure the application can be created with the same name
}
)
it("Adding Special Character ", function()
{
// Navigate to home Page
// Click on Create Organisation
// Add special as first character
// Ensure "Submit" get Active
// Now click outside and ensure the pop up closes
}
)
}
)

View File

@ -0,0 +1,18 @@
const homePage = require("../../../locators/HomePage.json");
describe("Reuse the name of the deleted application name inside the same organisation", function() {
it("Reuse the name of the deleted application name ", function()
{
// Navigate to home Page
// Create an Application by name "XYZ"
// Add some widgets
// Navigate back to the application
// Delete the Application
// Click on "Create New" option under samee organisation
// Enter the name "XYZ"
// Ensure the application can be created with the same name
}
)
}
)

View File

@ -0,0 +1,17 @@
const homePage = require("../../../locators/HomePage.json");
describe("Shared user icon ", function() {
it(" User Icon is disaplyed to user ", function()
{
// Navigate to home Page
//Click on Share Icon
// Click on Field to add an Email Id
// Click on the Roles field
// Add an role from the Dropdown
// CLick on Invite
//Now observe the icon next to the Share Icon
}
)
}
)

View File

@ -0,0 +1,15 @@
const homePage = require("../../../locators/HomePage.json");
describe("Adding Special Character ", function() {
it("Adding Special Character ", function()
{
// Navigate to home Page
// Click on Create Organisation
// Add special as first character
// Ensure "Submit" get Active
// Now click outside and ensure the pop up closes
}
)
}
)

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from "react";
import React, { forwardRef, ReactNode, Ref } from "react";
import { CommonComponentProps, Classes } from "./common";
import styled from "styled-components";
import Icon, { IconName, IconSize } from "./Icon";
@ -13,22 +13,31 @@ export type MenuItemProps = CommonComponentProps & {
href?: string;
type?: "warning";
ellipsize?: number;
selected?: boolean;
onSelect?: () => void;
};
const ItemRow = styled.a<{ disabled?: boolean }>`
const ItemRow = styled.a<{ disabled?: boolean; selected?: boolean }>`
display: flex;
align-items: center;
justify-content: space-between;
text-decoration: none;
padding: 0px ${props => props.theme.spaces[6]}px;
background-color: ${props =>
props.selected ? props.theme.colors.menuItem.hoverBg : "transparent"};
.${Classes.TEXT} {
color: ${props => props.theme.colors.menuItem.normalText};
color: ${props =>
props.selected
? props.theme.colors.menuItem.hoverText
: props.theme.colors.menuItem.normalText};
}
.${Classes.ICON} {
svg {
path {
fill: ${props => props.theme.colors.menuItem.normalIcon};
fill: ${props =>
props.selected
? props.theme.colors.menuItem.hoverIcon
: props.theme.colors.menuItem.normalIcon};
}
}
}
@ -78,40 +87,46 @@ const IconContainer = styled.span`
margin-right: ${props => props.theme.spaces[5]}px;
}
`;
function MenuItem(props: MenuItemProps) {
return props.ellipsize && props.text.length > props.ellipsize ? (
<TooltipComponent position={Position.BOTTOM} content={props.text}>
<MenuItemContent {...props} />
</TooltipComponent>
) : (
<MenuItemContent {...props} />
);
}
function MenuItemContent(props: MenuItemProps) {
return (
<ItemRow
href={props.href}
onClick={props.onSelect}
disabled={props.disabled}
data-cy={props.cypressSelector}
type={props.type}
>
<IconContainer className={props.className}>
{props.icon ? <Icon name={props.icon} size={IconSize.LARGE} /> : null}
{props.text ? (
<Text type={TextType.H5} weight={FontWeight.NORMAL}>
{props.ellipsize
? ellipsize(props.ellipsize, props.text)
: props.text}
</Text>
) : null}
</IconContainer>
{props.label ? props.label : null}
</ItemRow>
);
}
const MenuItem = forwardRef(
(props: MenuItemProps, ref: Ref<HTMLAnchorElement>) => {
return props.ellipsize && props.text.length > props.ellipsize ? (
<TooltipComponent position={Position.BOTTOM} content={props.text}>
<MenuItemContent ref={ref} {...props} />
</TooltipComponent>
) : (
<MenuItemContent ref={ref} {...props} />
);
},
);
const MenuItemContent = forwardRef(
(props: MenuItemProps, ref: Ref<HTMLAnchorElement>) => {
return (
<ItemRow
href={props.href}
onClick={props.onSelect}
disabled={props.disabled}
data-cy={props.cypressSelector}
type={props.type}
ref={ref}
selected={props.selected}
>
<IconContainer className={props.className}>
{props.icon ? <Icon name={props.icon} size={IconSize.LARGE} /> : null}
{props.text ? (
<Text type={TextType.H5} weight={FontWeight.NORMAL}>
{props.ellipsize
? ellipsize(props.ellipsize, props.text)
: props.text}
</Text>
) : null}
</IconContainer>
{props.label ? props.label : null}
</ItemRow>
);
},
);
MenuItemContent.displayName = "MenuItemContent";
MenuItem.displayName = "MenuItem";
function ellipsize(length: number, text: string) {
return text.length > length ? text.slice(0, length).concat(" ...") : text;

View File

@ -88,11 +88,12 @@ export const DeployLinkButton = (props: Props) => {
content={
<DeployLinkDialog>
<Tooltip
content={isCopied ? "Copied!" : "Copy app publish link"}
content={isCopied ? "Copied!" : "Copy link to published app"}
autoFocus={false}
interactionKind={PopoverInteractionKind.HOVER_TARGET_ONLY}
lazy
position={PopoverPosition.BOTTOM}
openOnTargetFocus={false}
>
<IconContainer onClick={copyToClipboard}>
<Icon icon="link" color="#BCCCD9" />

View File

@ -2,10 +2,10 @@ import React, { ReactNode } from "react";
import styled from "styled-components";
import * as Sentry from "@sentry/react";
type Props = { isValid: boolean; children: ReactNode };
type Props = { children: ReactNode };
type State = { hasError: boolean };
const ErrorBoundaryContainer = styled.div<{ isValid: boolean }>`
const ErrorBoundaryContainer = styled.div`
height: 100%;
width: 100%;
@ -41,7 +41,7 @@ class ErrorBoundary extends React.Component<Props, State> {
render() {
return (
<ErrorBoundaryContainer isValid={this.props.isValid}>
<ErrorBoundaryContainer>
{this.state.hasError ? (
<p>
Oops, Something went wrong.

View File

@ -1,4 +1,4 @@
import React, { Component, Fragment, useState } from "react";
import React, { Component, Fragment, useEffect, useRef, useState } from "react";
import styled from "styled-components";
import { connect, useSelector, useDispatch } from "react-redux";
import { AppState } from "reducers";
@ -340,6 +340,8 @@ const ApplicationAddCardWrapper = styled(Card)`
`;
function LeftPane() {
const menuRef = useRef<HTMLAnchorElement>(null);
const [selectedOrg, setSelectedOrg] = useState<string>("");
const fetchedUserOrgs = useSelector(getUserApplicationsOrgs);
const isFetchingApplications = useSelector(getIsFetchingApplications);
const NewWorkspaceTrigger = (
@ -359,6 +361,20 @@ function LeftPane() {
userOrgs = loadingUserOrgs as any;
}
const urlHash = decodeURI(
window.location.hash.substring(1, window.location.hash.length),
);
useEffect(() => {
const timer = setTimeout(() => {
if (menuRef && menuRef.current) {
menuRef.current.scrollIntoView({ behavior: "smooth" });
menuRef.current.click();
}
}, 0);
return () => clearTimeout(timer);
}, [fetchedUserOrgs]);
return (
<LeftPaneWrapper>
<LeftPaneSection
@ -375,6 +391,7 @@ function LeftPane() {
{userOrgs &&
userOrgs.map((org: any) => (
<MenuItem
{...(urlHash === org.organization.name ? { ref: menuRef } : {})}
className={
isFetchingApplications ? BlueprintClasses.SKELETON : ""
}
@ -383,6 +400,11 @@ function LeftPane() {
href={`${window.location.pathname}#${org.organization.name}`}
text={org.organization.name}
ellipsize={20}
onSelect={() => setSelectedOrg(org.organization.id)}
selected={
selectedOrg === org.organization.id &&
urlHash === org.organization.name
}
/>
))}
</WorkpsacesNavigator>

View File

@ -156,6 +156,7 @@ const PropertyPaneTitle = memo((props: PropertyPaneTitleProps) => {
}
position={Position.TOP}
hoverOpenDelay={200}
boundary="window"
>
<Icon color={theme.colors.paneSectionLabel} icon="help" iconSize={16} />
</Tooltip>

View File

@ -5,6 +5,7 @@ import styled from "styled-components";
import AutoToolTipComponent from "components/designSystems/appsmith/AutoToolTipComponent";
import { getType, Types } from "utils/TypeHelpers";
import { Colors } from "constants/Colors";
import ErrorBoundary from "components/editorComponents/ErrorBoundry";
interface TableProps {
data: Record<string, any>[];
@ -214,59 +215,61 @@ const Table = (props: TableProps) => {
if (rows.length === 0 || headerGroups.length === 0) return null;
return (
<TableWrapper>
<div className="tableWrap">
<div {...getTableProps()} className="table">
{headerGroups.map((headerGroup: any, index: number) => (
<div
key={index}
{...headerGroup.getHeaderGroupProps()}
className="tr"
>
{headerGroup.headers.map((column: any, columnIndex: number) => (
<div
key={columnIndex}
{...column.getHeaderProps()}
className="th header-reorder"
>
<ErrorBoundary>
<TableWrapper>
<div className="tableWrap">
<div {...getTableProps()} className="table">
{headerGroups.map((headerGroup: any, index: number) => (
<div
key={index}
{...headerGroup.getHeaderGroupProps()}
className="tr"
>
{headerGroup.headers.map((column: any, columnIndex: number) => (
<div
className={
!column.isHidden ? "draggable-header" : "hidden-header"
}
key={columnIndex}
{...column.getHeaderProps()}
className="th header-reorder"
>
{column.render("Header")}
<div
className={
!column.isHidden ? "draggable-header" : "hidden-header"
}
>
{column.render("Header")}
</div>
</div>
</div>
))}
))}
</div>
))}
<div {...getTableBodyProps()} className="tbody">
{rows.map((row: any, index: number) => {
prepareRow(row);
return (
<div key={index} {...row.getRowProps()} className={"tr"}>
{row.cells.map((cell: any, cellIndex: number) => {
return (
<div
key={cellIndex}
{...cell.getCellProps()}
className="td"
data-rowindex={index}
data-colindex={cellIndex}
>
<CellWrapper isHidden={false}>
{cell.render("Cell")}
</CellWrapper>
</div>
);
})}
</div>
);
})}
</div>
))}
<div {...getTableBodyProps()} className="tbody">
{rows.map((row: any, index: number) => {
prepareRow(row);
return (
<div key={index} {...row.getRowProps()} className={"tr"}>
{row.cells.map((cell: any, cellIndex: number) => {
return (
<div
key={cellIndex}
{...cell.getCellProps()}
className="td"
data-rowindex={index}
data-colindex={cellIndex}
>
<CellWrapper isHidden={false}>
{cell.render("Cell")}
</CellWrapper>
</div>
);
})}
</div>
);
})}
</div>
</div>
</div>
</TableWrapper>
</TableWrapper>
</ErrorBoundary>
);
};

View File

@ -1,4 +1,4 @@
import { createReducer } from "utils/AppsmithUtils";
import { createImmerReducer } from "utils/AppsmithUtils";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
@ -6,7 +6,7 @@ export type EvaluatedTreeState = DataTree;
const initialState: EvaluatedTreeState = {};
const evaluatedTreeReducer = createReducer(initialState, {
const evaluatedTreeReducer = createImmerReducer(initialState, {
[ReduxActionTypes.SET_EVALUATED_TREE]: (
state: EvaluatedTreeState,
action: ReduxAction<DataTree>,

View File

@ -1,7 +1,7 @@
import { combineReducers } from "redux";
import entityReducer from "./entityReducers";
import uiReducer from "./uiReducers";
import evaluationsReducer from "./evalutationReducers";
import evaluationsReducer from "./evaluationReducers";
import { reducer as formReducer } from "redux-form";
import { CanvasWidgetsReduxState } from "./entityReducers/canvasWidgetsReducer";
import { EditorReduxState } from "./uiReducers/editorReducer";
@ -35,8 +35,8 @@ import { PageCanvasStructureReduxState } from "./uiReducers/pageCanvasStructure"
import { ConfirmRunActionReduxState } from "./uiReducers/confirmRunActionReducer";
import { AppDataState } from "reducers/entityReducers/appReducer";
import { DatasourceNameReduxState } from "./uiReducers/datasourceNameReducer";
import { EvaluatedTreeState } from "./evalutationReducers/treeReducer";
import { EvaluationDependencyState } from "./evalutationReducers/dependencyReducer";
import { EvaluatedTreeState } from "./evaluationReducers/treeReducer";
import { EvaluationDependencyState } from "./evaluationReducers/dependencyReducer";
import { PageWidgetsReduxState } from "./uiReducers/pageWidgetsReducer";
const appReducer = combineReducers({

View File

@ -249,11 +249,42 @@ export function* evaluateDynamicBoundValueSaga(
const EXECUTION_PARAM_REFERENCE_REGEX = /this.params/g;
/**
* Api1
* URL: https://example.com/{{Text1.text}}
* Body: {
* "name": "{{this.params.name}}",
* "age": {{this.params.age}},
* "gender": {{Dropdown1.selectedOptionValue}}
* }
*
* If you call
* Api1.run(undefined, undefined, { name: "Hetu", age: Input1.text });
*
* executionParams is { name: "Hetu", age: Input1.text }
* bindings is [
* "Text1.text",
* "Dropdown1.selectedOptionValue",
* "this.params.name",
* "this.params.age",
* ]
*
* Return will be [
* { key: "Text1.text", value: "updateUser" },
* { key: "Dropdown1.selectedOptionValue", value: "M" },
* { key: "this.params.name", value: "Hetu" },
* { key: "this.params.age", value: 26 },
* ]
* @param bindings
* @param executionParams
*/
export function* getActionParams(
bindings: string[] | undefined,
executionParams?: Record<string, any>,
) {
if (_.isNil(bindings)) return [];
// This might look like a bug, but isn't.
// We send in stringified executionParams, but get back an object
const evaluatedExecutionParams = yield evaluateDynamicBoundValueSaga(
JSON.stringify(executionParams),
);

View File

@ -117,10 +117,11 @@ export function* evaluateSingleValue(
) {
if (evaluationWorker) {
const dataTree = yield select(getDataTree);
dataTree[EXECUTION_PARAM_KEY] = executionParams;
evaluationWorker.postMessage({
action: EVAL_WORKER_ACTIONS.EVAL_SINGLE,
dataTree,
dataTree: Object.assign({}, dataTree, {
[EXECUTION_PARAM_KEY]: executionParams,
}),
binding,
});
const workerResponse = yield take(workerChannel);

View File

@ -16,7 +16,7 @@ export const dataTreeTypeDefCreator = (dataTree: DataTree) => {
};
Object.keys(dataTree).forEach(entityName => {
const entity = dataTree[entityName];
if ("ENTITY_TYPE" in entity) {
if (entity && "ENTITY_TYPE" in entity) {
if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) {
const widgetType = entity.type;
if (widgetType in entityDefinitions) {

View File

@ -15,7 +15,6 @@ import {
CSSUnit,
CONTAINER_GRID_PADDING,
} from "constants/WidgetConstants";
import _ from "lodash";
import DraggableComponent from "components/editorComponents/DraggableComponent";
import ResizableComponent from "components/editorComponents/ResizableComponent";
import { ExecuteActionPayload } from "constants/ActionConstants";
@ -205,8 +204,8 @@ abstract class BaseWidget<
);
}
addErrorBoundary(content: ReactNode, isValid: boolean) {
return <ErrorBoundary isValid={isValid}>{content}</ErrorBoundary>;
addErrorBoundary(content: ReactNode) {
return <ErrorBoundary>{content}</ErrorBoundary>;
}
private getWidgetView(): ReactNode {
@ -226,7 +225,7 @@ abstract class BaseWidget<
case RenderModes.PAGE:
content = this.getPageView();
if (this.props.isVisible) {
content = this.addErrorBoundary(content, true);
content = this.addErrorBoundary(content);
if (!this.props.detachFromLayout) {
content = this.makePositioned(content);
}
@ -241,13 +240,8 @@ abstract class BaseWidget<
abstract getPageView(): ReactNode;
getCanvasView(): ReactNode {
let isValid = true;
if (this.props.invalidProps) {
isValid = _.keys(this.props.invalidProps).length === 0;
}
if (this.props.isLoading) isValid = true;
const content = this.getPageView();
return this.addErrorBoundary(content, isValid);
return this.addErrorBoundary(content);
}
// TODO(abhinav): Maybe make this a pure component to bailout from updating altogether.

View File

@ -239,7 +239,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
let inputFormat;
try {
const type = column.metaProperties.inputFormat;
if (type !== "EPOCH" && type !== "Milliseconds") {
if (type !== "Epoch" && type !== "Milliseconds") {
inputFormat = type;
moment(value, inputFormat);
} else if (!isNumber(value)) {
@ -253,6 +253,8 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
outputFormat = inputFormat;
}
if (column.metaProperties.inputFormat === "Milliseconds") {
value = Number(value);
} else if (column.metaProperties.inputFormat === "Epoch") {
value = 1000 * Number(value);
}
tableRow[accessor] = moment(value, inputFormat).format(
@ -656,8 +658,10 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
};
handleRowClick = (rowData: Record<string, unknown>, index: number) => {
const { selectedRowIndices } = this.props;
if (this.props.multiRowSelection) {
const selectedRowIndices = this.props.selectedRowIndices
? [...this.props.selectedRowIndices]
: [];
if (selectedRowIndices.includes(index)) {
const rowIndex = selectedRowIndices.indexOf(index);
selectedRowIndices.splice(rowIndex, 1);

View File

@ -0,0 +1,21 @@
package com.appsmith.external.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation is meant to introduce polymorphic behaviour in persistent objects. Since we do not expect Spring to
* be able to automatically detect such objects, objects marked with this annotation are specifically registered in the
* type mapper for {@link org.springframework.data.mongodb.core.MongoTemplate}
*
* The value associated to this annotation functions as an alias for the entity.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DocumentType {
public String value() default "";
}

View File

@ -0,0 +1,101 @@
package com.appsmith.external.annotations;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.convert.TypeInformationMapper;
import org.springframework.data.mapping.Alias;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This {@link TypeInformationMapper} implementation makes use of the {@link DocumentType} annotation to register all
* such entities as possible candidates for domain mapping.
*/
public class DocumentTypeMapper implements TypeInformationMapper {
private final Map<String, ClassTypeInformation<?>> aliasToTypeMap;
private final Map<ClassTypeInformation<?>, String> typeToAliasMap;
private DocumentTypeMapper(List<String> basePackagesToScan) {
aliasToTypeMap = new HashMap<>();
typeToAliasMap = new HashMap<>();
// Upon initialization, read all aliases from annotated entities
populateTypeMap(basePackagesToScan);
}
private void populateTypeMap(List<String> basePackagesToScan) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(DocumentType.class));
for (String basePackage : basePackagesToScan) {
for (BeanDefinition bd : scanner.findCandidateComponents(basePackage)) {
try {
Class<?> clazz = Class.forName(bd.getBeanClassName());
DocumentType documentTypeAnnotation = clazz.getAnnotation(DocumentType.class);
ClassTypeInformation<?> type = ClassTypeInformation.from(clazz);
String alias = documentTypeAnnotation.value();
aliasToTypeMap.put(alias, type);
typeToAliasMap.put(type, alias);
} catch (ClassNotFoundException e) {
throw new IllegalStateException(String.format("Class [%s] could not be loaded.", bd.getBeanClassName()), e);
}
}
}
}
@Override
public TypeInformation<?> resolveTypeFrom(Alias alias) {
if (aliasToTypeMap.containsKey((String) alias.getValue())) {
return aliasToTypeMap.get(alias.getValue());
}
return null;
}
@Override
public Alias createAliasFor(TypeInformation<?> typeInformation) {
if (typeToAliasMap.containsKey(typeInformation)) {
return Alias.of(typeToAliasMap.get(typeInformation));
}
return Alias.NONE;
}
public static class Builder {
List<String> basePackagesToScan;
public Builder() {
basePackagesToScan = new ArrayList<>();
}
public Builder withBasePackage(String basePackage) {
basePackagesToScan.add(basePackage);
return this;
}
public Builder withBasePackages(String[] basePackages) {
basePackagesToScan.addAll(Arrays.asList(basePackages));
return this;
}
public Builder withBasePackages(Collection< ? extends String> basePackages) {
basePackagesToScan.addAll(basePackages);
return this;
}
public DocumentTypeMapper build() {
return new DocumentTypeMapper(basePackagesToScan);
}
}
}

View File

@ -0,0 +1,6 @@
package com.appsmith.external.constants;
public class AuthType {
public static final String DB_AUTH = "dbAuth";
public static final String OAUTH2 = "oAuth2";
}

View File

@ -0,0 +1,8 @@
package com.appsmith.external.constants;
public class FieldName {
public static final String CLIENT_SECRET = "clientSecret";
public static final String TOKEN = "token";
public static final String PASSWORD = "password";
}

View File

@ -1,30 +1,53 @@
package com.appsmith.external.models;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import com.appsmith.external.constants.AuthType;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true,
property = "type",
defaultImpl = DBAuth.class)
@JsonSubTypes({
@JsonSubTypes.Type(value = DBAuth.class, name = AuthType.DB_AUTH),
@JsonSubTypes.Type(value = OAuth2.class, name = AuthType.OAUTH2)
})
public class AuthenticationDTO {
// In principle, this class should've been abstract. However, when this class is abstract, Spring's deserialization
// routines choke on identifying the correct class to instantiate and ends up trying to instantiate this abstract
// class and fails.
public enum Type {
SCRAM_SHA_1, SCRAM_SHA_256, MONGODB_CR, USERNAME_PASSWORD
@JsonIgnore
private Boolean isEncrypted;
@JsonIgnore
public Map<String, String> getEncryptionFields() {
return Collections.emptyMap();
}
Type authType;
public void setEncryptionFields(Map<String, String> encryptedFields) {
// This is supposed to be overridden by implementations.
}
String username;
@JsonIgnore
public Set<String> getEmptyEncryptionFields() {
return Collections.emptySet();
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
String password;
String databaseName;
@JsonIgnore
public boolean isEncrypted() {
return Boolean.TRUE.equals(isEncrypted);
}
}

View File

@ -0,0 +1,59 @@
package com.appsmith.external.models;
import com.appsmith.external.annotations.DocumentType;
import com.appsmith.external.constants.AuthType;
import com.appsmith.external.constants.FieldName;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.util.Map;
import java.util.Set;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@DocumentType(AuthType.DB_AUTH)
public class DBAuth extends AuthenticationDTO {
public enum Type {
SCRAM_SHA_1, SCRAM_SHA_256, MONGODB_CR, USERNAME_PASSWORD
}
Type authType;
String username;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
String password;
String databaseName;
@Override
public Map<String, String> getEncryptionFields() {
if (this.password != null && !this.password.isEmpty()) {
return Map.of(FieldName.PASSWORD, this.password);
}
return Map.of();
}
@Override
public void setEncryptionFields(Map<String, String> encryptedFields) {
if (encryptedFields != null && encryptedFields.containsKey(FieldName.PASSWORD)) {
this.password = encryptedFields.get(FieldName.PASSWORD);
}
}
@Override
public Set<String> getEmptyEncryptionFields() {
if (this.password == null || this.password.isEmpty()) {
return Set.of(FieldName.PASSWORD);
}
return Set.of();
}
}

View File

@ -0,0 +1,84 @@
package com.appsmith.external.models;
import com.appsmith.external.annotations.DocumentType;
import com.appsmith.external.constants.AuthType;
import com.appsmith.external.constants.FieldName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@DocumentType(AuthType.OAUTH2)
public class OAuth2 extends AuthenticationDTO {
public enum Type {
CLIENT_CREDENTIALS,
}
Type authType;
String clientId;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
String clientSecret;
String accessTokenUrl;
String scope;
@JsonIgnore
String token;
@JsonIgnore
Instant expiresAt;
@Override
public Map<String, String> getEncryptionFields() {
Map<String, String> map = new HashMap<>();
if (this.clientSecret != null) {
map.put(FieldName.CLIENT_SECRET, this.clientSecret);
}
if (this.token != null) {
map.put(FieldName.TOKEN, this.token);
}
return map;
}
@Override
public void setEncryptionFields(Map<String, String> encryptedFields) {
if (encryptedFields != null) {
if (encryptedFields.containsKey(FieldName.CLIENT_SECRET)) {
this.clientSecret = encryptedFields.get(FieldName.CLIENT_SECRET);
}
if (encryptedFields.containsKey(FieldName.TOKEN)) {
this.token = encryptedFields.get(FieldName.TOKEN);
}
}
}
@Override
public Set<String> getEmptyEncryptionFields() {
Set<String> set = new HashSet<>();
if (this.clientSecret == null || this.clientSecret.isEmpty()) {
set.add(FieldName.CLIENT_SECRET);
}
if (this.token == null || this.token.isEmpty()) {
set.add(FieldName.TOKEN);
}
return set;
}
}

View File

@ -0,0 +1,9 @@
package com.appsmith.external.models;
public interface UpdatableConnection {
void updateDatasource(DatasourceConfiguration datasourceConfiguration);
default boolean isUpdated() {
return false;
}
}

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.Endpoint;
@ -138,7 +138,7 @@ public class DynamoPlugin extends BasePlugin {
builder.endpointOverride(URI.create("http://" + endpoint.getHost() + ":" + endpoint.getPort()));
}
final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication == null || StringUtils.isEmpty(authentication.getDatabaseName())) {
return Mono.error(new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
@ -169,7 +169,7 @@ public class DynamoPlugin extends BasePlugin {
public Set<String> validateDatasource(@NonNull DatasourceConfiguration datasourceConfiguration) {
Set<String> invalids = new HashSet<>();
final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication == null) {
invalids.add("Missing AWS Access Key ID and Secret Access Key.");
} else {

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Endpoint;
import lombok.extern.log4j.Log4j;
@ -96,10 +96,11 @@ public class DynamoPluginTest {
Endpoint endpoint = new Endpoint();
endpoint.setHost(host);
endpoint.setPort(port.longValue());
dsConfig.setAuthentication(new AuthenticationDTO());
dsConfig.getAuthentication().setUsername("dummy");
dsConfig.getAuthentication().setPassword("dummy");
dsConfig.getAuthentication().setDatabaseName(Region.AP_SOUTH_1.toString());
DBAuth auth = new DBAuth();
auth.setUsername("dummy");
auth.setPassword("dummy");
auth.setDatabaseName(Region.AP_SOUTH_1.toString());
dsConfig.setAuthentication(auth);
dsConfig.setEndpoints(List.of(endpoint));
}

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.Endpoint;
@ -137,7 +137,7 @@ public class ElasticSearchPlugin extends BasePlugin {
final RestClientBuilder clientBuilder = RestClient.builder(hosts.toArray(new HttpHost[]{}));
final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication != null
&& !StringUtils.isEmpty(authentication.getUsername())
&& !StringUtils.isEmpty(authentication.getPassword())) {

View File

@ -2,7 +2,6 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Endpoint;
import lombok.extern.slf4j.Slf4j;

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.DatasourceTestResult;
@ -17,6 +17,7 @@ import com.google.cloud.firestore.CollectionReference;
import com.google.cloud.firestore.DocumentReference;
import com.google.cloud.firestore.DocumentSnapshot;
import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.Query;
import com.google.cloud.firestore.QueryDocumentSnapshot;
import com.google.cloud.firestore.QuerySnapshot;
import com.google.cloud.firestore.WriteResult;
@ -132,7 +133,7 @@ public class FirestorePlugin extends BasePlugin {
if (method.isDocumentLevel()) {
return handleDocumentLevelMethod(connection, path, method, mapBody);
} else {
return handleCollectionLevelMethod(connection, path, method, properties);
return handleCollectionLevelMethod(connection, path, method, properties, mapBody);
}
})
.subscribeOn(scheduler);
@ -221,15 +222,33 @@ public class FirestorePlugin extends BasePlugin {
Firestore connection,
String path,
com.external.plugins.Method method,
List<Property> properties
List<Property> properties,
Map<String, Object> mapBody
) {
final CollectionReference collection = connection.collection(path);
if (method == Method.GET_COLLECTION) {
return methodGetCollection(collection, properties);
} else if (method == Method.ADD_TO_COLLECTION) {
return methodAddToCollection(collection, mapBody);
}
return Mono.error(new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Unsupported collection-level command: " + method
));
}
private Mono<ActionExecutionResult> methodGetCollection(CollectionReference query, List<Property> properties) {
final String orderBy = properties.size() > 1 && properties.get(1) != null ? properties.get(1).getValue() : null;
final int limit = properties.size() > 2 && properties.get(2) != null ? Integer.parseInt(properties.get(2).getValue()) : 10;
final String queryFieldPath = properties.size() > 3 && properties.get(3) != null ? properties.get(3).getValue() : null;
final Op operator = properties.size() > 4 && properties.get(4) != null ? Op.valueOf(properties.get(4).getValue()) : null;
final String queryValue = properties.size() > 5 && properties.get(5) != null ? properties.get(5).getValue() : null;
return Mono.just(connection.collection(path))
return Mono.just(query)
// Apply ordering, if provided.
.map(query1 -> StringUtils.isEmpty(orderBy) ? query1 : query1.orderBy(orderBy))
// Apply where condition, if provided.
@ -285,17 +304,7 @@ public class FirestorePlugin extends BasePlugin {
// Apply limit, always provided, since without it we can inadvertently end up processing too much data.
.map(query1 -> query1.limit(limit))
// Run the Firestore query to get a Future of the results.
.flatMap(query1 -> {
switch (method) {
case GET_COLLECTION:
return Mono.just(query1.get());
default:
return Mono.error(new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Unknown collection method: " + method.toString() + "."
));
}
})
.map(Query::get)
// Consume the future to get the actual results.
.flatMap(resultFuture -> {
try {
@ -315,7 +324,35 @@ public class FirestorePlugin extends BasePlugin {
result.setIsExecutionSuccess(true);
System.out.println(
Thread.currentThread().getName()
+ ": In the Firestore Plugin, got action execution result"
+ ": In the Firestore Plugin, got action execution result for get collection"
);
return Mono.just(result);
});
}
private Mono<ActionExecutionResult> methodAddToCollection(CollectionReference collection, Map<String, Object> mapBody) {
return Mono.justOrEmpty(collection.add(mapBody))
.flatMap(future -> {
try {
return Mono.just(future.get());
} catch (InterruptedException | ExecutionException e) {
return Mono.error(new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
e.getMessage()
));
}
})
.flatMap(opResult -> {
ActionExecutionResult result = new ActionExecutionResult();
try {
result.setBody(resultToMap(opResult));
} catch (AppsmithPluginException e) {
return Mono.error(e);
}
result.setIsExecutionSuccess(true);
System.out.println(
Thread.currentThread().getName()
+ ": In the Firestore Plugin, got action execution result for add to collection"
);
return Mono.just(result);
});
@ -344,6 +381,13 @@ public class FirestorePlugin extends BasePlugin {
}
return documents;
} else if (objResult instanceof DocumentReference) {
DocumentReference documentReference = (DocumentReference) objResult;
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("id", documentReference.getId());
resultMap.put("path", documentReference.getPath());
return resultMap;
} else {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
@ -355,7 +399,7 @@ public class FirestorePlugin extends BasePlugin {
@Override
public Mono<Firestore> datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
final Set<String> errors = validateDatasource(datasourceConfiguration);
if (!CollectionUtils.isEmpty(errors)) {
@ -405,7 +449,7 @@ public class FirestorePlugin extends BasePlugin {
@Override
public Set<String> validateDatasource(DatasourceConfiguration datasourceConfiguration) {
final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
Set<String> invalids = new HashSet<>();

View File

@ -5,6 +5,7 @@ public enum Method {
GET_COLLECTION(false, false),
SET_DOCUMENT(true, true),
CREATE_DOCUMENT(true, true),
ADD_TO_COLLECTION(false, true),
UPDATE_DOCUMENT(true, true),
DELETE_DOCUMENT(true, false),
;

View File

@ -34,6 +34,10 @@
"label": "Create Document",
"value": "CREATE_DOCUMENT"
},
{
"label": "Add Document to Collection",
"value": "ADD_TO_COLLECTION"
},
{
"label": "Update Document",
"value": "UPDATE_DOCUMENT"

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Property;
import com.google.cloud.NoCredentials;
@ -25,6 +25,7 @@ import java.util.concurrent.ExecutionException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
@ -60,9 +61,10 @@ public class FirestorePluginTest {
firestoreConnection.document("changing/to-delete").set(Map.of("value", 1)).get();
dsConfig.setUrl(emulator.getEmulatorEndpoint());
dsConfig.setAuthentication(new AuthenticationDTO());
dsConfig.getAuthentication().setUsername("test-project");
dsConfig.getAuthentication().setPassword("");
DBAuth auth = new DBAuth();
auth.setUsername("test-project");
auth.setPassword("");
dsConfig.setAuthentication(auth);
}
@Test
@ -203,4 +205,27 @@ public class FirestorePluginTest {
.verifyComplete();
}
@Test
public void testAddToCollection() {
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setPath("changing");
actionConfiguration.setPluginSpecifiedTemplates(List.of(new Property("method", "ADD_TO_COLLECTION")));
actionConfiguration.setBody("{\n" +
" \"question\": \"What is the answer to life, universe and everything else?\",\n" +
" \"answer\": 42\n" +
"}");
Mono<ActionExecutionResult> resultMono = pluginExecutor
.execute(firestoreConnection, dsConfig, actionConfiguration);
StepVerifier.create(resultMono)
.assertNext(result -> {
assertTrue(result.getIsExecutionSuccess());
assertNotNull(firestoreConnection.document("changing/" + ((Map) result.getBody()).get("id")));
})
.verifyComplete();
}
}

View File

@ -2,8 +2,8 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.Connection;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.DatasourceTestResult;
@ -53,10 +53,10 @@ import java.util.stream.Collectors;
public class MongoPlugin extends BasePlugin {
private static final Set<AuthenticationDTO.Type> VALID_AUTH_TYPES = Set.of(
AuthenticationDTO.Type.SCRAM_SHA_1,
AuthenticationDTO.Type.SCRAM_SHA_256,
AuthenticationDTO.Type.MONGODB_CR // NOTE: Deprecated in the driver.
private static final Set<DBAuth.Type> VALID_AUTH_TYPES = Set.of(
DBAuth.Type.SCRAM_SHA_1,
DBAuth.Type.SCRAM_SHA_256,
DBAuth.Type.MONGODB_CR // NOTE: Deprecated in the driver.
);
private static final String VALID_AUTH_TYPES_STR = VALID_AUTH_TYPES.stream()
@ -180,7 +180,7 @@ public class MongoPlugin extends BasePlugin {
String databaseName = datasourceConfiguration.getConnection().getDefaultDatabaseName();
// If that's not available, pick the authentication database.
final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (StringUtils.isEmpty(databaseName) && authentication != null) {
databaseName = authentication.getDatabaseName();
}
@ -221,7 +221,7 @@ public class MongoPlugin extends BasePlugin {
builder.append("mongodb://");
}
AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication != null) {
builder
.append(urlEncode(authentication.getUsername()))
@ -293,12 +293,12 @@ public class MongoPlugin extends BasePlugin {
}
AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication == null) {
invalids.add("Missing authentication details.");
} else {
AuthenticationDTO.Type authType = authentication.getAuthType();
DBAuth.Type authType = authentication.getAuthType();
if (authType != null && VALID_AUTH_TYPES.contains(authType)) {

View File

@ -0,0 +1,16 @@
{
"templates": [
{
"file": "CREATE.json"
},
{
"file": "READ.json"
},
{
"file": "UPDATE.json"
},
{
"file": "DELETE.json"
}
]
}

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.Endpoint;
@ -196,7 +196,7 @@ public class MssqlPlugin extends BasePlugin {
));
}
AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection();
@ -282,15 +282,16 @@ public class MssqlPlugin extends BasePlugin {
invalids.add("Missing Connection Mode.");
}
if (datasourceConfiguration.getAuthentication() == null) {
DBAuth auth = (DBAuth) datasourceConfiguration.getAuthentication();
if (auth == null) {
invalids.add("Missing authentication details.");
} else {
if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getUsername())) {
if (StringUtils.isEmpty(auth.getUsername())) {
invalids.add("Missing username for authentication.");
}
if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getPassword())) {
if (StringUtils.isEmpty(auth.getPassword())) {
invalids.add("Missing password for authentication.");
}

View File

@ -0,0 +1,16 @@
{
"templates": [
{
"file": "CREATE.sql"
},
{
"file": "SELECT.sql"
},
{
"file": "UPDATE.sql"
},
{
"file": "DELETE.sql"
}
]
}

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Endpoint;
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
@ -107,8 +107,8 @@ public class MssqlPluginTest {
}
private DatasourceConfiguration createDatasourceConfiguration() {
AuthenticationDTO authDTO = new AuthenticationDTO();
authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD);
DBAuth authDTO = new DBAuth();
authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD);
authDTO.setUsername(username);
authDTO.setPassword(password);
@ -208,8 +208,9 @@ public class MssqlPluginTest {
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
// Set up random username and password and try to connect
dsConfig.getAuthentication().setUsername(new ObjectId().toString());
dsConfig.getAuthentication().setPassword(new ObjectId().toString());
DBAuth auth = (DBAuth) dsConfig.getAuthentication();
auth.setUsername(new ObjectId().toString());
auth.setPassword(new ObjectId().toString());
Mono<Connection> dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig);

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.DatasourceTestResult;
@ -55,15 +55,15 @@ public class MySqlPlugin extends BasePlugin {
private static final String TIMESTAMP_COLUMN_TYPE_NAME = "timestamp";
/**
Example output for COLUMNS_QUERY:
+------------+-----------+-------------+-------------+-------------+------------+----------------+
| table_name | column_id | column_name | column_type | is_nullable | COLUMN_KEY | EXTRA |
+------------+-----------+-------------+-------------+-------------+------------+----------------+
| test | 1 | id | int | 0 | PRI | auto_increment |
| test | 2 | firstname | varchar | 1 | | |
| test | 3 | middlename | varchar | 1 | | |
| test | 4 | lastname | varchar | 1 | | |
+------------+-----------+-------------+-------------+-------------+------------+----------------+
* Example output for COLUMNS_QUERY:
* +------------+-----------+-------------+-------------+-------------+------------+----------------+
* | table_name | column_id | column_name | column_type | is_nullable | COLUMN_KEY | EXTRA |
* +------------+-----------+-------------+-------------+-------------+------------+----------------+
* | test | 1 | id | int | 0 | PRI | auto_increment |
* | test | 2 | firstname | varchar | 1 | | |
* | test | 3 | middlename | varchar | 1 | | |
* | test | 4 | lastname | varchar | 1 | | |
* +------------+-----------+-------------+-------------+-------------+------------+----------------+
*/
private static final String COLUMNS_QUERY = "select tab.table_name as table_name,\n" +
" col.ordinal_position as column_id,\n" +
@ -82,12 +82,12 @@ public class MySqlPlugin extends BasePlugin {
" col.ordinal_position;";
/**
Example output for KEYS_QUERY:
+-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+
| CONSTRAINT_NAME | self_schema | self_table | constraint_type | self_column | foreign_schema | foreign_table | foreign_column |
+-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+
| PRIMARY | mytestdb | test | p | id | NULL | NULL | NULL |
+-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+
* Example output for KEYS_QUERY:
* +-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+
* | CONSTRAINT_NAME | self_schema | self_table | constraint_type | self_column | foreign_schema | foreign_table | foreign_column |
* +-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+
* | PRIMARY | mytestdb | test | p | id | NULL | NULL | NULL |
* +-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+
*/
private static final String KEYS_QUERY = "select i.constraint_name,\n" +
" i.TABLE_SCHEMA as self_schema,\n" +
@ -123,18 +123,17 @@ public class MySqlPlugin extends BasePlugin {
Iterator<ColumnMetadata> iterator = (Iterator<ColumnMetadata>) meta.getColumnMetadatas().iterator();
Map<String, Object> processedRow = new LinkedHashMap<>();
while(iterator.hasNext()) {
while (iterator.hasNext()) {
ColumnMetadata metaData = iterator.next();
String columnName = metaData.getName();
String typeName = metaData.getJavaType().toString();
Object columnValue = row.get(columnName);
if(java.time.LocalDate.class.toString().equalsIgnoreCase(typeName)
if (java.time.LocalDate.class.toString().equalsIgnoreCase(typeName)
&& columnValue != null) {
columnValue = DateTimeFormatter.ISO_DATE.format(row.get(columnName,
LocalDate.class));
}
else if ((java.time.LocalDateTime.class.toString().equalsIgnoreCase(typeName))
} else if ((java.time.LocalDateTime.class.toString().equalsIgnoreCase(typeName))
&& columnValue != null) {
columnValue = DateTimeFormatter.ISO_DATE_TIME.format(
LocalDateTime.of(
@ -142,17 +141,14 @@ public class MySqlPlugin extends BasePlugin {
row.get(columnName, LocalDateTime.class).toLocalTime()
)
) + "Z";
}
else if(java.time.LocalTime.class.toString().equalsIgnoreCase(typeName)
} else if (java.time.LocalTime.class.toString().equalsIgnoreCase(typeName)
&& columnValue != null) {
columnValue = DateTimeFormatter.ISO_TIME.format(row.get(columnName,
LocalTime.class));
}
else if (java.time.Year.class.toString().equalsIgnoreCase(typeName)
} else if (java.time.Year.class.toString().equalsIgnoreCase(typeName)
&& columnValue != null) {
columnValue = row.get(columnName, LocalDate.class).getYear();
}
else {
} else {
columnValue = row.get(columnName);
}
@ -165,10 +161,10 @@ public class MySqlPlugin extends BasePlugin {
/**
* 1. Check the type of sql query - i.e Select ... or Insert/Update/Drop
* 2. In case sql queries are chained together, then decide the type based on the last query. i.e In case of
* query "select * from test; updated test ..." the type of query will be based on the update statement.
* query "select * from test; updated test ..." the type of query will be based on the update statement.
* 3. This is used because the output returned to client is based on the type of the query. In case of a
* select query rows are returned, whereas, in case of any other query the number of updated rows is
* returned.
* select query rows are returned, whereas, in case of any other query the number of updated rows is
* returned.
*/
private boolean getIsSelectOrShowQuery(String query) {
String[] queries = query.split(";");
@ -189,17 +185,16 @@ public class MySqlPlugin extends BasePlugin {
boolean isSelectOrShowQuery = getIsSelectOrShowQuery(query);
final List<Map<String, Object>> rowsList = new ArrayList<>(50);
Flux<Result> resultFlux = Mono.from(connection.validate(ValidationDepth.REMOTE))
.flatMapMany(isValid -> {
if(isValid) {
return connection.createStatement(query).execute();
}
else {
return Flux.error(new StaleConnectionException());
}
});
.flatMapMany(isValid -> {
if (isValid) {
return connection.createStatement(query).execute();
} else {
return Flux.error(new StaleConnectionException());
}
});
Mono<List<Map<String, Object>>> resultMono = null;
if(isSelectOrShowQuery) {
if (isSelectOrShowQuery) {
resultMono = resultFlux
.flatMap(result -> {
return result.map((row, meta) -> {
@ -211,8 +206,7 @@ public class MySqlPlugin extends BasePlugin {
.flatMap(execResult -> {
return Mono.just(rowsList);
});
}
else {
} else {
resultMono = resultFlux
.flatMap(result -> result.getRowsUpdated())
.collectList()
@ -243,7 +237,7 @@ public class MySqlPlugin extends BasePlugin {
@Override
public Mono<Connection> datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection();
StringBuilder urlBuilder = new StringBuilder();
@ -328,16 +322,17 @@ public class MySqlPlugin extends BasePlugin {
if (datasourceConfiguration.getAuthentication() == null) {
invalids.add("Missing authentication details.");
} else {
if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getUsername())) {
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (StringUtils.isEmpty(authentication.getUsername())) {
invalids.add("Missing username for authentication.");
}
if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getPassword())) {
if (StringUtils.isEmpty(authentication.getPassword())) {
invalids.add("Missing password for authentication.");
}
if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getDatabaseName())) {
invalids.add("Missing database name");
if (StringUtils.isEmpty(authentication.getDatabaseName())) {
invalids.add("Missing database name.");
}
}
@ -516,11 +511,11 @@ public class MySqlPlugin extends BasePlugin {
.collectList()
.thenMany(Flux.from(connection.createStatement(KEYS_QUERY).execute()))
.flatMap(result -> {
return result.map((row, meta) -> {
getKeyInfo(row, meta, tablesByName, keyRegistry);
return result.map((row, meta) -> {
getKeyInfo(row, meta, tablesByName, keyRegistry);
return result;
});
return result;
});
})
.collectList()
.map(list -> {

View File

@ -0,0 +1,16 @@
{
"templates": [
{
"file": "CREATE.sql"
},
{
"file": "SELECT.sql"
},
{
"file": "UPDATE.sql"
},
{
"file": "DELETE.sql"
}
]
}

View File

@ -1,6 +1,12 @@
package com.external.plugins;
import com.appsmith.external.models.*;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.Endpoint;
import com.appsmith.external.models.Property;
import com.appsmith.external.pluginExceptions.StaleConnectionException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -9,7 +15,6 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.Batch;
import lombok.extern.log4j.Log4j;
import org.junit.Assert;
import org.junit.BeforeClass;
@ -119,8 +124,8 @@ public class MySqlPluginTest {
}
private static DatasourceConfiguration createDatasourceConfiguration() {
AuthenticationDTO authDTO = new AuthenticationDTO();
authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD);
DBAuth authDTO = new DBAuth();
authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD);
authDTO.setUsername(username);
authDTO.setPassword(password);
authDTO.setDatabaseName(database);
@ -147,8 +152,8 @@ public class MySqlPluginTest {
@Test
public void testConnectMySQLContainerWithInvalidTimezone() {
AuthenticationDTO authDTO = new AuthenticationDTO();
authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD);
DBAuth authDTO = new DBAuth();
authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD);
authDTO.setUsername(mySQLContainerWithInvalidTimezone.getUsername());
authDTO.setPassword(mySQLContainerWithInvalidTimezone.getPassword());
authDTO.setDatabaseName(mySQLContainerWithInvalidTimezone.getDatabaseName());
@ -227,9 +232,10 @@ public class MySqlPluginTest {
@Test
public void testValidateDatasourceNullCredentials() {
dsConfig.setConnection(new com.appsmith.external.models.Connection());
dsConfig.getAuthentication().setUsername(null);
dsConfig.getAuthentication().setPassword(null);
dsConfig.getAuthentication().setDatabaseName("someDbName");
DBAuth auth = (DBAuth) dsConfig.getAuthentication();
auth.setUsername(null);
auth.setPassword(null);
auth.setDatabaseName("someDbName");
Set<String> output = pluginExecutor.validateDatasource(dsConfig);
assertTrue(output.contains("Missing username for authentication."));
assertTrue(output.contains("Missing password for authentication."));
@ -237,10 +243,10 @@ public class MySqlPluginTest {
@Test
public void testValidateDatasourceMissingDBName() {
dsConfig.getAuthentication().setDatabaseName("");
((DBAuth) dsConfig.getAuthentication()).setDatabaseName("");
Set<String> output = pluginExecutor.validateDatasource(dsConfig);
assertEquals(output.size(), 1);
assertTrue(output.contains("Missing database name"));
assertTrue(output.contains("Missing database name."));
}
@Test

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.DatasourceTestResult;
@ -288,15 +288,16 @@ public class PostgresPlugin extends BasePlugin {
invalids.add("Missing authentication details.");
} else {
if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getUsername())) {
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (StringUtils.isEmpty(authentication.getUsername())) {
invalids.add("Missing username for authentication.");
}
if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getPassword())) {
if (StringUtils.isEmpty(authentication.getPassword())) {
invalids.add("Missing password for authentication.");
}
if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getDatabaseName())) {
if (StringUtils.isEmpty(authentication.getDatabaseName())) {
invalids.add("Missing database name.");
}
@ -509,7 +510,7 @@ public class PostgresPlugin extends BasePlugin {
config.addDataSourceProperty(SSL, isSslEnabled);
// Set authentication properties
AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication.getUsername() != null) {
config.setUsername(authentication.getUsername());
}

View File

@ -0,0 +1,16 @@
{
"templates": [
{
"file": "CREATE.sql"
},
{
"file": "SELECT.sql"
},
{
"file": "UPDATE.sql"
},
{
"file": "DELETE.sql"
}
]
}

View File

@ -3,6 +3,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.Endpoint;
@ -138,8 +139,8 @@ public class PostgresPluginTest {
}
private DatasourceConfiguration createDatasourceConfiguration() {
AuthenticationDTO authDTO = new AuthenticationDTO();
authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD);
DBAuth authDTO = new DBAuth();
authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD);
authDTO.setUsername(username);
authDTO.setPassword(password);
authDTO.setDatabaseName("postgres");

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.Endpoint;
@ -113,8 +113,8 @@ public class RedisPlugin extends BasePlugin {
Integer port = (int) (long) ObjectUtils.defaultIfNull(endpoint.getPort(), DEFAULT_PORT);
Jedis jedis = new Jedis(endpoint.getHost(), port);
AuthenticationDTO auth = datasourceConfiguration.getAuthentication();
if (auth != null && AuthenticationDTO.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) {
DBAuth auth = (DBAuth) datasourceConfiguration.getAuthentication();
if (auth != null && DBAuth.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) {
jedis.auth(auth.getUsername(), auth.getPassword());
}
@ -158,13 +158,13 @@ public class RedisPlugin extends BasePlugin {
}
}
AuthenticationDTO auth = datasourceConfiguration.getAuthentication();
if (auth != null && AuthenticationDTO.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) {
if (StringUtils.isNullOrEmpty(datasourceConfiguration.getAuthentication().getUsername())) {
DBAuth auth = (DBAuth) datasourceConfiguration.getAuthentication();
if (auth != null && DBAuth.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) {
if (StringUtils.isNullOrEmpty(auth.getUsername())) {
invalids.add("Missing username for authentication.");
}
if (StringUtils.isNullOrEmpty(datasourceConfiguration.getAuthentication().getPassword())) {
if (StringUtils.isNullOrEmpty(auth.getPassword())) {
invalids.add("Missing password for authentication.");
}
}

View File

@ -2,7 +2,7 @@ package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.Endpoint;
@ -99,8 +99,8 @@ public class RedisPluginTest {
Endpoint endpoint = new Endpoint();
endpoint.setHost("test-host");
AuthenticationDTO invalidAuth = new AuthenticationDTO();
invalidAuth.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD);
DBAuth invalidAuth = new DBAuth();
invalidAuth.setAuthType(DBAuth.Type.USERNAME_PASSWORD);
invalidDatasourceConfiguration.setAuthentication(invalidAuth);
invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint));
@ -115,8 +115,8 @@ public class RedisPluginTest {
public void itShouldValidateDatasource() {
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
AuthenticationDTO auth = new AuthenticationDTO();
auth.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD);
DBAuth auth = new DBAuth();
auth.setAuthType(DBAuth.Type.USERNAME_PASSWORD);
auth.setUsername("test-username");
auth.setPassword("test-password");

View File

@ -1,5 +1,7 @@
package com.appsmith.server.configurations;
import com.appsmith.external.annotations.DocumentTypeMapper;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.server.configurations.mongo.SoftDeleteMongoRepositoryFactoryBean;
import com.appsmith.server.repositories.BaseRepositoryImpl;
import com.github.cloudyrock.mongock.SpringBootMongock;
@ -8,10 +10,21 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.convert.DefaultTypeMapper;
import org.springframework.data.convert.SimpleTypeInformationMapper;
import org.springframework.data.convert.TypeInformationMapper;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoTypeMapper;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
import java.util.Arrays;
/**
* This configures the JPA Mongo repositories. The default base implementation is defined in {@link BaseRepositoryImpl}.
* This is required to add default clauses for default JPA queries defined by Spring Data.
@ -39,4 +52,28 @@ public class MongoConfig {
.build();
}
@Bean
public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MappingMongoConverter mappingMongoConverter) {
return new MongoTemplate(mongoDbFactory, mappingMongoConverter);
}
// Custom type mapper here includes our annotation based mapper that is meant to ensure correct mapping for sub-classes
// We have currently only included the package which contains the DTOs that need this mapping
@Bean
public DefaultTypeMapper typeMapper() {
TypeInformationMapper typeInformationMapper = new DocumentTypeMapper
.Builder()
.withBasePackages(new String[]{AuthenticationDTO.class.getPackageName()})
.build();
// This is a hack to include the default mapper as a fallback, because Spring seems to override its list instead of appending mappers
return new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, Arrays.asList(typeInformationMapper, new SimpleTypeInformationMapper()));
}
@Bean
public MappingMongoConverter mappingMongoConverter(DefaultTypeMapper typeMapper, MongoMappingContext context) {
MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context);
converter.setTypeMapper((MongoTypeMapper) typeMapper);
return converter;
}
}

View File

@ -9,17 +9,17 @@ import com.appsmith.server.services.UserOrganizationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.multipart.Part;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
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.RestController;
import reactor.core.publisher.Mono;
import java.util.List;

View File

@ -9,8 +9,6 @@ import lombok.Setter;
import lombok.ToString;
import org.springframework.data.mongodb.core.mapping.Document;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotBlank;
import java.util.List;

View File

@ -2,8 +2,9 @@ package com.appsmith.server.exceptions;
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
import com.appsmith.server.dtos.ResponseDTO;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.FieldError;
@ -13,17 +14,12 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import io.sentry.Sentry;
import io.sentry.SentryEvent;
import io.sentry.SentryOptions;
import io.sentry.SentryLevel;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.io.StringWriter;
import java.io.PrintWriter;
/**

View File

@ -74,7 +74,9 @@ public final class BeanCopyUtils {
Object targetValue = targetBeanWrapper.getPropertyValue(name);
if (targetValue != null && isDomainModel(propertyDescriptor.getPropertyType())) {
if (targetValue != null
&& sourceValue.getClass().isAssignableFrom(targetValue.getClass())
&& isDomainModel(propertyDescriptor.getPropertyType())) {
// Go deeper *only* if the property belongs to Appsmith's models, and both the source and target values
// are not null.
copyNestedNonNullProperties(sourceValue, targetValue);

View File

@ -1,7 +1,7 @@
package com.appsmith.server.migrations;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AppsmithRole;
import com.appsmith.server.constants.FieldName;
@ -38,14 +38,21 @@ import com.appsmith.server.services.OrganizationService;
import com.github.cloudyrock.mongock.ChangeLog;
import com.github.cloudyrock.mongock.ChangeSet;
import com.google.gson.Gson;
import com.mongodb.MongoException;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.Filters;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.apache.commons.lang.ObjectUtils;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.core.CollectionCallback;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.index.CompoundIndexDefinition;
import org.springframework.data.mongodb.core.index.Index;
@ -556,7 +563,7 @@ public class DatabaseChangelog {
);
for (final Datasource datasource : datasources) {
AuthenticationDTO authentication = datasource.getDatasourceConfiguration().getAuthentication();
DBAuth authentication = (DBAuth) datasource.getDatasourceConfiguration().getAuthentication();
authentication.setPassword(encryptionService.encryptString(authentication.getPassword()));
mongoTemplate.save(datasource);
}
@ -1408,4 +1415,84 @@ public class DatabaseChangelog {
}
}
@ChangeSet(order = "045", id = "update-authentication-type", author = "")
public void updateAuthenticationTypes(MongoTemplate mongoTemplate) {
mongoTemplate.execute("datasource", new CollectionCallback<String>() {
@Override
public String doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
// Only update _class for authentication objects that exist
MongoCursor cursor = collection.find(Filters.exists("datasourceConfiguration.authentication")).cursor();
while (cursor.hasNext()) {
Document current = (Document) cursor.next();
Document old = Document.parse(current.toJson());
// Extra precaution to only update _class for authentication objects that don't already have this
// Is this condition required? What does production datasource look like?
((Document) ((Document) current.get("datasourceConfiguration"))
.get("authentication"))
.putIfAbsent("_class", "dbAuth");
// Replace old document with the new one
collection.findOneAndReplace(old, current);
}
return null;
}
});
mongoTemplate.execute("newAction", new CollectionCallback<String>() {
@Override
public String doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
// Only update _class for authentication objects that exist
MongoCursor cursor = collection
.find(Filters.and(
Filters.exists("unpublishedAction.datasource"),
Filters.exists("unpublishedAction.datasource.datasourceConfiguration"),
Filters.exists("unpublishedAction.datasource.datasourceConfiguration.authentication"))).cursor();
while (cursor.hasNext()) {
Document current = (Document) cursor.next();
Document old = Document.parse(current.toJson());
// Extra precaution to only update _class for authentication objects that don't already have this
// Is this condition required? What does production datasource look like?
((Document) ((Document) ((Document) ((Document) current.get("unpublishedAction"))
.get("datasource"))
.get("datasourceConfiguration"))
.get("authentication"))
.putIfAbsent("_class", "dbAuth");
// Replace old document with the new one
collection.findOneAndReplace(old, current);
}
return null;
}
});
mongoTemplate.execute("newAction", new CollectionCallback<String>() {
@Override
public String doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
// Only update _class for authentication objects that exist
MongoCursor cursor = collection
.find(Filters.and(
Filters.exists("publishedAction.datasource"),
Filters.exists("publishedAction.datasource.datasourceConfiguration"),
Filters.exists("publishedAction.datasource.datasourceConfiguration.authentication"))).cursor();
while (cursor.hasNext()) {
Document current = (Document) cursor.next();
Document old = Document.parse(current.toJson());
// Extra precaution to only update _class for authentication objects that don't already have this
// Is this condition required? What does production datasource look like?
((Document) ((Document) ((Document) ((Document) current.get("publishedAction"))
.get("datasource"))
.get("datasourceConfiguration"))
.get("authentication"))
.putIfAbsent("_class", "dbAuth");
// Replace old document with the new one
collection.findOneAndReplace(old, current);
}
return null;
}
});
}
}

View File

@ -15,6 +15,7 @@ import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES;
@ -170,10 +171,15 @@ public class DatasourceContextServiceImpl implements DatasourceContextService {
}
@Override
public AuthenticationDTO decryptSensitiveFields(AuthenticationDTO authenticationDTO) {
if (authenticationDTO != null && authenticationDTO.getPassword() != null) {
authenticationDTO.setPassword(encryptionService.decryptString(authenticationDTO.getPassword()));
public AuthenticationDTO decryptSensitiveFields(AuthenticationDTO authentication) {
if (authentication != null && authentication.isEncrypted()) {
Map<String, String> decryptedFields = authentication.getEncryptionFields().entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> encryptionService.decryptString(e.getValue())));
authentication.setEncryptionFields(decryptedFields);
authentication.setIsEncrypted(false);
}
return authenticationDTO;
return authentication;
}
}

View File

@ -1,5 +1,6 @@
package com.appsmith.server.services;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Datasource;
@ -28,4 +29,6 @@ public interface DatasourceService extends CrudService<Datasource, String> {
Flux<Datasource> findAllByOrganizationId(String organizationId, AclPermission readDatasources);
Flux<Datasource> saveAll(List<Datasource> datasourceList);
AuthenticationDTO encryptAuthenticationFields(AuthenticationDTO authentication);
}

View File

@ -3,7 +3,6 @@ package com.appsmith.server.services;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.Endpoint;
import com.appsmith.external.models.Policy;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.acl.AclPermission;
@ -12,7 +11,6 @@ import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Datasource;
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.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
@ -36,6 +34,7 @@ import javax.validation.Validator;
import javax.validation.constraints.NotNull;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@ -111,23 +110,23 @@ public class DatasourceServiceImpl extends BaseService<DatasourceRepository, Dat
return datasourceMono
.flatMap(datasource1 ->
sessionUserService.getCurrentUser()
.flatMap(user -> {
// Create policies for this datasource -> This datasource should inherit its permissions and policies from
// the organization and this datasource should also allow the current user to crud this datasource.
return organizationService.findById(datasource1.getOrganizationId(), AclPermission.ORGANIZATION_MANAGE_APPLICATIONS)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, datasource1.getOrganizationId())))
.map(org -> {
Set<Policy> policySet = org.getPolicies().stream()
.filter(policy ->
policy.getPermission().equals(ORGANIZATION_MANAGE_APPLICATIONS.getValue()) ||
policy.getPermission().equals(ORGANIZATION_READ_APPLICATIONS.getValue())
).collect(Collectors.toSet());
.flatMap(user -> {
// Create policies for this datasource -> This datasource should inherit its permissions and policies from
// the organization and this datasource should also allow the current user to crud this datasource.
return organizationService.findById(datasource1.getOrganizationId(), AclPermission.ORGANIZATION_MANAGE_APPLICATIONS)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, datasource1.getOrganizationId())))
.map(org -> {
Set<Policy> policySet = org.getPolicies().stream()
.filter(policy ->
policy.getPermission().equals(ORGANIZATION_MANAGE_APPLICATIONS.getValue()) ||
policy.getPermission().equals(ORGANIZATION_READ_APPLICATIONS.getValue())
).collect(Collectors.toSet());
Set<Policy> documentPolicies = policyGenerator.getAllChildPolicies(policySet, Organization.class, Datasource.class);
datasource1.setPolicies(documentPolicies);
return datasource1;
});
})
Set<Policy> documentPolicies = policyGenerator.getAllChildPolicies(policySet, Organization.class, Datasource.class);
datasource1.setPolicies(documentPolicies);
return datasource1;
});
})
)
.flatMap(this::validateAndSaveDatasourceToRepository);
}
@ -157,10 +156,17 @@ public class DatasourceServiceImpl extends BaseService<DatasourceRepository, Dat
.flatMap(this::validateAndSaveDatasourceToRepository);
}
private AuthenticationDTO encryptAuthenticationFields(AuthenticationDTO authentication) {
// Encrypt password in AuthenticationDTO
if (authentication != null && authentication.getPassword() != null) {
authentication.setPassword(encryptionService.encryptString(authentication.getPassword()));
@Override
public AuthenticationDTO encryptAuthenticationFields(AuthenticationDTO authentication) {
if (authentication != null
&& CollectionUtils.isEmpty(authentication.getEmptyEncryptionFields())
&& !authentication.isEncrypted()) {
Map<String, String> encryptedFields = authentication.getEncryptionFields().entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> encryptionService.encryptString(e.getValue())));
authentication.setEncryptionFields(encryptedFields);
authentication.setIsEncrypted(true);
}
return authentication;
}
@ -232,30 +238,36 @@ public class DatasourceServiceImpl extends BaseService<DatasourceRepository, Dat
}
/**
* This function can now only be used if you send the entire datasource object and not just id inside the datasource object. We only fetch
* the password from the db if its a saved datasource before testing.
*/
* This function can now only be used if you send the entire datasource object and not just id inside the datasource object. We only fetch
* the password from the db if its a saved datasource before testing.
*/
@Override
public Mono<DatasourceTestResult> testDatasource(Datasource datasource) {
Mono<Datasource> datasourceMono = null;
// Fetch the password from the db if the datasource being tested does not have password set.
// Fetch any fields that maybe encrypted from the db if the datasource being tested does not have those fields set.
// This scenario would happen whenever an existing datasource is being tested and no changes are present in the
// password field (because password is not sent over the network after encryption back to the client
if (datasource.getId() != null && datasource.getDatasourceConfiguration()!=null &&
datasource.getDatasourceConfiguration().getAuthentication()!=null) {
String password = datasource.getDatasourceConfiguration().getAuthentication().getPassword();
if (password == null || password.isEmpty()) {
// encrypted field (because encrypted fields are not sent over the network after encryption back to the client
if (datasource.getId() != null && datasource.getDatasourceConfiguration() != null &&
datasource.getDatasourceConfiguration().getAuthentication() != null) {
Set<String> emptyFields = datasource.getDatasourceConfiguration().getAuthentication().getEmptyEncryptionFields();
if (emptyFields != null && !emptyFields.isEmpty()) {
datasourceMono = getById(datasource.getId())
// If datasource has encrypted password, decrypt and set it in the datasource which is being tested
.map(datasourceFromRepo-> {
if (datasourceFromRepo.getDatasourceConfiguration()!=null && datasourceFromRepo.getDatasourceConfiguration().getAuthentication()!=null) {
// If datasource has encrypted fields, decrypt and set it in the datasource which is being tested
.map(datasourceFromRepo -> {
if (datasourceFromRepo.getDatasourceConfiguration() != null && datasourceFromRepo.getDatasourceConfiguration().getAuthentication() != null) {
AuthenticationDTO authentication = datasourceFromRepo.getDatasourceConfiguration().getAuthentication();
if (authentication.getPassword() != null) {
String decryptedPassword = encryptionService.decryptString(authentication.getPassword());
datasource.getDatasourceConfiguration().getAuthentication().setPassword(decryptedPassword);
if (!authentication.getEncryptionFields().isEmpty()) {
Map<String, String> decryptedFields = authentication.getEncryptionFields();
decryptedFields = decryptedFields.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> encryptionService.decryptString(e.getValue())));
datasource.getDatasourceConfiguration().getAuthentication().setEncryptionFields(decryptedFields);
datasource.getDatasourceConfiguration().getAuthentication().setIsEncrypted(false);
}
}
return datasource;
})

View File

@ -7,8 +7,6 @@ import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.stereotype.Service;
import java.math.BigInteger;
@Service
public class EncryptionServiceImpl implements EncryptionService {
private final EncryptionConfig encryptionConfig;

View File

@ -10,7 +10,6 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

View File

@ -253,6 +253,17 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
Mono<Datasource> datasourceMono;
if (action.getDatasource().getId() == null) {
if (action.getDatasource().getDatasourceConfiguration() != null &&
action.getDatasource().getDatasourceConfiguration().getAuthentication() != null) {
action.getDatasource()
.getDatasourceConfiguration()
.setAuthentication(datasourceService.encryptAuthenticationFields(action
.getDatasource()
.getDatasourceConfiguration()
.getAuthentication()
));
}
datasourceMono = Mono.just(action.getDatasource())
.flatMap(datasourceService::validateDatasource);
} else {
@ -394,9 +405,6 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
// the update doesn't lead to resetting of this field.
action.setUserSetOnLoad(null);
NewAction newAction = new NewAction();
newAction.setUnpublishedAction(action);
Mono<NewAction> updatedActionMono = repository.findById(id, MANAGE_ACTIONS)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, id)))
.map(dbAction -> {
@ -412,7 +420,7 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
Mono<NewAction> analyticsUpdateMono = updatedActionMono
.flatMap(analyticsService::sendUpdateEvent);
// First Update the Action
// First Update the Action
return savedUpdatedActionMono
// Now send the update event to analytics service
.then(analyticsUpdateMono)
@ -438,38 +446,38 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
String actionId = executeActionDTO.getActionId();
// 2. Fetch the action from the DB and check if it can be executed
Mono<ActionDTO> actionMono = repository.findById(actionId, EXECUTE_ACTIONS)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId)))
.flatMap(dbAction -> {
ActionDTO action;
if (TRUE.equals(executeActionDTO.getViewMode())) {
action = dbAction.getPublishedAction();
// If the action has not been published, return error
if (action == null) {
return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId));
}
} else {
action = dbAction.getUnpublishedAction();
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId)))
.flatMap(dbAction -> {
ActionDTO action;
if (TRUE.equals(executeActionDTO.getViewMode())) {
action = dbAction.getPublishedAction();
// If the action has not been published, return error
if (action == null) {
return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId));
}
} else {
action = dbAction.getUnpublishedAction();
}
// Now check for erroneous situations which would deter the execution of the action :
// Now check for erroneous situations which would deter the execution of the action :
// Error out with in case of an invalid action
if (Boolean.FALSE.equals(action.getIsValid())) {
return Mono.error(new AppsmithException(
AppsmithError.INVALID_ACTION,
action.getName(),
actionId,
ArrayUtils.toString(action.getInvalids().toArray())
));
}
// Error out with in case of an invalid action
if (Boolean.FALSE.equals(action.getIsValid())) {
return Mono.error(new AppsmithException(
AppsmithError.INVALID_ACTION,
action.getName(),
actionId,
ArrayUtils.toString(action.getInvalids().toArray())
));
}
// Error out in case of JS Plugin (this is currently client side execution only)
if (dbAction.getPluginType() == PluginType.JS) {
return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION));
}
return Mono.just(action);
})
.cache();
// Error out in case of JS Plugin (this is currently client side execution only)
if (dbAction.getPluginType() == PluginType.JS) {
return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION));
}
return Mono.just(action);
})
.cache();
// 3. Instantiate the implementation class based on the query type
@ -683,7 +691,7 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
/**
* Given a list of names of actions and pageId, find all the actions matching this criteria of names and pageId
*
* @param names Set of Action names. The returned list of actions will be a subset of the actioned named in this set.
* @param names Set of Action names. The returned list of actions will be a subset of the actioned named in this set.
* @param pageId Id of the Page within which to look for Actions.
* @return A Flux of Actions that are identified to be executed on page-load.
*/

View File

@ -13,6 +13,7 @@ import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.repositories.PluginRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.pf4j.PluginManager;
@ -28,6 +29,7 @@ import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -44,6 +46,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -336,9 +339,13 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
templateCache.remove(pluginId)
)
// It's okay if the templates folder is not present, we just return empty templates collection.
.onErrorMap(throwable -> new AppsmithException(
AppsmithError.PLUGIN_LOAD_TEMPLATES_FAIL, Exceptions.unwrap(throwable).getMessage())
)
.onErrorMap(throwable -> {
log.error("Error loading templates for plugin {}.", plugin.getPackageName(), throwable);
return new AppsmithException(
AppsmithError.PLUGIN_LOAD_TEMPLATES_FAIL,
Exceptions.unwrap(throwable).getMessage()
);
})
.cache();
templateCache.put(pluginId, mono);
@ -354,27 +361,49 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
.getPluginClassLoader()
);
final Map<String, String> templates = new HashMap<>();
Resource[] resources;
final PluginTemplatesMeta pluginTemplatesMeta;
try {
resources = resolver.getResources("templates/*");
pluginTemplatesMeta = objectMapper.readValue(
resolver.getResource("templates/meta.json").getInputStream(),
PluginTemplatesMeta.class
);
} catch (IOException e) {
log.error("Error resolving templates in plugin for id: " + plugin.getId());
log.error("Error loading templates metadata in plugin for id: " + plugin.getId());
throw Exceptions.propagate(e);
}
for (final Resource resource : resources) {
final String filename = resource.getFilename();
if (pluginTemplatesMeta.getTemplates() == null) {
log.warn("Missing templates key in plugin templates meta.");
return Collections.emptyMap();
}
final Map<String, String> templates = new LinkedHashMap<>();
for (final PluginTemplate template : pluginTemplatesMeta.getTemplates()) {
final String filename = template.getFile();
if (filename == null) {
log.warn("Empty or missing file for a template in plugin {}.", plugin.getPackageName());
continue;
}
final Resource resource = resolver.getResource("templates/" + filename);
final String title = StringUtils.isEmpty(template.getTitle())
? filename.replaceFirst("\\.\\w+$", "")
: template.getTitle();
try {
final String templateContent = StreamUtils.copyToString(
resource.getInputStream(), Charset.defaultCharset());
if (filename != null) {
templates.put(filename.replaceFirst("\\.\\w+$", ""), templateContent);
}
templates.put(
title,
StreamUtils.copyToString(resource.getInputStream(), Charset.defaultCharset())
);
} catch (IOException e) {
log.error("Error loading template {} for plugin {}", filename, plugin.getId());
throw Exceptions.propagate(e);
}
}
@ -403,4 +432,15 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
}
});
}
@Data
static class PluginTemplatesMeta {
List<PluginTemplate> templates;
}
@Data
static class PluginTemplate {
String file;
String title = null;
}
}

View File

@ -16,9 +16,9 @@ import com.appsmith.server.domains.UserRole;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.notifications.EmailSender;
import com.appsmith.server.repositories.OrganizationRepository;
import com.appsmith.server.repositories.UserRepository;
import com.appsmith.server.notifications.EmailSender;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;

View File

@ -22,6 +22,8 @@ import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Map;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
@ -39,16 +41,16 @@ public class DatasourceStructureSolution {
public Mono<DatasourceStructure> getStructure(String datasourceId, boolean ignoreCache) {
return datasourceService.getById(datasourceId)
.flatMap(datasource -> getStructure(datasource, ignoreCache));
.flatMap(datasource -> getStructure(datasource, ignoreCache));
}
public Mono<DatasourceStructure> getStructure(Datasource datasource, boolean ignoreCache) {
// This mono, when computed, will yield the cached structure if applicable, or resolve to an empty mono.
// If the structure is `null` inside the datasource, this will resolve to empty as well.
// If the structure is `null` inside the datasource, this will resolve to empty as well.
final Mono<DatasourceStructure> cachedStructureMono =
ignoreCache ? Mono.empty() : Mono.justOrEmpty(datasource.getStructure());
ignoreCache ? Mono.empty() : Mono.justOrEmpty(datasource.getStructure());
decryptPasswordInDatasource(datasource);
decryptEncryptedFieldsInDatasource(datasource);
// This mono, when computed, will load the structure of the datasource by calling the plugin method.
final Mono<DatasourceStructure> loadStructureMono = pluginExecutorHelper
@ -83,12 +85,17 @@ public class DatasourceStructureSolution {
.defaultIfEmpty(new DatasourceStructure());
}
private Datasource decryptPasswordInDatasource(Datasource datasource) {
// If datasource has encrypted password, decrypt and set it in the datasource.
private Datasource decryptEncryptedFieldsInDatasource(Datasource datasource) {
// If datasource has encrypted fields, decrypt and set it in the datasource.
if (datasource.getDatasourceConfiguration() != null) {
AuthenticationDTO authentication = datasource.getDatasourceConfiguration().getAuthentication();
if (authentication != null && authentication.getPassword() != null) {
authentication.setPassword(encryptionService.decryptString(authentication.getPassword()));
if (authentication != null && authentication.getEmptyEncryptionFields().isEmpty() && authentication.isEncrypted()) {
Map<String, String> decryptedFields = authentication.getEncryptionFields().entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> encryptionService.decryptString(e.getValue())));
authentication.setEncryptionFields(decryptedFields);
authentication.setIsEncrypted(false);
}
}

View File

@ -1,6 +1,6 @@
package com.appsmith.server.services;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Datasource;
@ -64,7 +64,7 @@ public class DatasourceContextServiceTest {
datasource.setName("test datasource name for authenticated fields decryption test");
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
datasourceConfiguration.setUrl("http://test.com");
AuthenticationDTO authenticationDTO = new AuthenticationDTO();
DBAuth authenticationDTO = new DBAuth();
String username = "username";
String password = "password";
authenticationDTO.setUsername(username);
@ -81,8 +81,8 @@ public class DatasourceContextServiceTest {
StepVerifier
.create(datasourceMono)
.assertNext(savedDatasource -> {
AuthenticationDTO authentication = savedDatasource.getDatasourceConfiguration().getAuthentication();
AuthenticationDTO decryptedAuthentication = datasourceContextService.decryptSensitiveFields(authentication);
DBAuth authentication = (DBAuth) savedDatasource.getDatasourceConfiguration().getAuthentication();
DBAuth decryptedAuthentication = (DBAuth) datasourceContextService.decryptSensitiveFields(authentication);
assertThat(decryptedAuthentication.getPassword()).isEqualTo(password);
})
.verifyComplete();
@ -98,7 +98,7 @@ public class DatasourceContextServiceTest {
datasource.setName("test datasource name for authenticated fields decryption test null password");
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
datasourceConfiguration.setUrl("http://test.com");
AuthenticationDTO authenticationDTO = new AuthenticationDTO();
DBAuth authenticationDTO = new DBAuth();
datasourceConfiguration.setAuthentication(authenticationDTO);
datasource.setDatasourceConfiguration(datasourceConfiguration);
datasource.setOrganizationId(orgId);
@ -111,8 +111,8 @@ public class DatasourceContextServiceTest {
StepVerifier
.create(datasourceMono)
.assertNext(savedDatasource -> {
AuthenticationDTO authentication = savedDatasource.getDatasourceConfiguration().getAuthentication();
AuthenticationDTO decryptedAuthentication = datasourceContextService.decryptSensitiveFields(authentication);
DBAuth authentication = (DBAuth) savedDatasource.getDatasourceConfiguration().getAuthentication();
DBAuth decryptedAuthentication = (DBAuth) datasourceContextService.decryptSensitiveFields(authentication);
assertThat(decryptedAuthentication.getPassword()).isNull();
})
.verifyComplete();

View File

@ -1,11 +1,12 @@
package com.appsmith.server.services;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.Connection;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.Endpoint;
import com.appsmith.external.models.OAuth2;
import com.appsmith.external.models.Policy;
import com.appsmith.external.models.SSLDetails;
import com.appsmith.external.models.UploadedFile;
@ -74,7 +75,7 @@ public class DatasourceServiceTest {
@MockBean
PluginExecutorHelper pluginExecutorHelper;
String orgId = "";
String orgId = "";
@Before
@WithUserDetails(value = "api_user")
@ -234,9 +235,10 @@ public class DatasourceServiceTest {
Connection connection1 = new Connection();
SSLDetails ssl = new SSLDetails();
ssl.setKeyFile(new UploadedFile());
ssl.getKeyFile().setName("ssl_key_file_id");
ssl.getKeyFile().setName("ssl_key_file_id2");
connection1.setSsl(ssl);
datasourceConfiguration1.setConnection(connection1);
updates.setDatasourceConfiguration(datasourceConfiguration1);
return datasourceService.update(datasource1.getId(), updates);
});
@ -246,7 +248,72 @@ public class DatasourceServiceTest {
assertThat(createdDatasource.getId()).isNotEmpty();
assertThat(createdDatasource.getPluginId()).isEqualTo(datasource.getPluginId());
assertThat(createdDatasource.getName()).isEqualTo(datasource.getName());
assertThat(createdDatasource.getDatasourceConfiguration().getConnection().getSsl().getKeyFile().getName()).isEqualTo("ssl_key_file_id");
assertThat(createdDatasource.getDatasourceConfiguration().getConnection().getSsl().getKeyFile().getName()).isEqualTo("ssl_key_file_id2");
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void createAndUpdateDatasourceDifferentAuthentication() {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
Datasource datasource = new Datasource();
datasource.setName("test db datasource1");
datasource.setOrganizationId(orgId);
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
Connection connection = new Connection();
connection.setMode(Connection.Mode.READ_ONLY);
connection.setType(Connection.Type.REPLICA_SET);
SSLDetails sslDetails = new SSLDetails();
sslDetails.setAuthType(SSLDetails.AuthType.CA_CERTIFICATE);
sslDetails.setKeyFile(new UploadedFile("ssl_key_file_id", ""));
sslDetails.setCertificateFile(new UploadedFile("ssl_cert_file_id", ""));
connection.setSsl(sslDetails);
datasourceConfiguration.setConnection(connection);
DBAuth auth = new DBAuth();
auth.setUsername("test");
auth.setPassword("test");
datasourceConfiguration.setAuthentication(auth);
datasource.setDatasourceConfiguration(datasourceConfiguration);
datasource.setOrganizationId(orgId);
Mono<Plugin> pluginMono = pluginService.findByName("Installed Plugin Name");
Mono<Datasource> datasourceMono = pluginMono
.map(plugin -> {
datasource.setPluginId(plugin.getId());
return datasource;
})
.flatMap(datasourceService::create)
.flatMap(datasource1 -> {
Datasource updates = new Datasource();
DatasourceConfiguration datasourceConfiguration1 = new DatasourceConfiguration();
Connection connection1 = new Connection();
SSLDetails ssl = new SSLDetails();
ssl.setKeyFile(new UploadedFile());
ssl.getKeyFile().setName("ssl_key_file_id2");
connection1.setSsl(ssl);
OAuth2 auth2 = new OAuth2();
auth2.setClientId("test");
auth2.setClientSecret("test");
datasourceConfiguration1.setAuthentication(auth2);
datasourceConfiguration1.setConnection(connection1);
updates.setDatasourceConfiguration(datasourceConfiguration1);
return datasourceService.update(datasource1.getId(), updates);
});
StepVerifier
.create(datasourceMono)
.assertNext(createdDatasource -> {
assertThat(createdDatasource.getId()).isNotEmpty();
assertThat(createdDatasource.getPluginId()).isEqualTo(datasource.getPluginId());
assertThat(createdDatasource.getName()).isEqualTo(datasource.getName());
assertThat(createdDatasource.getDatasourceConfiguration().getConnection().getSsl().getKeyFile().getName()).isEqualTo("ssl_key_file_id2");
assertThat(createdDatasource.getDatasourceConfiguration().getAuthentication() instanceof OAuth2).isTrue();
})
.verifyComplete();
}
@ -270,10 +337,10 @@ public class DatasourceServiceTest {
final Mono<Tuple2<Datasource, Datasource>> datasourcesMono = pluginMono
.flatMap(plugin -> {
datasource1.setPluginId(plugin.getId());
datasource2.setPluginId(plugin.getId());
return datasourceService.create(datasource1);
})
datasource1.setPluginId(plugin.getId());
datasource2.setPluginId(plugin.getId());
return datasourceService.create(datasource1);
})
.zipWhen(datasource -> datasourceService.create(datasource2));
StepVerifier
@ -323,6 +390,54 @@ public class DatasourceServiceTest {
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void testDatasourceEmptyFields() {
Datasource datasource = new Datasource();
datasource.setName("test db datasource empty");
datasource.setOrganizationId(orgId);
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
Connection connection = new Connection();
connection.setMode(Connection.Mode.READ_ONLY);
connection.setType(Connection.Type.REPLICA_SET);
SSLDetails sslDetails = new SSLDetails();
sslDetails.setAuthType(SSLDetails.AuthType.CA_CERTIFICATE);
sslDetails.setKeyFile(new UploadedFile("ssl_key_file_id", ""));
sslDetails.setCertificateFile(new UploadedFile("ssl_cert_file_id", ""));
connection.setSsl(sslDetails);
datasourceConfiguration.setConnection(connection);
DBAuth auth = new DBAuth();
auth.setUsername("test");
auth.setPassword("test");
datasourceConfiguration.setAuthentication(auth);
datasource.setDatasourceConfiguration(datasourceConfiguration);
datasource.setOrganizationId(orgId);
Mono<Plugin> pluginMono = pluginService.findByName("Installed Plugin Name");
Mono<Datasource> datasourceMono = pluginMono.map(plugin -> {
datasource.setPluginId(plugin.getId());
return datasource;
}).flatMap(datasourceService::create);
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
Mono<DatasourceTestResult> testResultMono = datasourceMono.flatMap(datasource1 -> {
((DBAuth) datasource1.getDatasourceConfiguration().getAuthentication()).setPassword(null);
return datasourceService.testDatasource(datasource1);
});
StepVerifier
.create(testResultMono)
.assertNext(testResult -> {
assertThat(testResult).isNotNull();
assertThat(testResult.getInvalids()).isEmpty();
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void deleteDatasourceWithoutActions() {
@ -424,13 +539,13 @@ public class DatasourceServiceTest {
@WithUserDetails(value = "api_user")
public void checkEncryptionOfAuthenticationDTOTest() {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
Mono<Plugin> pluginMono = pluginService.findByName("Installed Plugin Name");
Datasource datasource = new Datasource();
datasource.setName("test datasource name for authenticated fields encryption test");
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
datasourceConfiguration.setUrl("http://test.com");
AuthenticationDTO authenticationDTO = new AuthenticationDTO();
DBAuth authenticationDTO = new DBAuth();
String username = "username";
String password = "password";
authenticationDTO.setUsername(username);
@ -447,7 +562,7 @@ public class DatasourceServiceTest {
StepVerifier
.create(datasourceMono)
.assertNext(savedDatasource -> {
AuthenticationDTO authentication = savedDatasource.getDatasourceConfiguration().getAuthentication();
DBAuth authentication = (DBAuth) savedDatasource.getDatasourceConfiguration().getAuthentication();
assertThat(authentication.getUsername()).isEqualTo(username);
assertThat(authentication.getPassword()).isEqualTo(encryptionService.encryptString(password));
})
@ -464,7 +579,7 @@ public class DatasourceServiceTest {
datasource.setName("test datasource name for authenticated fields encryption test null password.");
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
datasourceConfiguration.setUrl("http://test.com");
AuthenticationDTO authenticationDTO = new AuthenticationDTO();
DBAuth authenticationDTO = new DBAuth();
authenticationDTO.setDatabaseName("admin");
datasourceConfiguration.setAuthentication(authenticationDTO);
datasource.setDatasourceConfiguration(datasourceConfiguration);
@ -478,9 +593,10 @@ public class DatasourceServiceTest {
StepVerifier
.create(datasourceMono)
.assertNext(savedDatasource -> {
AuthenticationDTO authentication = savedDatasource.getDatasourceConfiguration().getAuthentication();
DBAuth authentication = (DBAuth) savedDatasource.getDatasourceConfiguration().getAuthentication();
assertThat(authentication.getUsername()).isNull();
assertThat(authentication.getPassword()).isNull();
assertThat(authentication.isEncrypted()).isFalse();
})
.verifyComplete();
}
@ -495,7 +611,7 @@ public class DatasourceServiceTest {
datasource.setName("test datasource name for authenticated fields encryption test post update");
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
datasourceConfiguration.setUrl("http://test.com");
AuthenticationDTO authenticationDTO = new AuthenticationDTO();
DBAuth authenticationDTO = new DBAuth();
String username = "username";
String password = "password";
authenticationDTO.setUsername(username);
@ -519,9 +635,10 @@ public class DatasourceServiceTest {
StepVerifier
.create(datasourceMono)
.assertNext(updatedDatasource -> {
AuthenticationDTO authentication = updatedDatasource.getDatasourceConfiguration().getAuthentication();
DBAuth authentication = (DBAuth) updatedDatasource.getDatasourceConfiguration().getAuthentication();
assertThat(authentication.getUsername()).isEqualTo(username);
assertThat(authentication.getPassword()).isEqualTo(encryptionService.encryptString(password));
assertThat(authentication.isEncrypted()).isTrue();
})
.verifyComplete();
}

View File

@ -1,7 +1,7 @@
package com.appsmith.server.solutions;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Property;
import com.appsmith.server.constants.FieldName;
@ -361,8 +361,9 @@ public class ExamplesOrganizationClonerTests {
ds2.setName("datasource 2");
ds2.setOrganizationId(organization.getId());
ds2.setDatasourceConfiguration(new DatasourceConfiguration());
ds2.getDatasourceConfiguration().setAuthentication(new AuthenticationDTO());
ds2.getDatasourceConfiguration().getAuthentication().setPassword("answer-to-life");
DBAuth auth = new DBAuth();
auth.setPassword("answer-to-life");
ds2.getDatasourceConfiguration().setAuthentication(auth);
return Mono.when(
datasourceService.create(ds1),
@ -398,7 +399,7 @@ public class ExamplesOrganizationClonerTests {
.findFirst()
.orElseThrow();
assertThat(ds2.getDatasourceConfiguration().getAuthentication()).isNotNull();
assertThat(ds2.getDatasourceConfiguration().getAuthentication().getPassword())
assertThat(((DBAuth) ds2.getDatasourceConfiguration().getAuthentication()).getPassword())
.isEqualTo(encryptionService.encryptString("answer-to-life"));
assertThat(data.applications).isEmpty();

View File

@ -524,8 +524,8 @@ echo "Installing Appsmith to '$install_dir'."
mkdir -p "$install_dir"
echo ""
if confirm y "Would you like to initialize the default database?"; then
echo "Appsmith needs to create a MongoDB instance."
echo "Appsmith needs a MongoDB instance to run"
if confirm y "Initialise a new database? (Recommended)"; then
mongo_host="mongo"
mongo_database="appsmith"