diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java index 37fc3534a8..65c5bd5414 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java @@ -16,6 +16,7 @@ public interface PluginConstants { String ANTHROPIC_PLUGIN = "anthropic-plugin"; String GOOGLE_AI_PLUGIN = "googleai-plugin"; String DATABRICKS_PLUGIN = "databricks-plugin"; + String AWS_LAMBDA_PLUGIN = "aws-lambda-plugin"; } public static final String DEFAULT_REST_DATASOURCE = "DEFAULT_REST_DATASOURCE"; @@ -43,6 +44,7 @@ public interface PluginConstants { public static final String ANTHROPIC_PLUGIN_NAME = "Anthropic"; public static final String GOOGLE_AI_PLUGIN_NAME = "Google AI"; public static final String DATABRICKS_PLUGIN_NAME = "Databricks"; + public static final String AWS_LAMBDA_PLUGIN_NAME = "AWS Lambda"; } interface HostName { diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/pom.xml b/app/server/appsmith-plugins/awsLambdaPlugin/pom.xml new file mode 100644 index 0000000000..d89ae8093b --- /dev/null +++ b/app/server/appsmith-plugins/awsLambdaPlugin/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + com.appsmith + appsmith-plugins + 1.0-SNAPSHOT + + + com.external.plugins + awsLambdaPlugin + 1.0-SNAPSHOT + awsLambdaPlugin + http://maven.apache.org + + + + com.amazonaws + aws-java-sdk-lambda + 1.12.622 + + + com.fasterxml.jackson.core + * + + + + + com.amazonaws + aws-java-sdk-osgi + 1.12.622 + + + com.fasterxml.jackson.core + * + + + + + + + io.projectreactor + reactor-test + 3.2.11.RELEASE + test + + + org.mockito + mockito-core + 3.1.0 + test + + + + + + + + maven-shade-plugin + + + maven-dependency-plugin + + + copy-dependencies + + copy-dependencies + + package + + runtime + ${project.build.directory}/lib + + + + + + + + diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/java/com/external/plugins/AwsLambdaPlugin.java b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/java/com/external/plugins/AwsLambdaPlugin.java new file mode 100644 index 0000000000..8a4757400c --- /dev/null +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/java/com/external/plugins/AwsLambdaPlugin.java @@ -0,0 +1,242 @@ +package com.external.plugins; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.lambda.AWSLambda; +import com.amazonaws.services.lambda.AWSLambdaClientBuilder; +import com.amazonaws.services.lambda.model.AWSLambdaException; +import com.amazonaws.services.lambda.model.FunctionConfiguration; +import com.amazonaws.services.lambda.model.InvokeRequest; +import com.amazonaws.services.lambda.model.InvokeResult; +import com.amazonaws.services.lambda.model.ListFunctionsResult; +import com.amazonaws.services.lambda.model.ResourceNotFoundException; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +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.DatasourceTestResult; +import com.appsmith.external.models.TriggerRequestDTO; +import com.appsmith.external.models.TriggerResultDTO; +import com.appsmith.external.plugins.BasePlugin; +import com.appsmith.external.plugins.PluginExecutor; +import com.fasterxml.jackson.databind.node.ArrayNode; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.Extension; +import org.pf4j.PluginWrapper; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static com.appsmith.external.helpers.PluginUtils.STRING_TYPE; +import static com.appsmith.external.helpers.PluginUtils.getDataValueSafelyFromFormData; + +public class AwsLambdaPlugin extends BasePlugin { + + public AwsLambdaPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Slf4j + @Extension + public static class AwsLambdaPluginExecutor implements PluginExecutor { + + @Override + public Mono execute( + AWSLambda connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + + Map formData = actionConfiguration.getFormData(); + String command = getDataValueSafelyFromFormData(formData, "command", STRING_TYPE); + + return Mono.fromCallable(() -> { + ActionExecutionResult result; + switch (Objects.requireNonNull(command)) { + case "LIST_FUNCTIONS" -> result = listFunctions(actionConfiguration, connection); + case "INVOKE_FUNCTION" -> result = invokeFunction(actionConfiguration, connection); + default -> throw new IllegalStateException("Unexpected value: " + command); + } + + return result; + }) + .onErrorMap( + IllegalArgumentException.class, + e -> new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, "Unsupported command: " + command)) + .onErrorMap( + ResourceNotFoundException.class, + e -> new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getErrorMessage())) + .onErrorMap( + Exception.class, + e -> new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage())) + .map(obj -> obj) + .subscribeOn(Schedulers.boundedElastic()); + } + + @Override + public Mono trigger( + AWSLambda connection, DatasourceConfiguration datasourceConfiguration, TriggerRequestDTO request) { + if (!StringUtils.hasText(request.getRequestType())) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "request type is missing"); + } + ActionExecutionResult actionExecutionResult = listFunctions(null, connection); + ArrayNode body = (ArrayNode) actionExecutionResult.getBody(); + List> functionNames = StreamSupport.stream(body.spliterator(), false) + .map(function -> function.get("functionName").asText()) + .sorted() + .map(functionName -> Map.of("label", functionName, "value", functionName)) + .collect(Collectors.toList()); + + TriggerResultDTO triggerResultDTO = new TriggerResultDTO(); + triggerResultDTO.setTrigger(functionNames); + + return Mono.just(triggerResultDTO); + } + + ActionExecutionResult invokeFunction(ActionConfiguration actionConfiguration, AWSLambda connection) { + InvokeRequest invokeRequest = new InvokeRequest(); + invokeRequest.setFunctionName( + getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "functionName", STRING_TYPE)); + invokeRequest.setPayload( + getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "body", STRING_TYPE)); + invokeRequest.setInvocationType( + getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "invocationType", STRING_TYPE)); + InvokeResult invokeResult = connection.invoke(invokeRequest); + + ActionExecutionResult result = new ActionExecutionResult(); + result.setStatusCode(String.valueOf(invokeResult.getStatusCode())); + Boolean isExecutionSuccess = (invokeResult.getFunctionError() == null); + result.setIsExecutionSuccess(isExecutionSuccess); + ByteBuffer responseBuffer = invokeResult.getPayload(); + String responsePayload = ObjectUtils.isEmpty(responseBuffer) + ? null + : new String(responseBuffer.array(), StandardCharsets.UTF_8); + result.setBody(responsePayload); + + return result; + } + + ActionExecutionResult listFunctions(ActionConfiguration actionConfiguration, AWSLambda connection) { + ListFunctionsResult listFunctionsResult = connection.listFunctions(); + List functions = listFunctionsResult.getFunctions(); + + ActionExecutionResult result = new ActionExecutionResult(); + result.setBody(objectMapper.valueToTree(functions)); + result.setIsExecutionSuccess(true); + return result; + } + + @Override + public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); + String accessKey = authentication.getUsername(); + String secretKey = authentication.getPassword(); + String authenticationType = authentication.getAuthenticationType(); + String region = + (String) datasourceConfiguration.getProperties().get(1).getValue(); + + if (!StringUtils.hasText(region)) { + region = "us-east-1"; // Default region + } + + AWSLambdaClientBuilder awsLambdaClientBuilder = + AWSLambdaClientBuilder.standard().withRegion(Regions.fromName(region)); + + // If access key and secret key are not provided, use the default credentials provider chain. That will + // pick up the instance role if running on an EC2 instance. + if ("accessKey".equals(authenticationType)) { + BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey); + + AWSStaticCredentialsProvider staticCredentials = new AWSStaticCredentialsProvider(awsCreds); + + awsLambdaClientBuilder = awsLambdaClientBuilder.withCredentials(staticCredentials); + } + + AWSLambda awsLambda = awsLambdaClientBuilder.build(); + return Mono.just(awsLambda); + } + + @Override + public void datasourceDestroy(AWSLambda connection) { + // No need to do anything here. + } + + @Override + public Mono testDatasource(AWSLambda connection) { + return Mono.fromCallable(() -> { + /* + * - Please note that as of 28 Jan 2021, the way Amazon client SDK works, creating a connection + * object with wrong credentials does not throw any exception. + * - Hence, adding a listFunctions() method call to test the connection. + */ + connection.listFunctions(); + return new DatasourceTestResult(); + }) + .onErrorResume(error -> { + if (error instanceof AWSLambdaException + && "AccessDenied".equals(((AWSLambdaException) error).getErrorCode())) { + /* + * Sometimes a valid account credential may not have permission to run listFunctions action + * . In this case `AccessDenied` error is returned. + * That fact that the credentials caused `AccessDenied` error instead of invalid access key + * id or signature mismatch error means that the credentials are valid, we are able to + * establish a connection as well, but the account does not have permission to run + * listFunctions. + */ + return Mono.just(new DatasourceTestResult()); + } + + return Mono.just(new DatasourceTestResult(error.getMessage())); + }); + } + + @Override + public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) { + Set invalids = new HashSet<>(); + if (datasourceConfiguration == null + || datasourceConfiguration.getAuthentication() == null + || !StringUtils.hasText( + datasourceConfiguration.getAuthentication().getAuthenticationType())) { + invalids.add("Invalid authentication mechanism provided. Please choose valid authentication type."); + return invalids; + } + + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); + + if ("instanceRole".equals(authentication.getAuthenticationType()) + && "true".equalsIgnoreCase(System.getenv("APPSMITH_CLOUD_HOSTING"))) { + // Instance role is not supported for cloud hosting. It's only supported for self-hosted environments. + // This is to prevent a security risk where a user can use the instance role to access resources in a + // hosted environment. + invalids.add( + "Instance role is not supported for cloud hosting. Please choose a different authentication type."); + } else if ("accessKey".equals(authentication.getAuthenticationType())) { + // Only check for access key and secret key if accessKey authentication is selected. + if (!StringUtils.hasText(authentication.getUsername())) { + invalids.add("Unable to find an AWS access key. Please add a valid access key."); + } + + if (!StringUtils.hasText(authentication.getPassword())) { + invalids.add("Unable to find an AWS secret key. Please add a valid secret key."); + } + } + + return invalids; + } + } +} diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/invoke.json b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/invoke.json new file mode 100644 index 0000000000..ef5d12fab3 --- /dev/null +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/invoke.json @@ -0,0 +1,81 @@ +{ + "identifier": "INVOKE_FUNCTION", + "controlType": "SECTION", + "conditionals": { + "show": "{{actionConfiguration.formData.command.data === 'INVOKE_FUNCTION'}}" + }, + "children": [ + { + "controlType": "SECTION", + "label": "Details of lambda function", + "children": [ + { + "label": "Function to invoke", + "tooltipText": "This is the name of the AWS lambda function that will be invoked.", + "subtitle": "", + "isRequired": true, + "propertyName": "function_name", + "configProperty": "actionConfiguration.formData.functionName.data", + "controlType": "DROP_DOWN", + "initialValue": "", + "options": [], + "placeholderText": "All function names will be fetched.", + "fetchOptionsConditionally": true, + "setFirstOptionAsDefault": true, + "alternateViewTypes": [ + "json" + ], + "conditionals": { + "enable": "{{true}}", + "fetchDynamicValues": { + "condition": "{{actionConfiguration.formData.command.data === 'INVOKE_FUNCTION'}}", + "config": { + "params": { + "requestType": "FUNCTION_NAMES", + "displayType": "DROP_DOWN" + } + } + } + } + }, + { + "label": "Type of invocation", + "tooltipText": "Should the invocation be synchronous or asynchronous?", + "subtitle": "", + "isRequired": true, + "propertyName": "invocation_type", + "configProperty": "actionConfiguration.formData.invocationType.data", + "controlType": "DROP_DOWN", + "initialValue": "", + "options": [ + { + "label": "Synchronous", + "value": "RequestResponse" + }, + { + "label": "Asynchronous", + "value": "Event" + }, + { + "label": "Dry run", + "value": "DryRun" + } + ], + "placeholderText": "", + "fetchOptionsConditionally": false, + "setFirstOptionAsDefault": true, + "alternateViewTypes": [ + "json" + ] + }, + { + "label": "Post body", + "configProperty": "actionConfiguration.formData.body.data", + "controlType": "QUERY_DYNAMIC_TEXT", + "initialValue": "", + "placeHolderText": "{`\"key1\": \"value1\"`}" + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/list.json b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/list.json new file mode 100644 index 0000000000..72e004a0c0 --- /dev/null +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/list.json @@ -0,0 +1,8 @@ +{ + "identifier": "LIST_FUNCTIONS", + "controlType": "SECTION", + "conditionals": { + "show": "{{actionConfiguration.formData.command.data === 'LIST_FUNCTIONS'}}" + }, + "children": [] +} diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/root.json b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/root.json new file mode 100644 index 0000000000..279428fb4f --- /dev/null +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/root.json @@ -0,0 +1,31 @@ +{ + "editor": [ + { + "controlType": "SECTION", + "identifier": "SELECTOR", + "children": [ + { + "label": "Commands", + "description": "Choose the method you would like to use", + "configProperty": "actionConfiguration.formData.command.data", + "controlType": "DROP_DOWN", + "initialValue": "LIST_FUNCTIONS", + "options": [ + { + "label": "List all functions", + "value": "LIST_FUNCTIONS" + }, + { + "label": "Invoke a function", + "value": "INVOKE_FUNCTION" + } + ] + } + ] + } + ], + "files": [ + "list.json", + "invoke.json" + ] +} diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/form.json new file mode 100644 index 0000000000..80c12fcf08 --- /dev/null +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/form.json @@ -0,0 +1,60 @@ +{ + "form": [ + { + "sectionName": "Details", + "id": 1, + "children": [ + { + "label": "Authentication type", + "configProperty": "datasourceConfiguration.authentication.authenticationType", + "controlType": "DROP_DOWN", + "initialValue": "accessKey", + "setFirstOptionAsDefault": true, + "options": [ + { + "label": "AWS access key", + "value": "accessKey" + }, + { + "label": "Instance role", + "value": "instanceRole" + } + ] + }, + { + "label": "Access key", + "configProperty": "datasourceConfiguration.authentication.username", + "controlType": "INPUT_TEXT", + "isRequired": true, + "initialValue": "", + "hidden": { + "path": "datasourceConfiguration.authentication.authenticationType", + "comparison": "NOT_EQUALS", + "value": "accessKey" + } + }, + { + "label": "Secret key", + "configProperty": "datasourceConfiguration.authentication.password", + "controlType": "INPUT_TEXT", + "dataType": "PASSWORD", + "initialValue": "", + "isRequired": true, + "encrypted": true, + "hidden": { + "path": "datasourceConfiguration.authentication.authenticationType", + "comparison": "NOT_EQUALS", + "value": "accessKey" + } + }, + { + "label": "Region", + "configProperty": "datasourceConfiguration.properties[1].value", + "controlType": "INPUT_TEXT", + "initialValue": "", + "placeholderText": "us-east-1" + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/plugin.properties b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/plugin.properties new file mode 100644 index 0000000000..c573bb1802 --- /dev/null +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=aws-lambda-plugin +plugin.class=com.external.plugins.AwsLambdaPlugin +plugin.version=1.0-SNAPSHOT +plugin.provider=tech@appsmith.com +plugin.dependencies= diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/test/java/com/external/plugins/AwsLambdaPluginTest.java b/app/server/appsmith-plugins/awsLambdaPlugin/src/test/java/com/external/plugins/AwsLambdaPluginTest.java new file mode 100644 index 0000000000..818031378b --- /dev/null +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/test/java/com/external/plugins/AwsLambdaPluginTest.java @@ -0,0 +1,196 @@ +package com.external.plugins; + +import com.amazonaws.services.lambda.AWSLambda; +import com.amazonaws.services.lambda.model.FunctionConfiguration; +import com.amazonaws.services.lambda.model.InvokeResult; +import com.amazonaws.services.lambda.model.ListFunctionsResult; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +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.Property; +import com.appsmith.external.models.TriggerRequestDTO; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Testcontainers; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.appsmith.external.helpers.PluginUtils.setDataValueSafelyInFormData; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Testcontainers +public class AwsLambdaPluginTest { + + private static String accessKey; + private static String secretKey; + private static String region; + + AwsLambdaPlugin.AwsLambdaPluginExecutor pluginExecutor = new AwsLambdaPlugin.AwsLambdaPluginExecutor(); + + @BeforeAll + public static void setUp() { + accessKey = "random_access_key"; + secretKey = "random_secret_key"; + region = "ap-south-1"; + } + + private DatasourceConfiguration createDatasourceConfiguration() { + DBAuth authDTO = new DBAuth(); + authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); + authDTO.setUsername(accessKey); + authDTO.setPassword(secretKey); + + DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + dsConfig.setAuthentication(authDTO); + ArrayList properties = new ArrayList<>(); + properties.add(null); // since index 0 is not used anymore. + properties.add(new Property("region", region)); + dsConfig.setProperties(properties); + return dsConfig; + } + + @Test + public void testExecuteListFunctions() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + Map configMap = new HashMap<>(); + setDataValueSafelyInFormData(configMap, "command", "LIST_FUNCTIONS"); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setFormData(configMap); + + // Mock the Lambda connection + AWSLambda mockLambda = mock(AWSLambda.class); + ListFunctionsResult mockFunctionsResult = new ListFunctionsResult(); + mockFunctionsResult.setFunctions(List.of(new FunctionConfiguration().withFunctionName("test-aws-lambda"))); + when(mockLambda.listFunctions()).thenReturn(mockFunctionsResult); + + Mono resultMono = + pluginExecutor.execute(mockLambda, datasourceConfiguration, actionConfiguration); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertEquals(1, ((ArrayNode) result.getBody()).size()); + }) + .verifyComplete(); + } + + @Test + public void testExecuteInvokeFunction() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + Map configMap = new HashMap<>(); + setDataValueSafelyInFormData(configMap, "command", "INVOKE_FUNCTION"); + setDataValueSafelyInFormData(configMap, "body", "{\"data\": \"\"}"); + setDataValueSafelyInFormData(configMap, "functionName", "test-aws-lambda"); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setFormData(configMap); + + // Mock the Lambda connection + AWSLambda mockLambda = mock(AWSLambda.class); + InvokeResult mockResult = new InvokeResult(); + mockResult.setPayload(ByteBuffer.wrap("Hello World".getBytes())); + when(mockLambda.invoke(any())).thenReturn(mockResult); + + Mono resultMono = + pluginExecutor.execute(mockLambda, datasourceConfiguration, actionConfiguration); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + assertEquals("Hello World", result.getBody().toString()); + }) + .verifyComplete(); + } + + @Test + public void testValidateDatasource_missingDatasourceConfiguration() { + // Test case: Missing datasource configuration + Set invalids = pluginExecutor.validateDatasource(null); + assertEquals(1, invalids.size()); + assertTrue(invalids.contains( + "Invalid authentication mechanism provided. Please choose valid authentication type.")); + } + + @Test + public void testValidateDatasource_missingAccessKey() { + // Test case: Missing AWS access key + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + DBAuth authentication = new DBAuth(); + authentication.setAuthenticationType("accessKey"); + authentication.setPassword("random_secret_key"); + datasourceConfiguration.setAuthentication(authentication); + Set invalids = pluginExecutor.validateDatasource(datasourceConfiguration); + assertEquals(1, invalids.size()); + assertTrue(invalids.contains("Missing AWS access key")); + } + + @Test + public void testValidateDatasource_missingSecretKey() { + AwsLambdaPlugin.AwsLambdaPluginExecutor pluginExecutor = new AwsLambdaPlugin.AwsLambdaPluginExecutor(); + + // Test case: Missing AWS secret key + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + DBAuth authentication = new DBAuth(); + authentication.setAuthenticationType("accessKey"); + authentication.setUsername("random_access_key"); + authentication.setPassword(null); + datasourceConfiguration.setAuthentication(authentication); + Set invalids = pluginExecutor.validateDatasource(datasourceConfiguration); + assertEquals(1, invalids.size()); + assertTrue(invalids.contains("Missing AWS secret key")); + } + + @Test + public void testValidateDatasource_validConfigurationForAccessKey() { + AwsLambdaPlugin.AwsLambdaPluginExecutor pluginExecutor = new AwsLambdaPlugin.AwsLambdaPluginExecutor(); + + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + DBAuth authentication = new DBAuth(); + authentication.setAuthenticationType("accessKey"); + authentication.setUsername("random_access_key"); + authentication.setPassword("random_secret_key"); + datasourceConfiguration.setAuthentication(authentication); + Set invalids = pluginExecutor.validateDatasource(datasourceConfiguration); + assertEquals(0, invalids.size()); + } + + @Test + public void testValidateDatasource_validConfigurationForInstanceRole() { + AwsLambdaPlugin.AwsLambdaPluginExecutor pluginExecutor = new AwsLambdaPlugin.AwsLambdaPluginExecutor(); + + // Test case: Valid datasource configuration + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + DBAuth authentication = new DBAuth(); + authentication.setAuthenticationType("instanceRole"); + datasourceConfiguration.setAuthentication(authentication); + Set invalids = pluginExecutor.validateDatasource(datasourceConfiguration); + assertEquals(0, invalids.size()); + } + + @Test + public void testTrigger_missingRequestType() { + // Test case: Missing request type + AWSLambda mockLambda = mock(AWSLambda.class); + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + TriggerRequestDTO request = new TriggerRequestDTO(); + + assertThrows(AppsmithPluginException.class, () -> { + pluginExecutor.trigger(mockLambda, datasourceConfiguration, request).block(); + }); + } +} diff --git a/app/server/appsmith-plugins/pom.xml b/app/server/appsmith-plugins/pom.xml index 3737443035..bea3d5e16d 100644 --- a/app/server/appsmith-plugins/pom.xml +++ b/app/server/appsmith-plugins/pom.xml @@ -66,6 +66,8 @@ googleAiPlugin + awsLambdaPlugin + databricksPlugin diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration040AddAWSLambdaPlugin.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration040AddAWSLambdaPlugin.java new file mode 100644 index 0000000000..876314a6e2 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration040AddAWSLambdaPlugin.java @@ -0,0 +1,54 @@ +package com.appsmith.server.migrations.db.ce; + +import com.appsmith.external.constants.PluginConstants; +import com.appsmith.external.models.PluginType; +import com.appsmith.server.domains.Plugin; +import io.mongock.api.annotations.ChangeUnit; +import io.mongock.api.annotations.Execution; +import io.mongock.api.annotations.RollbackExecution; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.mongodb.core.MongoTemplate; + +import static com.appsmith.server.migrations.DatabaseChangelog1.installPluginToAllWorkspaces; + +@Slf4j +@ChangeUnit(order = "040", id = "add-aws-lambda-plugin", author = " ") +public class Migration040AddAWSLambdaPlugin { + + private final MongoTemplate mongoTemplate; + + public Migration040AddAWSLambdaPlugin(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @RollbackExecution + public void rollbackExecution() {} + + @Execution + public void addPluginToDbAndWorkspace() { + Plugin plugin = new Plugin(); + plugin.setName(PluginConstants.PluginName.AWS_LAMBDA_PLUGIN_NAME); + plugin.setType(PluginType.REMOTE); + plugin.setPluginName(PluginConstants.PluginName.AWS_LAMBDA_PLUGIN_NAME); + plugin.setPackageName(PluginConstants.PackageName.AWS_LAMBDA_PLUGIN); + plugin.setUiComponent("UQIDbEditorForm"); + plugin.setDatasourceComponent("DbEditorForm"); + plugin.setResponseType(Plugin.ResponseType.JSON); + plugin.setIconLocation("https://assets.appsmith.com/aws-lambda-logo.svg"); + plugin.setDocumentationLink("https://docs.appsmith.com/connect-data/reference/aws-lambda"); + plugin.setDefaultInstall(true); + try { + mongoTemplate.insert(plugin); + } catch (DuplicateKeyException e) { + log.warn(plugin.getPackageName() + " already present in database."); + } + + if (plugin.getId() == null) { + log.error("Failed to insert the AWS Lambda plugin into the database."); + return; + } + + installPluginToAllWorkspaces(mongoTemplate, plugin.getId()); + } +}