From ed8cd5520e3a433a86cb41c49b8e1353b4401c53 Mon Sep 17 00:00:00 2001
From: Andriy Redko <andriy.redko@aiven.io>
Date: Mon, 16 Dec 2024 15:00:55 -0500
Subject: [PATCH] [POC] [Security Manager Replacement] GraalVM sandboxing

Signed-off-by: Andriy Redko <andriy.redko@aiven.io>
---
 libs/espresso-sm/build.gradle                 |  44 +++++++
 .../opensearch/espresso/sandbox/Sandbox.java  | 113 ++++++++++++++++++
 .../identity/shiro/ShiroIdentityPlugin.java   |  12 ++
 .../opensearch/bootstrap/QuickBoostrap.java   |  60 ++++++++++
 4 files changed, 229 insertions(+)
 create mode 100644 libs/espresso-sm/build.gradle
 create mode 100644 libs/espresso-sm/src/main/java/org/opensearch/espresso/sandbox/Sandbox.java
 create mode 100644 server/src/main/java/org/opensearch/bootstrap/QuickBoostrap.java

diff --git a/libs/espresso-sm/build.gradle b/libs/espresso-sm/build.gradle
new file mode 100644
index 0000000000000..726ff0b216a50
--- /dev/null
+++ b/libs/espresso-sm/build.gradle
@@ -0,0 +1,44 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ *
+ * Modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+import org.opensearch.gradle.info.BuildParams
+
+apply plugin: 'opensearch.publish'
+
+base {
+  archivesName = 'esspresso-sm'
+}
+
+dependencies {
+  implementation "org.graalvm.polyglot:polyglot:24.1.1"
+  implementation "org.graalvm.sdk:nativeimage:24.1.1"
+  implementation "org.graalvm.sdk:collections:24.1.1"
+  implementation "org.graalvm.sdk:word:24.1.1"
+  implementation "org.graalvm.sdk:jniutils:24.1.1"
+  implementation "org.graalvm.llvm:llvm-api:24.1.1"
+  implementation "org.graalvm.llvm:llvm-language:24.1.1"
+  implementation "org.graalvm.llvm:llvm-language-native:24.1.1"
+  implementation "org.graalvm.llvm:llvm-language-native-resources:24.1.1"
+  implementation "org.graalvm.llvm:llvm-language-nfi:24.1.1"
+  implementation "org.graalvm.truffle:truffle-api:24.1.1"
+  implementation "org.graalvm.truffle:truffle-compiler:24.1.1"
+  implementation "org.graalvm.truffle:truffle-nfi:24.1.1"
+  implementation "org.graalvm.truffle:truffle-nfi-libffi:24.1.1"
+  implementation "org.graalvm.truffle:truffle-runtime:24.1.1"
+  implementation "org.graalvm.espresso:espresso-language:24.1.1"
+  implementation "org.graalvm.espresso:espresso-libs-resources-linux-amd64:24.1.1"
+  implementation "org.graalvm.espresso:espresso-runtime-resources-linux-amd64:24.1.1"
+  implementation project(":server")
+}
+
+tasks.named('forbiddenApisMain').configure {
+  replaceSignatureFiles 'jdk-signatures'
+}
diff --git a/libs/espresso-sm/src/main/java/org/opensearch/espresso/sandbox/Sandbox.java b/libs/espresso-sm/src/main/java/org/opensearch/espresso/sandbox/Sandbox.java
new file mode 100644
index 0000000000000..bfeccaac2be45
--- /dev/null
+++ b/libs/espresso-sm/src/main/java/org/opensearch/espresso/sandbox/Sandbox.java
@@ -0,0 +1,113 @@
+/*
+ * Modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+package org.opensearch.espresso.sandbox;
+
+import org.opensearch.client.Client;
+import org.opensearch.client.node.NodeClient;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.threadpool.ThreadPool;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.Engine;
+import org.graalvm.polyglot.HostAccess;
+import org.graalvm.polyglot.PolyglotAccess;
+import org.graalvm.polyglot.Value;
+import org.graalvm.polyglot.io.IOAccess;
+
+/**
+ * GraalVM Sandbox
+ */
+public class Sandbox {
+    /**
+     * GraalVM Sandbox runner
+     */
+    public static void main(String[] args) throws IOException, InterruptedException {
+        final String opensearchHome = args[0];
+        System.out.println("OpenSearch home: " + opensearchHome);
+
+        final Engine engine = Engine.newBuilder().build();
+
+        System.out.println("Host JVM version: " + Runtime.version());
+        final String plugin = loadPlugin(opensearchHome, engine);
+        System.out.println(plugin);
+    }
+
+    private static String loadPlugin(String opensearchHome, Engine engine) throws IOException {
+        // See please:
+        // - https://github.com/oracle/graal/issues/10239
+        // -
+        // https://github.com/oracle/graal/blob/master/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/EspressoOptions.java
+        // -
+        // https://github.com/oracle/graal/blob/master/espresso/src/com.oracle.truffle.espresso.launcher/src/com/oracle/truffle/espresso/launcher/EspressoLauncher.java
+        final Context context = Context.newBuilder("java")
+            .option("java.JavaHome", "/usr/lib/jvm/java-21-openjdk-amd64/")
+            .option(
+                "java.Classpath",
+                ("${opensearchHome}/lib/lucene-core-9.12.0.jar:"
+                    + "${opensearchHome}/lib/opensearch-cli-3.0.0-SNAPSHOT.jar:"
+                    + "${opensearchHome}/lib/jackson-core-2.17.2.jar:"
+                    + "${opensearchHome}/lib/jackson-dataformat-cbor-2.17.2.jar:"
+                    + "${opensearchHome}/lib/jackson-dataformat-smile-2.17.2.jar:"
+                    + "${opensearchHome}/lib/jackson-dataformat-yaml-2.17.2.jar:"
+                    + "${opensearchHome}/lib/snakeyaml-2.1.jar:"
+                    + "${opensearchHome}/lib/opensearch-3.0.0-SNAPSHOT.jar:"
+                    + "${opensearchHome}/lib/opensearch-core-3.0.0-SNAPSHOT.jar:"
+                    + "${opensearchHome}/lib/opensearch-common-3.0.0-SNAPSHOT.jar:"
+                    + "${opensearchHome}/lib/opensearch-x-content-3.0.0-SNAPSHOT.jar:"
+                    + "${opensearchHome}/lib/opensearch-secure-sm-3.0.0-SNAPSHOT.jar:"
+                    + "${opensearchHome}/lib/log4j-core-2.21.0.jar:"
+                    + "${opensearchHome}/lib/log4j-jul-2.21.0.jar:"
+                    + "${opensearchHome}/lib/log4j-api-2.21.0.jar:"
+                    + "${opensearchHome}/plugins/identity-shiro/slf4j-api-1.7.36.jar:"
+                    + "${opensearchHome}/plugins/identity-shiro/passay-1.6.3.jar:"
+                    + "${opensearchHome}/plugins/identity-shiro/identity-shiro-3.0.0-SNAPSHOT.jar:"
+                    + "${opensearchHome}/plugins/identity-shiro/shiro-core-1.13.0.jar").replaceAll(
+                        "[$][{]opensearchHome[}]",
+                        opensearchHome
+                    )
+            )
+            .option("java.Properties.java.security.manager", "allow")
+            .option("java.PolyglotInterfaceMappings", getInterfaceMappings())
+            .option("java.Polyglot", "true")
+            .allowExperimentalOptions(true)
+            .allowNativeAccess(true)
+            .allowCreateThread(true)
+            .allowHostAccess(HostAccess.NONE)
+            .allowIO(IOAccess.NONE)
+            .allowPolyglotAccess(PolyglotAccess.newBuilder().allowBindingsAccess("java").build())
+            .engine(engine)
+            .build();
+
+        final Value runtime = context.getBindings("java").getMember("java.lang.Runtime");
+        System.out.println("Polyglot JVM version: " + runtime.invokeMember("version").toString());
+
+        final Path homePath = new File(opensearchHome).toPath();
+        final Path configPath = new File(opensearchHome + "/config").toPath();
+        context.getBindings("java")
+            .getMember("org.opensearch.bootstrap.QuickBoostrap")
+            .invokeMember("bootstrap", configPath.toString(), homePath.toString());
+
+        final Value securityManager = context.getBindings("java").getMember("java.lang.System").invokeMember("getSecurityManager");
+        System.out.println("Security Manager? " + securityManager.toString());
+
+        final Value settings = context.getBindings("java").getMember("org.opensearch.common.settings.Settings").getMember("EMPTY");
+        final Value result = context.getBindings("java").getMember("org.opensearch.identity.shiro.ShiroIdentityPlugin");
+        final Value instance = result.newInstance(settings);
+        System.out.println("Shiro Plugin? " + instance.toString());
+
+        final Client client = new NodeClient(Settings.EMPTY, new ThreadPool(Settings.EMPTY));
+        final Value socket = instance.invokeMember("getSocket", client);
+        return socket.toString();
+    }
+
+    private static String getInterfaceMappings() {
+        return "org.opensearch.client.Client;" + "org.opensearch.client.AdminClient;";
+    }
+}
diff --git a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java
index 2da788242a745..d542b2aa4b2c3 100644
--- a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java
+++ b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java
@@ -39,6 +39,8 @@
 import org.opensearch.threadpool.ThreadPool;
 import org.opensearch.watcher.ResourceWatcherService;
 
