Skip to content

Commit

Permalink
[ML] Create inference_user and inference_admin roles (#106371)
Browse files Browse the repository at this point in the history
Defines new inference_user and inference_admin roles with the 
related cluster privileges manage_inference and monitor_inference.
inference_user can list the models and preform inference, 
inference_admin can do the same plus create and delete models
  • Loading branch information
davidkyle committed Mar 20, 2024
1 parent 0d6b368 commit 2087b65
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 6 deletions.
4 changes: 3 additions & 1 deletion docs/reference/inference/delete-inference.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ own model, use the <<ml-df-trained-models-apis>>.
==== {api-request-title}

`DELETE /_inference/<model_id>`

`DELETE /_inference/<task_type>/<model_id>`

[discrete]
[[delete-inference-api-prereqs]]
==== {api-prereq-title}

* Requires the `manage` <<privileges-list-cluster,cluster privilege>>.
* Requires the `manage_inference` <<privileges-list-cluster,cluster privilege>>
(the built-in `inference_admin` role grants this privilege)


[discrete]
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/inference/get-inference.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ own model, use the <<ml-df-trained-models-apis>>.
[[get-inference-api-prereqs]]
==== {api-prereq-title}

* Requires the `manage` <<privileges-list-cluster,cluster privilege>>.
* Requires the `monitor_inference` <<privileges-list-cluster,cluster privilege>>
(the built-in `inference_admin` and `inference_user` roles grant this privilege)

[discrete]
[[get-inference-api-desc]]
Expand Down
5 changes: 3 additions & 2 deletions docs/reference/inference/post-inference.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ own model, use the <<ml-df-trained-models-apis>>.
==== {api-request-title}

`POST /_inference/<model_id>`

`POST /_inference/<task_type>/<model_id>`


[discrete]
[[post-inference-api-prereqs]]
==== {api-prereq-title}

* Requires the `manage` <<privileges-list-cluster,cluster privilege>>.

* Requires the `monitor_inference` <<privileges-list-cluster,cluster privilege>>
(the built-in `inference_admin` and `inference_user` roles grant this privilege)

[discrete]
[[post-inference-api-desc]]
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/inference/put-inference.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ or if you want to use non-NLP models, use the <<ml-df-trained-models-apis>>.
[[put-inference-api-prereqs]]
==== {api-prereq-title}

* Requires the `manage` <<privileges-list-cluster,cluster privilege>>.
* Requires the `manage_inference` <<privileges-list-cluster,cluster privilege>>
(the built-in `inference_admin` role grants this privilege)


[discrete]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ A successful call returns an object with "cluster" and "index" fields.
"manage_enrich",
"manage_ilm",
"manage_index_templates",
"manage_inference",
"manage_ingest_pipelines",
"manage_logstash_pipelines",
"manage_ml",
Expand All @@ -99,6 +100,7 @@ A successful call returns an object with "cluster" and "index" fields.
"monitor",
"monitor_data_frame_transforms",
"monitor_enrich",
"monitor_inference",
"monitor_ml",
"monitor_rollup",
"monitor_snapshot",
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/security/authorization/built-in-roles.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ Grants full access to all features in {kib} (including Solutions) and read-only
Grants access to manage *all* enrich indices (`.enrich-*`) and *all* operations on
ingest pipelines.

[[built-in-roles-inference-admin]] `inference_admin`::
Provides all of the privileges of the `inference_user` role and the full
use of the {inference} APIs. Grants the `manage_inference` cluster privilege.

[[built-in-roles-inference-user]] `inference_user`::
Provides the minimum privileges required to view {inference} configurations
and perform inference. Grants the `monintor_inference` cluster privilege.

[[built-in-roles-ingest-user]] `ingest_admin` ::
Grants access to manage *all* index templates and *all* ingest pipeline configurations.
+
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ public class ClusterPrivilegeResolver {
GetComponentTemplateAction.NAME,
GetComposableIndexTemplateAction.NAME
);
private static final Set<String> MONITOR_INFERENCE_PATTERN = Set.of(
"cluster:monitor/xpack/inference*",
"cluster:monitor/xpack/ml/trained_models/deployment/infer"
);
private static final Set<String> MONITOR_ML_PATTERN = Set.of("cluster:monitor/xpack/ml/*");
private static final Set<String> MONITOR_TEXT_STRUCTURE_PATTERN = Set.of("cluster:monitor/text_structure/*");
private static final Set<String> MONITOR_TRANSFORM_PATTERN = Set.of("cluster:monitor/data_frame/*", "cluster:monitor/transform/*");
Expand All @@ -110,6 +114,13 @@ public class ClusterPrivilegeResolver {
"indices:admin/index_template/*"
);
private static final Predicate<String> ACTION_MATCHER = Automatons.predicate(ALL_CLUSTER_PATTERN);
private static final Set<String> MANAGE_INFERENCE_PATTERN = Set.of(
"cluster:admin/xpack/inference/*",
"cluster:monitor/xpack/inference*", // no trailing slash to match the POST InferenceAction name
"cluster:admin/xpack/ml/trained_models/deployment/start",
"cluster:admin/xpack/ml/trained_models/deployment/stop",
"cluster:monitor/xpack/ml/trained_models/deployment/infer"
);
private static final Set<String> MANAGE_ML_PATTERN = Set.of("cluster:admin/xpack/ml/*", "cluster:monitor/xpack/ml/*");
private static final Set<String> MANAGE_TRANSFORM_PATTERN = Set.of(
"cluster:admin/data_frame/*",
Expand Down Expand Up @@ -182,6 +193,10 @@ public class ClusterPrivilegeResolver {
public static final NamedClusterPrivilege NONE = new ActionClusterPrivilege("none", Set.of(), Set.of());
public static final NamedClusterPrivilege ALL = new ActionClusterPrivilege("all", ALL_CLUSTER_PATTERN);
public static final NamedClusterPrivilege MONITOR = new ActionClusterPrivilege("monitor", MONITOR_PATTERN);
public static final NamedClusterPrivilege MONITOR_INFERENCE = new ActionClusterPrivilege(
"monitor_inference",
MONITOR_INFERENCE_PATTERN
);
public static final NamedClusterPrivilege MONITOR_ML = new ActionClusterPrivilege("monitor_ml", MONITOR_ML_PATTERN);
public static final NamedClusterPrivilege MONITOR_TRANSFORM_DEPRECATED = new ActionClusterPrivilege(
"monitor_data_frame_transforms",
Expand All @@ -199,6 +214,7 @@ public class ClusterPrivilegeResolver {
public static final NamedClusterPrivilege MONITOR_ROLLUP = new ActionClusterPrivilege("monitor_rollup", MONITOR_ROLLUP_PATTERN);
public static final NamedClusterPrivilege MONITOR_ENRICH = new ActionClusterPrivilege("monitor_enrich", MONITOR_ENRICH_PATTERN);
public static final NamedClusterPrivilege MANAGE = new ActionClusterPrivilege("manage", ALL_CLUSTER_PATTERN, ALL_SECURITY_PATTERN);
public static final NamedClusterPrivilege MANAGE_INFERENCE = new ActionClusterPrivilege("manage_inference", MANAGE_INFERENCE_PATTERN);
public static final NamedClusterPrivilege MANAGE_ML = new ActionClusterPrivilege("manage_ml", MANAGE_ML_PATTERN);
public static final NamedClusterPrivilege MANAGE_TRANSFORM_DEPRECATED = new ActionClusterPrivilege(
"manage_data_frame_transforms",
Expand Down Expand Up @@ -348,6 +364,7 @@ public class ClusterPrivilegeResolver {
NONE,
ALL,
MONITOR,
MONITOR_INFERENCE,
MONITOR_ML,
MONITOR_TEXT_STRUCTURE,
MONITOR_TRANSFORM_DEPRECATED,
Expand All @@ -356,6 +373,7 @@ public class ClusterPrivilegeResolver {
MONITOR_ROLLUP,
MONITOR_ENRICH,
MANAGE,
MANAGE_INFERENCE,
MANAGE_ML,
MANAGE_TRANSFORM_DEPRECATED,
MANAGE_TRANSFORM,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,32 @@ private static Map<String, RoleDescriptor> initializeReservedRoles() {
null
)
),
entry(
"inference_admin",
new RoleDescriptor(
"inference_admin",
new String[] { "manage_inference" },
null,
null,
null,
null,
MetadataUtils.DEFAULT_RESERVED_METADATA,
null
)
),
entry(
"inference_user",
new RoleDescriptor(
"inference_user",
new String[] { "monitor_inference" },
null,
null,
null,
null,
MetadataUtils.DEFAULT_RESERVED_METADATA,
null
)
),
entry(
"machine_learning_user",
new RoleDescriptor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ public void testIsReserved() {
assertThat(ReservedRolesStore.isReserved("transport_client"), is(true));
assertThat(ReservedRolesStore.isReserved("kibana_admin"), is(true));
assertThat(ReservedRolesStore.isReserved("kibana_user"), is(true));
assertThat(ReservedRolesStore.isReserved("inference_admin"), is(true));
assertThat(ReservedRolesStore.isReserved("inference_user"), is(true));
assertThat(ReservedRolesStore.isReserved("ingest_admin"), is(true));
assertThat(ReservedRolesStore.isReserved("monitoring_user"), is(true));
assertThat(ReservedRolesStore.isReserved("reporting_user"), is(true));
Expand Down Expand Up @@ -3877,6 +3879,46 @@ public void testEnrichUserRole() {
assertOnlyReadAllowed(role, ".enrich-foo");
}

public void testInferenceAdminRole() {
final TransportRequest request = mock(TransportRequest.class);
final Authentication authentication = AuthenticationTestHelper.builder().build();

RoleDescriptor roleDescriptor = ReservedRolesStore.roleDescriptor("inference_admin");
assertNotNull(roleDescriptor);
assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true));

Role role = Role.buildFromRoleDescriptor(roleDescriptor, new FieldPermissionsCache(Settings.EMPTY), RESTRICTED_INDICES);
assertTrue(role.cluster().check("cluster:monitor/xpack/inference", request, authentication));
assertTrue(role.cluster().check("cluster:monitor/xpack/inference/get", request, authentication));
assertTrue(role.cluster().check("cluster:admin/xpack/inference/put", request, authentication));
assertTrue(role.cluster().check("cluster:admin/xpack/inference/delete", request, authentication));
assertTrue(role.cluster().check("cluster:monitor/xpack/ml/trained_models/deployment/infer", request, authentication));
assertTrue(role.cluster().check("cluster:admin/xpack/ml/trained_models/deployment/start", request, authentication));
assertTrue(role.cluster().check("cluster:admin/xpack/ml/trained_models/deployment/stop", request, authentication));
assertFalse(role.runAs().check(randomAlphaOfLengthBetween(1, 30)));
assertNoAccessAllowed(role, ".inference");
}

public void testInferenceUserRole() {
final TransportRequest request = mock(TransportRequest.class);
final Authentication authentication = AuthenticationTestHelper.builder().build();

RoleDescriptor roleDescriptor = ReservedRolesStore.roleDescriptor("inference_user");
assertNotNull(roleDescriptor);
assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true));

Role role = Role.buildFromRoleDescriptor(roleDescriptor, new FieldPermissionsCache(Settings.EMPTY), RESTRICTED_INDICES);
assertTrue(role.cluster().check("cluster:monitor/xpack/inference", request, authentication));
assertTrue(role.cluster().check("cluster:monitor/xpack/inference/get", request, authentication));
assertFalse(role.cluster().check("cluster:admin/xpack/inference/put", request, authentication));
assertFalse(role.cluster().check("cluster:admin/xpack/inference/delete", request, authentication));
assertTrue(role.cluster().check("cluster:monitor/xpack/ml/trained_models/deployment/infer", request, authentication));
assertFalse(role.cluster().check("cluster:admin/xpack/ml/trained_models/deployment/start", request, authentication));
assertFalse(role.cluster().check("cluster:admin/xpack/ml/trained_models/deployment/stop", request, authentication));
assertFalse(role.runAs().check(randomAlphaOfLengthBetween(1, 30)));
assertNoAccessAllowed(role, ".inference");
}

private IndexAbstraction mockIndexAbstraction(String name) {
IndexAbstraction mock = mock(IndexAbstraction.class);
when(mock.getName()).thenReturn(name);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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.inference;

import org.apache.http.HttpHost;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.test.cluster.ElasticsearchCluster;
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
import org.elasticsearch.test.rest.ESRestTestCase;
import org.junit.ClassRule;

import java.io.IOException;

import static org.hamcrest.Matchers.equalTo;

public class InferencePermissionsIT extends ESRestTestCase {

private static final String PASSWORD = "secret-test-password";

@ClassRule
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
.distribution(DistributionType.DEFAULT)
.setting("xpack.license.self_generated.type", "trial")
.setting("xpack.security.enabled", "true")
.plugin("inference-service-test")
.user("x_pack_rest_user", "x-pack-test-password")
.user("test_inference_admin", PASSWORD, "inference_admin", false)
.user("test_inference_user", PASSWORD, "inference_user", false)
.user("test_no_privileged", PASSWORD, "", false)
.build();

@Override
protected String getTestRestCluster() {
return cluster.getHttpAddresses();
}

@Override
protected Settings restClientSettings() {
// use the privileged users here but not in the tests
String token = basicAuthHeaderValue("x_pack_rest_user", new SecureString("x-pack-test-password".toCharArray()));
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
}

public void testPermissions() throws IOException {
var putRequest = new Request("PUT", "_inference/sparse_embedding/permissions_test");
putRequest.setJsonEntity(InferenceBaseRestTest.mockSparseServiceModelConfig());
var getAllRequest = new Request("GET", "_inference/sparse_embedding/_all");
var deleteRequest = new Request("DELETE", "_inference/sparse_embedding/permissions_test");

var putModelForTestingInference = new Request("PUT", "_inference/sparse_embedding/model_to_test_user_priv");
putModelForTestingInference.setJsonEntity(InferenceBaseRestTest.mockSparseServiceModelConfig());

var inferRequest = new Request("POST", "_inference/sparse_embedding/model_to_test_user_priv");
var bodyBuilder = new StringBuilder("{\"input\": [");
for (var in : new String[] { "foo", "bar" }) {
bodyBuilder.append('"').append(in).append('"').append(',');
}
// remove last comma
bodyBuilder.deleteCharAt(bodyBuilder.length() - 1);
bodyBuilder.append("]}");
inferRequest.setJsonEntity(bodyBuilder.toString());

var deleteInferenceModel = new Request("DELETE", "_inference/sparse_embedding/model_to_test_user_priv");

try (RestClient inferenceAdminClient = buildClient(inferenceAdminClientSettings(), getClusterHosts().toArray(new HttpHost[0]))) {
makeRequest(inferenceAdminClient, putRequest, true);
makeRequest(inferenceAdminClient, getAllRequest, true);
makeRequest(inferenceAdminClient, deleteRequest, true);
// create a model now as the other clients don't have the privilege to do so
makeRequest(inferenceAdminClient, putModelForTestingInference, true);
makeRequest(inferenceAdminClient, inferRequest, true);
}

try (RestClient inferenceUserClient = buildClient(inferenceUserClientSettings(), getClusterHosts().toArray(new HttpHost[0]))) {
makeRequest(inferenceUserClient, putRequest, false);
makeRequest(inferenceUserClient, getAllRequest, true);
makeRequest(inferenceUserClient, inferRequest, true);
makeRequest(inferenceUserClient, deleteInferenceModel, false);
}

try (RestClient unprivilegedClient = buildClient(unprivilegedUserClientSettings(), getClusterHosts().toArray(new HttpHost[0]))) {
makeRequest(unprivilegedClient, putRequest, false);
makeRequest(unprivilegedClient, getAllRequest, false);
makeRequest(unprivilegedClient, inferRequest, false);
makeRequest(unprivilegedClient, deleteInferenceModel, false);
}
}

private Settings inferenceAdminClientSettings() {
String token = basicAuthHeaderValue("test_inference_admin", new SecureString(PASSWORD.toCharArray()));
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
}

private Settings inferenceUserClientSettings() {
String token = basicAuthHeaderValue("test_inference_user", new SecureString(PASSWORD.toCharArray()));
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
}

private Settings unprivilegedUserClientSettings() {
String token = basicAuthHeaderValue("test_no_privileged", new SecureString(PASSWORD.toCharArray()));
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
}

/*
* This makes the given request with the given client. It asserts a 200 response if expectSuccess is true, and asserts an exception
* with a 403 response if expectStatus is false.
*/
private void makeRequest(RestClient client, Request request, boolean expectSuccess) throws IOException {
if (expectSuccess) {
Response response = client.performRequest(request);
assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus()));
} else {
ResponseException exception = expectThrows(ResponseException.class, () -> client.performRequest(request));
assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ setup:
# This is fragile - it needs to be updated every time we add a new cluster/index privilege
# I would much prefer we could just check that specific entries are in the array, but we don't have
# an assertion for that
- length: { "cluster" : 55 }
- length: { "cluster" : 57 }
- length: { "index" : 22 }

0 comments on commit 2087b65

Please sign in to comment.