瀏覽代碼

[ML] add ML package loader module (#95207)

This PR introduces a new x-pack module for downloading and installing prepackaged models. The module is necessary, because we have to bypass the java security manager in order to open an http connections and/or access a file. The module limits this to only the minimum number of classes. 2 internal actions are introduced to get metadata of a model and the model itself.

Core changes have been implemented in: #95175
Hendrik Muhs 2 年之前
父節點
當前提交
9114965c4d

+ 25 - 0
x-pack/plugin/ml-package-loader/build.gradle

@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import org.apache.tools.ant.taskdefs.condition.Os
+import org.elasticsearch.gradle.OS
+
+apply plugin: 'elasticsearch.internal-es-plugin'
+
+esplugin {
+  name 'ml-package-loader'
+  description 'Loader for prepackaged Machine Learning Models from Elastic'
+  classname 'org.elasticsearch.xpack.ml.packageloader.MachineLearningPackageLoader'
+  extendedPlugins = ['x-pack-core']
+}
+
+dependencies {
+  implementation project(path: ':libs:elasticsearch-logging')
+  compileOnly project(":server")
+  compileOnly project(path: xpackModule('core'))
+  testImplementation(testArtifact(project(xpackModule('core'))))
+}

+ 55 - 0
x-pack/plugin/ml-package-loader/src/main/java/org/elasticsearch/xpack/ml/packageloader/MachineLearningPackageLoader.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+package org.elasticsearch.xpack.ml.packageloader;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.settings.Setting;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.plugins.ActionPlugin;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.xpack.core.ml.packageloader.action.GetTrainedModelPackageConfigAction;
+import org.elasticsearch.xpack.core.ml.packageloader.action.LoadTrainedModelPackageAction;
+import org.elasticsearch.xpack.ml.packageloader.action.TransportGetTrainedModelPackageConfigAction;
+import org.elasticsearch.xpack.ml.packageloader.action.TransportLoadTrainedModelPackage;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class MachineLearningPackageLoader extends Plugin implements ActionPlugin {
+
+    private final Settings settings;
+
+    public static final String DEFAULT_ML_MODELS_REPOSITORY = "https://ml-models.elastic.co";
+    public static final Setting<String> MODEL_REPOSITORY = Setting.simpleString(
+        "xpack.ml.model_repository",
+        DEFAULT_ML_MODELS_REPOSITORY,
+        Setting.Property.NodeScope,
+        Setting.Property.Dynamic
+    );
+
+    // re-using thread pool setup by the ml plugin
+    public static final String UTILITY_THREAD_POOL_NAME = "ml_utility";
+
+    public MachineLearningPackageLoader(Settings settings) {
+        this.settings = settings;
+    }
+
+    @Override
+    public List<Setting<?>> getSettings() {
+        return List.of(MODEL_REPOSITORY);
+    }
+
+    @Override
+    public List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> getActions() {
+        // all internal, no rest endpoint
+        return Arrays.asList(
+            new ActionHandler<>(GetTrainedModelPackageConfigAction.INSTANCE, TransportGetTrainedModelPackageConfigAction.class),
+            new ActionHandler<>(LoadTrainedModelPackageAction.INSTANCE, TransportLoadTrainedModelPackage.class)
+        );
+    }
+}

+ 195 - 0
x-pack/plugin/ml-package-loader/src/main/java/org/elasticsearch/xpack/ml/packageloader/action/ModelLoaderUtils.java

@@ -0,0 +1,195 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.ml.packageloader.action;
+
+import org.elasticsearch.ElasticsearchStatusException;
+import org.elasticsearch.ResourceNotFoundException;
+import org.elasticsearch.SpecialPermission;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.hash.MessageDigests;
+import org.elasticsearch.common.io.Streams;
+import org.elasticsearch.common.unit.ByteSizeUnit;
+import org.elasticsearch.common.unit.ByteSizeValue;
+import org.elasticsearch.core.SuppressForbidden;
+import org.elasticsearch.core.Tuple;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xcontent.XContentParserConfiguration;
+import org.elasticsearch.xcontent.XContentType;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.nio.file.Files;
+import java.security.AccessController;
+import java.security.MessageDigest;
+import java.security.PrivilegedAction;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
+import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
+
+/**
+ * Helper class for downloading pre-trained Elastic models, available on ml-models.elastic.co or as file
+ */
+final class ModelLoaderUtils {
+
+    public static String METADATA_FILE_EXTENSION = ".metadata.json";
+    public static String MODEL_FILE_EXTENSION = ".pt";
+
+    private static ByteSizeValue VOCABULARY_SIZE_LIMIT = new ByteSizeValue(10, ByteSizeUnit.MB);
+    private static final String VOCABULARY = "vocabulary";
+    private static final String MERGES = "merges";
+
+    static class InputStreamChunker {
+
+        private final InputStream inputStream;
+        private final MessageDigest digestSha256 = MessageDigests.sha256();
+        private final int chunkSize;
+
+        InputStreamChunker(InputStream inputStream, int chunkSize) {
+            this.inputStream = inputStream;
+            this.chunkSize = chunkSize;
+        }
+
+        public BytesArray next() throws IOException {
+            int bytesRead = 0;
+            byte[] buf = new byte[chunkSize];
+
+            while (bytesRead < chunkSize) {
+                int read = inputStream.read(buf, bytesRead, chunkSize - bytesRead);
+                // EOF??
+                if (read == -1) {
+                    break;
+                }
+                bytesRead += read;
+            }
+            digestSha256.update(buf, 0, bytesRead);
+
+            return new BytesArray(buf, 0, bytesRead);
+        }
+
+        public String getSha256() {
+            return MessageDigests.toHexString(digestSha256.digest());
+        }
+
+    }
+
+    static InputStream getInputStreamFromModelRepository(URI uri) throws IOException {
+        String scheme = uri.getScheme().toLowerCase(Locale.ROOT);
+        switch (scheme) {
+            case "http":
+            case "https":
+                return getHttpOrHttpsInputStream(uri);
+            case "file":
+                return getFileInputStream(uri);
+            default:
+                throw new IllegalArgumentException("unsupported scheme");
+        }
+    }
+
+    public static Tuple<List<String>, List<String>> loadVocabulary(URI uri) {
+        try {
+            InputStream vocabInputStream = getInputStreamFromModelRepository(uri);
+
+            if (uri.getPath().endsWith(".json")) {
+                XContentParser sourceParser = XContentType.JSON.xContent()
+                    .createParser(
+                        XContentParserConfiguration.EMPTY,
+                        Streams.limitStream(vocabInputStream, VOCABULARY_SIZE_LIMIT.getBytes())
+                    );
+                Map<String, List<Object>> vocabAndMerges = sourceParser.map(HashMap::new, XContentParser::list);
+
+                List<String> vocabulary = vocabAndMerges.containsKey(VOCABULARY)
+                    ? vocabAndMerges.get(VOCABULARY).stream().map(Object::toString).collect(Collectors.toList())
+                    : Collections.emptyList();
+                List<String> merges = vocabAndMerges.containsKey(MERGES)
+                    ? vocabAndMerges.get(MERGES).stream().map(Object::toString).collect(Collectors.toList())
+                    : Collections.emptyList();
+
+                return Tuple.tuple(vocabulary, merges);
+            }
+
+            throw new IllegalArgumentException("unknown format vocabulary file format");
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to load vocabulary file", e);
+        }
+    }
+
+    private ModelLoaderUtils() {}
+
+    @SuppressWarnings("'java.lang.SecurityManager' is deprecated and marked for removal ")
+    @SuppressForbidden(reason = "we need socket connection to download")
+    private static InputStream getHttpOrHttpsInputStream(URI uri) throws IOException {
+
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            sm.checkPermission(new SpecialPermission());
+        }
+
+        PrivilegedAction<InputStream> privilegedHttpReader = () -> {
+            try {
+                HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection();
+                switch (conn.getResponseCode()) {
+                    case HTTP_OK:
+                        return conn.getInputStream();
+                    case HTTP_MOVED_PERM:
+                    case HTTP_MOVED_TEMP:
+                    case HTTP_SEE_OTHER:
+                        throw new IllegalStateException("redirects aren't supported yet");
+                    case HTTP_NOT_FOUND:
+                        throw new ResourceNotFoundException("{} not found", uri);
+                    default:
+                        int responseCode = conn.getResponseCode();
+                        throw new ElasticsearchStatusException("error during downloading {}", RestStatus.fromCode(responseCode), uri);
+                }
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+        };
+
+        return AccessController.doPrivileged(privilegedHttpReader);
+    }
+
+    @SuppressWarnings("'java.lang.SecurityManager' is deprecated and marked for removal ")
+    @SuppressForbidden(reason = "we need load model data from a file")
+    private static InputStream getFileInputStream(URI uri) {
+
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            sm.checkPermission(new SpecialPermission());
+        }
+
+        PrivilegedAction<InputStream> privilegedFileReader = () -> {
+            File file = new File(uri);
+            if (file.exists() == false) {
+                throw new ResourceNotFoundException("{} not found", uri);
+            }
+
+            try {
+                return Files.newInputStream(file.toPath());
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+        };
+
+        return AccessController.doPrivileged(privilegedFileReader);
+    }
+
+}

+ 130 - 0
x-pack/plugin/ml-package-loader/src/main/java/org/elasticsearch/xpack/ml/packageloader/action/TransportGetTrainedModelPackageConfigAction.java

@@ -0,0 +1,130 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.ml.packageloader.action;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchStatusException;
+import org.elasticsearch.ResourceNotFoundException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.master.TransportMasterNodeAction;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.block.ClusterBlockException;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xcontent.XContentParserConfiguration;
+import org.elasticsearch.xcontent.XContentType;
+import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ModelPackageConfig;
+import org.elasticsearch.xpack.core.ml.packageloader.action.GetTrainedModelPackageConfigAction;
+import org.elasticsearch.xpack.core.ml.packageloader.action.GetTrainedModelPackageConfigAction.Request;
+import org.elasticsearch.xpack.core.ml.packageloader.action.GetTrainedModelPackageConfigAction.Response;
+import org.elasticsearch.xpack.ml.packageloader.MachineLearningPackageLoader;
+
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.elasticsearch.core.Strings.format;
+
+public class TransportGetTrainedModelPackageConfigAction extends TransportMasterNodeAction<Request, Response> {
+
+    private static final Logger logger = LogManager.getLogger(TransportGetTrainedModelPackageConfigAction.class);
+    private final Settings settings;
+
+    @Inject
+    public TransportGetTrainedModelPackageConfigAction(
+        Settings settings,
+        TransportService transportService,
+        ClusterService clusterService,
+        ThreadPool threadPool,
+        ActionFilters actionFilters,
+        IndexNameExpressionResolver indexNameExpressionResolver
+    ) {
+        super(
+            GetTrainedModelPackageConfigAction.NAME,
+            transportService,
+            clusterService,
+            threadPool,
+            actionFilters,
+            GetTrainedModelPackageConfigAction.Request::new,
+            indexNameExpressionResolver,
+            GetTrainedModelPackageConfigAction.Response::new,
+            ThreadPool.Names.SAME
+        );
+        this.settings = settings;
+    }
+
+    @Override
+    protected void masterOperation(Task task, Request request, ClusterState state, ActionListener<Response> listener) throws Exception {
+        String repository = MachineLearningPackageLoader.MODEL_REPOSITORY.get(settings);
+        String packagedModelId = request.getPackagedModelId();
+        logger.trace(() -> format("Fetch package manifest for [%s] from [%s]", packagedModelId, repository));
+
+        threadPool.executor(MachineLearningPackageLoader.UTILITY_THREAD_POOL_NAME).execute(() -> {
+            try {
+                URI uri = new URI(repository).resolve(packagedModelId + ModelLoaderUtils.METADATA_FILE_EXTENSION);
+                InputStream inputStream = ModelLoaderUtils.getInputStreamFromModelRepository(uri);
+
+                try (
+                    XContentParser parser = XContentType.JSON.xContent()
+                        .createParser(XContentParserConfiguration.EMPTY, inputStream.readAllBytes())
+                ) {
+                    ModelPackageConfig packageConfig = ModelPackageConfig.fromXContentLenient(parser);
+
+                    if (packagedModelId.equals(packageConfig.getPackagedModelId()) == false) {
+                        // the package is somehow broken
+                        listener.onFailure(new ElasticsearchStatusException("Invalid package", RestStatus.INTERNAL_SERVER_ERROR));
+                        return;
+                    }
+
+                    if (packageConfig.getSize() <= 0) {
+                        listener.onFailure(new ElasticsearchStatusException("Invalid package size", RestStatus.INTERNAL_SERVER_ERROR));
+                        return;
+                    }
+
+                    if (Strings.isNullOrEmpty(packageConfig.getSha256())) {
+                        listener.onFailure(new ElasticsearchStatusException("Invalid package sha", RestStatus.INTERNAL_SERVER_ERROR));
+                        return;
+                    }
+
+                    ModelPackageConfig withRepository = new ModelPackageConfig.Builder(packageConfig).setModelRepository(repository)
+                        .build();
+
+                    listener.onResponse(new Response(withRepository));
+                }
+
+                // TODO: use proper ElasticsearchStatusExceptions
+            } catch (MalformedURLException e) {
+                listener.onFailure(new IllegalArgumentException("Invalid connection configuration: " + e.getMessage(), e));
+            } catch (URISyntaxException e) {
+                // TODO: what if the URI contained credentials, don't leak it in the exception
+                listener.onFailure(new IllegalArgumentException("Invalid connection configuration: " + e.getMessage(), e));
+            } catch (ResourceNotFoundException e) {
+                // TODO: don't leak the full url and package details
+                listener.onFailure(new IllegalArgumentException("Failed to find package", e));
+            } catch (Exception e) {
+                listener.onFailure(new IllegalArgumentException("Failed to load package metadata", e));
+            }
+        });
+    }
+
+    @Override
+    protected ClusterBlockException checkBlock(Request request, ClusterState state) {
+        return null;
+    }
+}

+ 146 - 0
x-pack/plugin/ml-package-loader/src/main/java/org/elasticsearch/xpack/ml/packageloader/action/TransportLoadTrainedModelPackage.java

@@ -0,0 +1,146 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.ml.packageloader.action;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.action.support.master.TransportMasterNodeAction;
+import org.elasticsearch.client.internal.Client;
+import org.elasticsearch.client.internal.OriginSettingClient;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.block.ClusterBlockException;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.core.Tuple;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.xpack.core.ml.action.NodeAcknowledgedResponse;
+import org.elasticsearch.xpack.core.ml.action.PutTrainedModelDefinitionPartAction;
+import org.elasticsearch.xpack.core.ml.action.PutTrainedModelVocabularyAction;
+import org.elasticsearch.xpack.core.ml.inference.trainedmodel.ModelPackageConfig;
+import org.elasticsearch.xpack.core.ml.packageloader.action.LoadTrainedModelPackageAction;
+import org.elasticsearch.xpack.core.ml.packageloader.action.LoadTrainedModelPackageAction.Request;
+import org.elasticsearch.xpack.ml.packageloader.MachineLearningPackageLoader;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import static org.elasticsearch.core.Strings.format;
+import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN;
+
+public class TransportLoadTrainedModelPackage extends TransportMasterNodeAction<Request, AcknowledgedResponse> {
+
+    private static final int DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024; // 4MB
+
+    private static final Logger logger = LogManager.getLogger(TransportLoadTrainedModelPackage.class);
+
+    private final Client client;
+
+    @Inject
+    public TransportLoadTrainedModelPackage(
+        TransportService transportService,
+        ClusterService clusterService,
+        ThreadPool threadPool,
+        ActionFilters actionFilters,
+        IndexNameExpressionResolver indexNameExpressionResolver,
+        Client client
+    ) {
+        super(
+            LoadTrainedModelPackageAction.NAME,
+            transportService,
+            clusterService,
+            threadPool,
+            actionFilters,
+            LoadTrainedModelPackageAction.Request::new,
+            indexNameExpressionResolver,
+            NodeAcknowledgedResponse::new,
+            ThreadPool.Names.SAME
+        );
+        this.client = new OriginSettingClient(client, ML_ORIGIN);
+    }
+
+    @Override
+    protected void masterOperation(Task task, Request request, ClusterState state, ActionListener<AcknowledgedResponse> listener)
+        throws Exception {
+        ModelPackageConfig modelPackageConfig = request.getModelPackageConfig();
+        String repository = modelPackageConfig.getModelRepository();
+        String modelId = request.getModelId();
+        long size = modelPackageConfig.getSize();
+        String packagedModelId = modelPackageConfig.getPackagedModelId();
+
+        threadPool.executor(MachineLearningPackageLoader.UTILITY_THREAD_POOL_NAME).execute(() -> {
+            try {
+                URI uri = new URI(repository).resolve(packagedModelId + ModelLoaderUtils.MODEL_FILE_EXTENSION);
+
+                // Uploading other artefacts of the model first, that way the model is last and a simple search can be used to check if the
+                // download is complete
+                if (Strings.isNullOrEmpty(modelPackageConfig.getVocabularyFile()) == false) {
+                    Tuple<List<String>, List<String>> vocabularyAndMerges = ModelLoaderUtils.loadVocabulary(
+                        new URI(repository).resolve(modelPackageConfig.getVocabularyFile())
+                    );
+
+                    PutTrainedModelVocabularyAction.Request r2 = new PutTrainedModelVocabularyAction.Request(
+                        modelId,
+                        vocabularyAndMerges.v1(),
+                        vocabularyAndMerges.v2()
+                    );
+                    client.execute(PutTrainedModelVocabularyAction.INSTANCE, r2).actionGet();
+                    logger.debug(() -> format("uploaded model vocabulary [%s]", modelPackageConfig.getVocabularyFile()));
+                }
+
+                InputStream modelInputStream = ModelLoaderUtils.getInputStreamFromModelRepository(uri);
+
+                ModelLoaderUtils.InputStreamChunker chunkIterator = new ModelLoaderUtils.InputStreamChunker(
+                    modelInputStream,
+                    DEFAULT_CHUNK_SIZE
+                );
+
+                // simple round up
+                int totalParts = (int) ((size + DEFAULT_CHUNK_SIZE - 1) / DEFAULT_CHUNK_SIZE);
+
+                for (int part = 0; part < totalParts; ++part) {
+                    BytesArray definition = chunkIterator.next();
+                    PutTrainedModelDefinitionPartAction.Request r = new PutTrainedModelDefinitionPartAction.Request(
+                        modelId,
+                        definition,
+                        part,
+                        size,
+                        totalParts
+                    );
+
+                    client.execute(PutTrainedModelDefinitionPartAction.INSTANCE, r).actionGet();
+                }
+                logger.debug(() -> format("finished uploading model using [%d] parts", totalParts));
+            } catch (MalformedURLException e) {
+                logger.error(format("Invalid URL [%s]", e));
+            } catch (URISyntaxException e) {
+                logger.error(format("Invalid URL syntax [%s]", e));
+            } catch (IOException e) {
+                logger.error(format("IOException [%s]", e));
+            }
+        });
+
+        listener.onResponse(AcknowledgedResponse.TRUE);
+    }
+
+    @Override
+    protected ClusterBlockException checkBlock(Request request, ClusterState state) {
+        return null;
+    }
+}

+ 13 - 0
x-pack/plugin/ml-package-loader/src/main/plugin-metadata/plugin-security.policy

@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+grant {
+  permission java.net.SocketPermission "*", "connect";
+
+  permission java.io.FilePermission "<<ALL FILES>>", "read";
+};

+ 2 - 0
x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java

@@ -258,6 +258,8 @@ public class Constants {
         "cluster:internal/xpack/ml/reset_mode",
         "cluster:internal/xpack/ml/trained_models/cache/info",
         "cluster:internal/xpack/ml/trained_models/deployments/stats/get",
+        "cluster:internal/xpack/ml/trained_models/package_loader/get_config",
+        "cluster:internal/xpack/ml/trained_models/package_loader/load",
         "cluster:internal/xpack/transform/reset_mode",
         "cluster:monitor/allocation/explain",
         "cluster:monitor/async_search/status",