+import java.io.IOException;
+import java.net.ServerSocket;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.function.Supplier;
@@ -138,6 +140,16 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c
         }
     }
 
+    /**
+     * Deliberately introducing the network access attempt to trigger SecurityException
+     */
+    public ServerSocket getSocket(Client client) throws IOException {
+        System.out.println("Client? " + client);
+        System.out.println("AdminClient? " + client.admin());
+        client.admin().cluster().prepareState().execute().actionGet();
+        return new ServerSocket(0);
+    }
+
     public PluginSubject getPluginSubject(Plugin plugin) {
         return new ShiroPluginSubject(threadPool);
     }
diff --git a/server/src/main/java/org/opensearch/bootstrap/QuickBoostrap.java b/server/src/main/java/org/opensearch/bootstrap/QuickBoostrap.java
new file mode 100644
index 0000000000000..923fb9e19c6eb
--- /dev/null
+++ b/server/src/main/java/org/opensearch/bootstrap/QuickBoostrap.java
@@ -0,0 +1,60 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.bootstrap;
+
+import org.opensearch.cli.UserException;
+import org.opensearch.common.logging.LogConfigurator;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.env.Environment;
+import org.opensearch.node.InternalSettingsPreparer;
+import org.opensearch.node.Node;
+import org.opensearch.node.NodeValidationException;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
+
+/**
+ * Quick bootstrap
+ */
+public class QuickBoostrap {
+    /**
+     * This method is invoked by {@link OpenSearch#main(String[])} to startup opensearch.
+     */
+    public static void bootstrap(final String configPath, final String homePath) throws BootstrapException, NodeValidationException,
+        UserException {
+
+        // force the class initializer for BootstrapInfo to run before
+        // the security manager is installed
+        BootstrapInfo.init();
+
+        Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), homePath).build();
+        final Environment environment = InternalSettingsPreparer.prepareEnvironment(
+            settings,
+            Collections.emptyMap(),
+            Paths.get(configPath),
+            // HOSTNAME is set by opensearch-env and opensearch-env.bat so it is always available
+            () -> System.getenv("HOSTNAME")
+        );
+
+        LogConfigurator.setNodeName(Node.NODE_NAME_SETTING.get(environment.settings()));
+        try {
+            LogConfigurator.configure(environment);
+        } catch (IOException e) {
+            throw new BootstrapException(e);
+        }
+
+        try {
+            Security.configure(environment, BootstrapSettings.SECURITY_FILTER_BAD_DEFAULTS_SETTING.get(settings));
+        } catch (IOException | NoSuchAlgorithmException e) {
+            throw new BootstrapException(e);
+        }
+    }
+}