Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor request marking for serverless and operator modes #110370

Merged
merged 34 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6741289
Rename path_restricted to restrict_for_serverless
n1v0lg Jul 2, 2024
c76b56d
Should
n1v0lg Jul 2, 2024
bc973c9
FIx
n1v0lg Jul 2, 2024
5734020
Renames
n1v0lg Jul 2, 2024
083995c
Naming is easy
n1v0lg Jul 2, 2024
eef7fbf
One more
n1v0lg Jul 3, 2024
55e562f
Fix
n1v0lg Jul 3, 2024
2160008
More
n1v0lg Jul 3, 2024
ba37140
Typos and assert
n1v0lg Jul 3, 2024
01b9171
Merge branch 'main' into rename-path-restricted
n1v0lg Jul 9, 2024
efb0cba
Merge branch 'main' into rename-path-restricted
n1v0lg Jul 16, 2024
4a87239
Two params
n1v0lg Jul 16, 2024
409e957
Moar
n1v0lg Jul 16, 2024
24b4891
Use feature to unbreak stateful
n1v0lg Jul 17, 2024
c9eecb2
Merge branch 'main' into rename-path-restricted
n1v0lg Jul 17, 2024
046bd6e
More
n1v0lg Jul 17, 2024
2fc4e32
Supported params
n1v0lg Jul 17, 2024
41a477a
Params
n1v0lg Jul 17, 2024
058f591
Merge branch 'main' into rename-path-restricted
n1v0lg Jul 18, 2024
340dde0
Tests
n1v0lg Jul 18, 2024
ca33943
Will this work
n1v0lg Jul 18, 2024
bf0d176
Merge branch 'main' into rename-path-restricted
n1v0lg Jul 19, 2024
0581c71
Clean up
n1v0lg Jul 19, 2024
1081d32
Clean up docs tests
n1v0lg Jul 19, 2024
86a066d
More
n1v0lg Jul 19, 2024
e71cd56
Merge branch 'main' into rename-path-restricted
n1v0lg Jul 19, 2024
52146d2
Merge branch 'main' into rename-path-restricted
n1v0lg Jul 19, 2024
dea8e5b
Merge branch 'main' into rename-path-restricted
n1v0lg Jul 22, 2024
d25a178
Merge branch 'main' into rename-path-restricted
n1v0lg Jul 22, 2024
a95764f
Merge branch 'main' into rename-path-restricted
n1v0lg Jul 23, 2024
f9b728b
Merge branch 'main' into rename-path-restricted
n1v0lg Aug 8, 2024
82d3db7
Merge
n1v0lg Aug 12, 2024
451e10d
Address comments
n1v0lg Aug 12, 2024
7b3f927
Merge branch 'main' into rename-path-restricted
n1v0lg Aug 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ public static DataStreamLifecycle fromXContent(XContentParser parser) throws IOE
* Adds a retention param to signal that this serialisation should include the effective retention metadata
*/
public static ToXContent.Params maybeAddEffectiveRetentionParams(ToXContent.Params params) {
boolean shouldAddEffectiveRetention = Objects.equals(params.param(RestRequest.PATH_RESTRICTED), "serverless");
boolean shouldAddEffectiveRetention = params.paramAsBoolean(RestRequest.USE_SERVERLESS_PARTIAL_API_RESTRICTIONS, false);
return new DelegatingMapParams(
Map.of(INCLUDE_EFFECTIVE_RETENTION_PARAM_NAME, Boolean.toString(shouldAddEffectiveRetention)),
params
Expand Down
32 changes: 26 additions & 6 deletions server/src/main/java/org/elasticsearch/rest/RestRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,18 @@

public class RestRequest implements ToXContent.Params, Traceable {

public static final String PATH_RESTRICTED = "pathRestricted";
/**
* This internal parameter indicates that APIs that have partial API restrictions in Serverless mode should apply them to the request.
* Partial API restrictions are for APIs that are public in Serverless but prevent the use of certain fields in the request, omit
* fields from the response, or otherwise partially restrict functionality.
*
* This parameter is set for all request that are made in Serverless mode against a public API by non-operator users.
*
* If you have an API with partial restrictions, use this flag to check if you need to apply them or not (i.e., if the request is
* subject to them, or is exempt). You can check for the presence of this parameter among the REST parameters of the request instance,
* or use {@link #shouldUseServerlessPartialApiRestrictions()}.
*/
public static final String USE_SERVERLESS_PARTIAL_API_RESTRICTIONS = "useServerlessPartialApiRestrictions";
// tchar pattern as defined by RFC7230 section 3.2.6
private static final Pattern TCHAR_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+\\-.\\^_`|~]+");

Expand Down Expand Up @@ -616,13 +627,22 @@ public boolean hasExplicitRestApiVersion() {
return restApiVersion.isPresent();
}

public void markPathRestricted(String restriction) {
if (params.containsKey(PATH_RESTRICTED)) {
throw new IllegalArgumentException("The parameter [" + PATH_RESTRICTED + "] is already defined.");
public void setUseServerlessPartialApiRestrictions() {
if (params.containsKey(USE_SERVERLESS_PARTIAL_API_RESTRICTIONS)) {
throw new IllegalArgumentException("The parameter [" + USE_SERVERLESS_PARTIAL_API_RESTRICTIONS + "] is already defined.");
}
params.put(PATH_RESTRICTED, restriction);
params.put(USE_SERVERLESS_PARTIAL_API_RESTRICTIONS, "true");
n1v0lg marked this conversation as resolved.
Show resolved Hide resolved
// this parameter is intended be consumed via ToXContent.Params.param(..), not this.params(..) so don't require it is consumed here
consumedParams.add(PATH_RESTRICTED);
consumedParams.add(USE_SERVERLESS_PARTIAL_API_RESTRICTIONS);
}

/**
* @see #USE_SERVERLESS_PARTIAL_API_RESTRICTIONS
*/
public boolean shouldUseServerlessPartialApiRestrictions() {
final boolean hasParam = hasParam(USE_SERVERLESS_PARTIAL_API_RESTRICTIONS);
assert false == hasParam || params.get(USE_SERVERLESS_PARTIAL_API_RESTRICTIONS).equals("true");
return hasParam;
}

@Override
Expand Down
8 changes: 5 additions & 3 deletions server/src/main/java/org/elasticsearch/rest/RestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import java.util.regex.Pattern;

import static org.elasticsearch.action.support.master.AcknowledgedRequest.DEFAULT_ACK_TIMEOUT;
import static org.elasticsearch.rest.RestRequest.PATH_RESTRICTED;
import static org.elasticsearch.rest.RestRequest.USE_SERVERLESS_PARTIAL_API_RESTRICTIONS;

public class RestUtils {

Expand Down Expand Up @@ -85,8 +85,10 @@ private static String decodeQueryStringParam(final String s) {
}

private static void addParam(Map<String, String> params, String name, String value) {
if (PATH_RESTRICTED.equalsIgnoreCase(name)) {
throw new IllegalArgumentException("parameter [" + PATH_RESTRICTED + "] is reserved and may not set");
if (USE_SERVERLESS_PARTIAL_API_RESTRICTIONS.equalsIgnoreCase(name)) {
throw new IllegalArgumentException(
"parameter [" + USE_SERVERLESS_PARTIAL_API_RESTRICTIONS + "] is reserved and may not be set"
);
}
params.put(name, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,4 @@
@Target(ElementType.TYPE)
public @interface ServerlessScope {
Scope value();

/**
* A value used when restricting a response of a serverless endpoints.
*/
String SERVERLESS_RESTRICTION = "serverless";
n1v0lg marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import java.io.IOException;
import java.util.Map;

import static org.elasticsearch.rest.RestRequest.PATH_RESTRICTED;
import static org.elasticsearch.rest.RestRequest.USE_SERVERLESS_PARTIAL_API_RESTRICTIONS;
import static org.hamcrest.Matchers.equalTo;

public class GetDataStreamLifecycleActionTests extends ESTestCase {
Expand Down Expand Up @@ -75,7 +75,7 @@ private Map<String, Object> getXContentMap(
TimeValue globalMaxRetention
) throws IOException {
try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
ToXContent.Params params = new ToXContent.MapParams(Map.of(PATH_RESTRICTED, "serverless"));
ToXContent.Params params = new ToXContent.MapParams(Map.of(USE_SERVERLESS_PARTIAL_API_RESTRICTIONS, "true"));
RolloverConfiguration rolloverConfiguration = null;
DataStreamGlobalRetention globalRetention = new DataStreamGlobalRetention(globalDefaultRetention, globalMaxRetention);
dataStreamLifecycle.toXContent(builder, params, rolloverConfiguration, globalRetention);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import static org.elasticsearch.cluster.metadata.DataStreamLifecycle.RetentionSource.DATA_STREAM_CONFIGURATION;
import static org.elasticsearch.cluster.metadata.DataStreamLifecycle.RetentionSource.DEFAULT_GLOBAL_RETENTION;
import static org.elasticsearch.cluster.metadata.DataStreamLifecycle.RetentionSource.MAX_GLOBAL_RETENTION;
import static org.elasticsearch.rest.RestRequest.PATH_RESTRICTED;
import static org.elasticsearch.rest.RestRequest.USE_SERVERLESS_PARTIAL_API_RESTRICTIONS;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
Expand Down Expand Up @@ -354,13 +354,7 @@ public void testEffectiveRetentionParams() {
}
{
ToXContent.Params params = DataStreamLifecycle.maybeAddEffectiveRetentionParams(
new ToXContent.MapParams(Map.of(PATH_RESTRICTED, "not-serverless"))
);
assertThat(params.paramAsBoolean(DataStreamLifecycle.INCLUDE_EFFECTIVE_RETENTION_PARAM_NAME, false), equalTo(false));
}
{
ToXContent.Params params = DataStreamLifecycle.maybeAddEffectiveRetentionParams(
new ToXContent.MapParams(Map.of(PATH_RESTRICTED, "serverless"))
new ToXContent.MapParams(Map.of(USE_SERVERLESS_PARTIAL_API_RESTRICTIONS, "true"))
);
assertThat(params.paramAsBoolean(DataStreamLifecycle.INCLUDE_EFFECTIVE_RETENTION_PARAM_NAME, false), equalTo(true));
}
Expand Down
21 changes: 11 additions & 10 deletions server/src/test/java/org/elasticsearch/rest/RestRequestTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.elasticsearch.rest.RestRequest.PATH_RESTRICTED;
import static org.elasticsearch.rest.RestRequest.USE_SERVERLESS_PARTIAL_API_RESTRICTIONS;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
Expand Down Expand Up @@ -249,16 +249,17 @@ public void testRequiredContent() {
assertEquals("unknown content type", e.getMessage());
}

public void testMarkPathRestricted() {
public void testUseServerlessPartialApiRestrictions() {
RestRequest request1 = contentRestRequest("content", new HashMap<>());
request1.markPathRestricted("foo");
assertEquals(request1.param(PATH_RESTRICTED), "foo");
IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> request1.markPathRestricted("foo"));
assertThat(exception.getMessage(), is("The parameter [" + PATH_RESTRICTED + "] is already defined."));

RestRequest request2 = contentRestRequest("content", Map.of(PATH_RESTRICTED, "foo"));
exception = expectThrows(IllegalArgumentException.class, () -> request2.markPathRestricted("bar"));
assertThat(exception.getMessage(), is("The parameter [" + PATH_RESTRICTED + "] is already defined."));
request1.setUseServerlessPartialApiRestrictions();
assertEquals(request1.param(USE_SERVERLESS_PARTIAL_API_RESTRICTIONS), "true");
assertTrue(request1.shouldUseServerlessPartialApiRestrictions());
IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, request1::setUseServerlessPartialApiRestrictions);
assertThat(exception.getMessage(), is("The parameter [" + USE_SERVERLESS_PARTIAL_API_RESTRICTIONS + "] is already defined."));

RestRequest request2 = contentRestRequest("content", Map.of(USE_SERVERLESS_PARTIAL_API_RESTRICTIONS, "true"));
exception = expectThrows(IllegalArgumentException.class, request2::setUseServerlessPartialApiRestrictions);
assertThat(exception.getMessage(), is("The parameter [" + USE_SERVERLESS_PARTIAL_API_RESTRICTIONS + "] is already defined."));
}

public static RestRequest contentRestRequest(String content, Map<String, String> params) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import java.util.Map;
import java.util.regex.Pattern;

import static org.elasticsearch.rest.RestRequest.PATH_RESTRICTED;
import static org.elasticsearch.rest.RestRequest.USE_SERVERLESS_PARTIAL_API_RESTRICTIONS;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
Expand Down Expand Up @@ -161,12 +161,12 @@ public void testCrazyURL() {

public void testReservedParameters() {
Map<String, String> params = new HashMap<>();
String uri = "something?" + PATH_RESTRICTED + "=value";
String uri = "something?" + USE_SERVERLESS_PARTIAL_API_RESTRICTIONS + "=value";
IllegalArgumentException exception = expectThrows(
IllegalArgumentException.class,
() -> RestUtils.decodeQueryString(uri, uri.indexOf('?') + 1, params)
);
assertEquals(exception.getMessage(), "parameter [" + PATH_RESTRICTED + "] is reserved and may not set");
assertEquals(exception.getMessage(), "parameter [" + USE_SERVERLESS_PARTIAL_API_RESTRICTIONS + "] is reserved and may not be set");
}

private void assertCorsSettingRegexIsNull(String settingsValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@ public interface OperatorOnlyRegistry {
* A fully restricted REST API mandates that the implementation of this method throw an
* {@link org.elasticsearch.ElasticsearchStatusException} with an appropriate status code and error message.
*
* A partially restricted REST API mandates that the {@link RestRequest} is marked as restricted so that the downstream handler can
* behave appropriately.
* For example, to restrict the REST response the implementation
* should call {@link RestRequest#markPathRestricted(String)} so that the downstream handler can properly restrict the response
* before returning to the client. Note - a partial restriction should not throw an exception.
* A partially restricted REST API is available to non-operator users but either the request or response for it is restricted
* (e.g., certain fields are not allowed in the request).
*
* @param restHandler The {@link RestHandler} to check for any restrictions
* @param restRequest The {@link RestRequest} to check for any restrictions and mark any partially restricted REST API's
* The implementation of this method should mark all {@link RestRequest}s for APIs that are *not* fully restricted as candidates
* for partial restriction. It's the responsibility of the downstream REST handler to apply the necessary partial restrictions
* (if any).
*
* Marking requests is an implementation-specific detail. For Serverless mode restrictions, this is implemented via
* {@link RestRequest#setUseServerlessPartialApiRestrictions()} and {@link RestRequest#shouldUseServerlessPartialApiRestrictions()}}.
*
* @param restHandler The {@link RestHandler} to check for any full restrictions
* @param restRequest The {@link RestRequest} to mark all REST APIs that are not fully restricted as possible subjects to partial
* restrictions
* @throws ElasticsearchStatusException if the request should be denied in its entirety (fully restricted)
*/
void checkRest(RestHandler restHandler, RestRequest restRequest) throws ElasticsearchException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public String getName() {

@Override
protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException {
CreateApiKeyRequestBuilder builder = builderFactory.create(client, request.hasParam(RestRequest.PATH_RESTRICTED))
CreateApiKeyRequestBuilder builder = builderFactory.create(client, request.shouldUseServerlessPartialApiRestrictions())
.source(request.requiredContent(), request.getXContentType());
String refresh = request.param("refresh");
if (refresh != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@ public String getName() {

@Override
public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
final boolean restrictResponse = request.hasParam(RestRequest.PATH_RESTRICTED);
final boolean shouldRestrictForServerless = request.shouldUseServerlessPartialApiRestrictions();
return channel -> client.execute(
GetBuiltinPrivilegesAction.INSTANCE,
new GetBuiltinPrivilegesRequest(),
new RestBuilderListener<>(channel) {
@Override
public RestResponse buildResponse(GetBuiltinPrivilegesResponse response, XContentBuilder builder) throws Exception {
final var translatedResponse = responseTranslator.translate(response, restrictResponse);
final var translatedResponse = responseTranslator.translate(response, shouldRestrictForServerless);
builder.startObject();
builder.array("cluster", translatedResponse.getClusterPrivileges());
builder.array("index", translatedResponse.getIndexPrivileges());
Expand All @@ -86,9 +86,9 @@ public RestResponse buildResponse(GetBuiltinPrivilegesResponse response, XConten

@Override
protected Exception innerCheckFeatureAvailable(RestRequest request) {
final boolean restrictPath = request.hasParam(RestRequest.PATH_RESTRICTED);
assert false == restrictPath || DiscoveryNode.isStateless(settings);
if (false == restrictPath) {
final boolean shouldRestrictForServerless = request.shouldUseServerlessPartialApiRestrictions();
assert false == shouldRestrictForServerless || DiscoveryNode.isStateless(settings);
if (false == shouldRestrictForServerless) {
return super.innerCheckFeatureAvailable(request);
}
// This is a temporary hack: we are re-using the native roles setting as an overall feature flag for custom roles.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
package org.elasticsearch.xpack.security.rest.action.role;

import org.elasticsearch.client.internal.node.NodeClient;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.RestApiVersion;
Expand Down Expand Up @@ -54,9 +53,9 @@ public String getName() {
@Override
public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
final String[] roles = request.paramAsStringArray("name", Strings.EMPTY_ARRAY);
final boolean restrictRequest = isPathRestricted(request);
final boolean restrictToNativeRolesOnly = request.shouldUseServerlessPartialApiRestrictions();
return channel -> new GetRolesRequestBuilder(client).names(roles)
.nativeOnly(restrictRequest)
.nativeOnly(restrictToNativeRolesOnly)
.execute(new RestBuilderListener<>(channel) {
@Override
public RestResponse buildResponse(GetRolesResponse response, XContentBuilder builder) throws Exception {
Expand Down Expand Up @@ -84,17 +83,10 @@ protected Exception innerCheckFeatureAvailable(RestRequest request) {
// Note: For non-restricted requests this action handles both reserved roles and native
// roles, and should still be available even if native role management is disabled.
// For restricted requests it should only be available if native role management is enabled
final boolean restrictPath = isPathRestricted(request);
if (false == restrictPath) {
if (false == request.shouldUseServerlessPartialApiRestrictions()) {
return null;
} else {
return super.innerCheckFeatureAvailable(request);
}
}

private boolean isPathRestricted(RestRequest request) {
final boolean restrictRequest = request.hasParam(RestRequest.PATH_RESTRICTED);
assert false == restrictRequest || DiscoveryNode.isStateless(settings);
return restrictRequest;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public String getName() {

@Override
public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
final boolean restrictRequest = request.hasParam(RestRequest.PATH_RESTRICTED);
final boolean restrictRequest = request.shouldUseServerlessPartialApiRestrictions();
final PutRoleRequestBuilder requestBuilder = builderFactory.create(client, restrictRequest)
.source(request.param("name"), request.requiredContent(), request.getXContentType())
.setRefreshPolicy(request.param("refresh"));
Expand Down