diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java index fa4dc9ae2b..f7a836d360 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java @@ -35,7 +35,16 @@ public class ActionConfiguration { HttpMethod httpMethod; // DB action fields - String query; + + /* + * For SQL plugins, the query field would be of the following format : + * { + * "cmd" : "select * from users;" + * } + * For noSQL plugins, the query json would be constructed according to + * the requirements of the plugin. + */ + Map query; /* * Future plugins could require more fields that are not covered above. diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ResourceConfiguration.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ResourceConfiguration.java index 123e13207d..9ea7823128 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ResourceConfiguration.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ResourceConfiguration.java @@ -23,4 +23,9 @@ public class ResourceConfiguration { //For REST API List headers; + + //This field is for plugins which allow the database name to be specified outside of the connection URL. The + //expectation from the plugins is that if the database name has been provided via this field, the database name + //should be according to this, else if database name is null, pick the database name from the URL. + String databaseName; } diff --git a/app/server/appsmith-plugins/mongoPlugin/pom.xml b/app/server/appsmith-plugins/mongoPlugin/pom.xml new file mode 100644 index 0000000000..3d07dac22c --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/pom.xml @@ -0,0 +1,84 @@ + + + + 4.0.0 + + com.external.plugins + mongoPlugin + 1.0-SNAPSHOT + + mongoPlugin + + + UTF-8 + 11 + ${java.version} + ${java.version} + mongo-plugin + com.external.plugins.MongoPlugin + 1.0-SNAPSHOT + tech@appsmith.com + + + + + + junit + junit + 4.11 + test + + + + org.pf4j + pf4j-spring + 0.5.0 + + + + com.appsmith + interfaces + 1.0-SNAPSHOT + + + + org.projectlombok + lombok + 1.18.8 + provided + + + org.mongodb + mongo-java-driver + 3.11.1 + + + + + + + + maven-compiler-plugin + 3.8.1 + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.dependencies} + + + + + + + + diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java new file mode 100644 index 0000000000..cac064c9c7 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java @@ -0,0 +1,130 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.ResourceConfiguration; +import com.appsmith.external.plugins.BasePlugin; +import com.appsmith.external.plugins.PluginExecutor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientURI; +import com.mongodb.client.MongoDatabase; +import lombok.extern.slf4j.Slf4j; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.json.JSONArray; +import org.json.JSONObject; +import org.pf4j.Extension; +import org.pf4j.PluginWrapper; +import reactor.core.publisher.Mono; + +import java.math.BigInteger; + +@Slf4j +public class MongoPlugin extends BasePlugin { + + private static ObjectMapper objectMapper; + + public MongoPlugin(PluginWrapper wrapper) { + super(wrapper); + this.objectMapper = new ObjectMapper(); + } + + @Slf4j + @Extension + public static class MongoPluginExecutor implements PluginExecutor { + + /** + * For reference on creating the json queries for Mongo please head to + * https://docs.huihoo.com/mongodb/3.4/reference/command/index.html + * + * @param connection : This is the connection that is established to the data source. This connection is according + * to the parameters in Resource Configuration + * @param resourceConfiguration : These are the configurations which have been used to create a Resource from a Plugin + * @param actionConfiguration : These are the configurations which have been used to create an Action from a Resource. + * @return + */ + @Override + public Mono execute(Object connection, + ResourceConfiguration resourceConfiguration, + ActionConfiguration actionConfiguration) { + + ActionExecutionResult result = new ActionExecutionResult(); + MongoClient mongoClient = (MongoClient) connection; + if (mongoClient == null) { + return Mono.error(new Exception("Mongo Client is null.")); + } + + MongoClientURI mongoClientURI= new MongoClientURI(resourceConfiguration.getUrl()); + + String databaseName = resourceConfiguration.getDatabaseName(); + if (databaseName == null) { + databaseName = mongoClientURI.getDatabase(); + } + + MongoDatabase database = mongoClient.getDatabase(databaseName); + + Bson command = new Document(actionConfiguration.getQuery()); + + try { + + Document mongoOutput = database.runCommand(command); + + JSONObject outputJson = new JSONObject(mongoOutput.toJson()); + //The output json contains the key "ok". This is the status of the command + BigInteger status = outputJson.getBigInteger("ok"); + JSONArray headerArray = new JSONArray(); + if (BigInteger.ONE == status) { + //The json contains key "cursor" when find command was issued and there are 1 or more results. In case + //there are no results for find, this key is not present in the result json. + if (outputJson.has("cursor")) { + JSONArray outputResult = outputJson.getJSONObject("cursor").getJSONArray("firstBatch"); + result.setBody(objectMapper.readTree(outputResult.toString())); + } + //The json contains key "n" when insert/update command is issued. "n" for update signifies the no of + //documents selected for update. "n" in case of insert signifies the number of documents inserted. + if (outputJson.has("n")) { + JSONObject body = new JSONObject().put("n", outputJson.getBigInteger("n")); + headerArray.put(body); + } + //The json key constains key "nModified" in case of update command. This signifies the no of + //documents updated. + if (outputJson.has("nModified")) { + JSONObject body = new JSONObject().put("nModified", outputJson.getBigInteger("nModified")); + headerArray.put(body); + } + + /** TODO + * Go through all the possible fields that are returned in the output JSON and add all the fields + * that are important to the headerArray. + */ + } + + JSONObject statusJson = new JSONObject().put("ok", status); + headerArray.put(statusJson); + result.setHeaders(objectMapper.readTree(headerArray.toString())); + } catch (Exception e) { + return Mono.error(e); + } + + return Mono.just(result); + } + + @Override + public Object resourceCreate(ResourceConfiguration resourceConfiguration) { + + MongoClientURI mongoClientURI= new MongoClientURI(resourceConfiguration.getUrl()); + return new MongoClient(mongoClientURI); + } + + @Override + public void resourceDestroy(Object connection) { + MongoClient mongoClient = (MongoClient) connection; + if (mongoClient != null) { + mongoClient.close(); + } + } + + } + +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java new file mode 100644 index 0000000000..01146d96f5 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java @@ -0,0 +1,18 @@ +package com.external.plugins; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * Unit test for simple App. + */ +public class MongoPluginTest { + /** + * Rigorous Test :-) + */ + @Test + public void shouldAnswerWithTrue() { + assertTrue(true); + } +} diff --git a/app/server/appsmith-plugins/pom.xml b/app/server/appsmith-plugins/pom.xml index 66e0a7f36c..d5a92eb925 100644 --- a/app/server/appsmith-plugins/pom.xml +++ b/app/server/appsmith-plugins/pom.xml @@ -17,6 +17,7 @@ postgresPlugin restApiPlugin + mongoPlugin \ No newline at end of file diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java index a367229bba..c5e0a64b8d 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java @@ -2,14 +2,11 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.Param; import com.appsmith.external.models.ResourceConfiguration; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; -import org.json.JSONObject; import org.pf4j.Extension; import org.pf4j.PluginException; import org.pf4j.PluginWrapper; @@ -24,7 +21,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.HashMap; -import java.util.List; +import java.util.Map; @Slf4j public class PostgresPlugin extends BasePlugin { @@ -38,6 +35,13 @@ public class PostgresPlugin extends BasePlugin { this.objectMapper = new ObjectMapper(); } + /** + * Postgres plugin receives the query as json of the following format : + * { + * "cmd" : "select * from users;" + * } + */ + @Slf4j @Extension public static class PostgresPluginExecutor implements PluginExecutor { @@ -53,8 +57,9 @@ public class PostgresPlugin extends BasePlugin { ArrayList list = new ArrayList(50); try { Statement statement = conn.createStatement(); - - ResultSet resultSet = statement.executeQuery(actionConfiguration.getQuery()); + Map queryJson = actionConfiguration.getQuery(); + String query = (String) queryJson.get("cmd"); + ResultSet resultSet = statement.executeQuery(query); ResultSetMetaData metaData = resultSet.getMetaData(); Integer colCount = metaData.getColumnCount(); while (resultSet.next()) { @@ -90,7 +95,8 @@ public class PostgresPlugin extends BasePlugin { } catch (SQLException e) { log.error("", e); } - return conn; + // Connection wasn't created. Return null + return null; } @Override diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java index 0fc8154fbf..994a55aa9b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java @@ -18,7 +18,8 @@ public enum AppsmithError { UNAUTHORIZED_ACCESS(401, 4002, "Unauthorized access"), INTERNAL_SERVER_ERROR(500, 5000, "Internal server error while processing request"), REPOSITORY_SAVE_FAILED(500, 5001, "Repository save failed."), - PLUGIN_INSTALLATION_FAILED_DOWNLOAD_ERROR(500, 5002, "Due to error in downloading the plugin from remote repository, plugin installation has failed. Check the jar location and try again."); + PLUGIN_INSTALLATION_FAILED_DOWNLOAD_ERROR(500, 5002, "Due to error in downloading the plugin from remote repository, plugin installation has failed. Check the jar location and try again."), + PLUGIN_RUN_FAILED(500, 5003, "Plugin execution failed with error {0}"); private Integer httpErrorCode; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java index 7e2bc17714..1a86786123 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java @@ -10,7 +10,6 @@ import com.appsmith.server.domains.Page; import com.appsmith.server.domains.PageAction; import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.Resource; -import com.appsmith.server.domains.ResourceContext; import com.appsmith.server.domains.User; import com.appsmith.server.dtos.ExecuteActionDTO; import com.appsmith.server.exceptions.AppsmithError; @@ -171,15 +170,15 @@ public class ActionServiceImpl extends BaseService pluginExecutor.execute( resourceContext.getConnection(), resourceConfiguration, actionConfiguration)); })) + .onErrorResume(e -> Mono.error(new AppsmithException(AppsmithError.PLUGIN_RUN_FAILED, e.getMessage()))) .flatMap(obj -> obj); } diff --git a/app/server/appsmith-server/src/main/resources/public/appsmith/authz/acl.rego b/app/server/appsmith-server/src/main/resources/public/appsmith/authz/acl.rego index aedcc7d21b..5fd3fdae6d 100644 --- a/app/server/appsmith-server/src/main/resources/public/appsmith/authz/acl.rego +++ b/app/server/appsmith-server/src/main/resources/public/appsmith/authz/acl.rego @@ -13,6 +13,9 @@ url_allow = true { p = op.permission } +# This is a global list of all the routes for all controllers. Any new controller that is written must +# carry an entry in this array. OPA performs ACL based on an intersection of these entries and permissions +# for a user + permissions inherited via the groups that the user is a part of. allowed_operations = [ {"method": "POST", "resource": "users", "permission": "create:users"}, {"method": "GET", "resource": "users", "permission": "read:users"},