DEFAULT_EXECUTABLE_CONTEXTS = unmodifiableList(Arrays.asList(
new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("do"), DoSection::parse),
new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("set"), SetSection::parse),
+ new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("transform_and_set"), TransformAndSetSection::parse),
new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("match"), MatchAssertion::parse),
new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("is_true"), IsTrueAssertion::parse),
new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("is_false"), IsFalseAssertion::parse),
diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSection.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSection.java
new file mode 100644
index 0000000000000..7b0b915dd97df
--- /dev/null
+++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSection.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.test.rest.yaml.section;
+
+import org.elasticsearch.common.ParsingException;
+import org.elasticsearch.common.xcontent.XContentLocation;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.rest.yaml.ClientYamlTestExecutionContext;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a transform_and_set section:
+ *
+ *
+ * In the following example,
+ * - transform_and_set: { login_creds: "#base64EncodeCredentials(user,password)" }
+ * user and password are from the response which are joined by ':' and Base64 encoded and then stashed as 'login_creds'
+ *
+ */
+public class TransformAndSetSection implements ExecutableSection {
+ public static TransformAndSetSection parse(XContentParser parser) throws IOException {
+ String currentFieldName = null;
+ XContentParser.Token token;
+
+ TransformAndSetSection transformAndStashSection = new TransformAndSetSection(parser.getTokenLocation());
+
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+ } else if (token.isValue()) {
+ transformAndStashSection.addSet(currentFieldName, parser.text());
+ }
+ }
+
+ parser.nextToken();
+
+ if (transformAndStashSection.getStash().isEmpty()) {
+ throw new ParsingException(transformAndStashSection.location, "transform_and_set section must set at least a value");
+ }
+
+ return transformAndStashSection;
+ }
+
+ private final Map transformStash = new HashMap<>();
+ private final XContentLocation location;
+
+ public TransformAndSetSection(XContentLocation location) {
+ this.location = location;
+ }
+
+ public void addSet(String stashedField, String transformThis) {
+ transformStash.put(stashedField, transformThis);
+ }
+
+ public Map getStash() {
+ return transformStash;
+ }
+
+ @Override
+ public XContentLocation getLocation() {
+ return location;
+ }
+
+ @Override
+ public void execute(ClientYamlTestExecutionContext executionContext) throws IOException {
+ for (Map.Entry entry : transformStash.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ if (value.startsWith("#base64EncodeCredentials(") && value.endsWith(")")) {
+ value = entry.getValue().substring("#base64EncodeCredentials(".length(), entry.getValue().lastIndexOf(")"));
+ String[] idAndPassword = value.split(",");
+ if (idAndPassword.length == 2) {
+ String credentials = executionContext.response(idAndPassword[0].trim()) + ":"
+ + executionContext.response(idAndPassword[1].trim());
+ value = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ } else {
+ throw new IllegalArgumentException("base64EncodeCredentials requires a username/id and a password parameters");
+ }
+ }
+ executionContext.stash().stashValue(key, value);
+ }
+ }
+
+}
diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSectionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSectionTests.java
new file mode 100644
index 0000000000000..a61f91de287e7
--- /dev/null
+++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSectionTests.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.test.rest.yaml.section;
+
+import org.elasticsearch.common.ParsingException;
+import org.elasticsearch.common.xcontent.yaml.YamlXContent;
+import org.elasticsearch.test.rest.yaml.ClientYamlTestExecutionContext;
+import org.elasticsearch.test.rest.yaml.Stash;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class TransformAndSetSectionTests extends AbstractClientYamlTestFragmentParserTestCase {
+
+ public void testParseSingleValue() throws Exception {
+ parser = createParser(YamlXContent.yamlXContent,
+ "{ key: value }"
+ );
+
+ TransformAndSetSection transformAndSet = TransformAndSetSection.parse(parser);
+ assertThat(transformAndSet, notNullValue());
+ assertThat(transformAndSet.getStash(), notNullValue());
+ assertThat(transformAndSet.getStash().size(), equalTo(1));
+ assertThat(transformAndSet.getStash().get("key"), equalTo("value"));
+ }
+
+ public void testParseMultipleValues() throws Exception {
+ parser = createParser(YamlXContent.yamlXContent,
+ "{ key1: value1, key2: value2 }"
+ );
+
+ TransformAndSetSection transformAndSet = TransformAndSetSection.parse(parser);
+ assertThat(transformAndSet, notNullValue());
+ assertThat(transformAndSet.getStash(), notNullValue());
+ assertThat(transformAndSet.getStash().size(), equalTo(2));
+ assertThat(transformAndSet.getStash().get("key1"), equalTo("value1"));
+ assertThat(transformAndSet.getStash().get("key2"), equalTo("value2"));
+ }
+
+ public void testTransformation() throws Exception {
+ parser = createParser(YamlXContent.yamlXContent, "{ login_creds: \"#base64EncodeCredentials(id,api_key)\" }");
+
+ TransformAndSetSection transformAndSet = TransformAndSetSection.parse(parser);
+ assertThat(transformAndSet, notNullValue());
+ assertThat(transformAndSet.getStash(), notNullValue());
+ assertThat(transformAndSet.getStash().size(), equalTo(1));
+ assertThat(transformAndSet.getStash().get("login_creds"), equalTo("#base64EncodeCredentials(id,api_key)"));
+
+ ClientYamlTestExecutionContext executionContext = mock(ClientYamlTestExecutionContext.class);
+ when(executionContext.response("id")).thenReturn("user");
+ when(executionContext.response("api_key")).thenReturn("password");
+ Stash stash = new Stash();
+ when(executionContext.stash()).thenReturn(stash);
+ transformAndSet.execute(executionContext);
+ verify(executionContext).response("id");
+ verify(executionContext).response("api_key");
+ verify(executionContext).stash();
+ assertThat(stash.getValue("$login_creds"),
+ equalTo(Base64.getEncoder().encodeToString("user:password".getBytes(StandardCharsets.UTF_8))));
+ verifyNoMoreInteractions(executionContext);
+ }
+
+ public void testParseSetSectionNoValues() throws Exception {
+ parser = createParser(YamlXContent.yamlXContent,
+ "{ }"
+ );
+
+ Exception e = expectThrows(ParsingException.class, () -> TransformAndSetSection.parse(parser));
+ assertThat(e.getMessage(), is("transform_and_set section must set at least a value"));
+ }
+}
diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle
index 27f815b1637f1..ecfd30bb7469b 100644
--- a/x-pack/docs/build.gradle
+++ b/x-pack/docs/build.gradle
@@ -73,6 +73,7 @@ project.copyRestSpec.from(xpackResources) {
}
integTestCluster {
setting 'xpack.security.enabled', 'true'
+ setting 'xpack.security.authc.api_key.enabled', 'true'
setting 'xpack.security.authc.token.enabled', 'true'
// Disable monitoring exporters for the docs tests
setting 'xpack.monitoring.exporters._local.type', 'local'
diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc
index 851bd2ba327b2..c59c44312ae60 100644
--- a/x-pack/docs/en/rest-api/security.asciidoc
+++ b/x-pack/docs/en/rest-api/security.asciidoc
@@ -51,6 +51,17 @@ without requiring basic authentication:
* <>
* <>
+[float]
+[[security-api-keys]]
+=== API Keys
+
+You can use the following APIs to create, retrieve and invalidate API keys for access
+without requiring basic authentication:
+
+* <