From a5874e0eb3c06831f1fa303330e2fd5bacb313da Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Thu, 5 Jul 2018 10:51:07 -0500 Subject: [PATCH 1/7] Watcher: Store username on watch execution There is currently no way to see what user executed a watch. This commit adds the decrypted username to each execution in the watch history, in a new field executed_by. Closes #31772 --- .../core/security/authc/Authentication.java | 10 +++- .../execution/WatchExecutionContext.java | 16 ++++++ .../core/watcher/history/WatchRecord.java | 17 ++++-- .../WatcherIndexTemplateRegistryField.java | 3 +- .../src/main/resources/watch-history.json | 3 + .../roles.yml | 1 + .../20_test_run_as_execute_watch.yml | 56 +++++++++++++++++++ 7 files changed, 97 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index 2a2fdd95d61a9..161d9d449990f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -88,13 +88,17 @@ static Authentication deserializeHeaderAndPutInContext(String header, ThreadCont throws IOException, IllegalArgumentException { assert ctx.getTransient(AuthenticationField.AUTHENTICATION_KEY) == null; + Authentication authentication = decode(header); + ctx.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication); + return authentication; + } + + public static Authentication decode(String header) throws IOException { byte[] bytes = Base64.getDecoder().decode(header); StreamInput input = StreamInput.wrap(bytes); Version version = Version.readVersion(input); input.setVersion(version); - Authentication authentication = new Authentication(input); - ctx.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication); - return authentication; + return new Authentication(input); } /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java index 4cdd4bb0e3575..6c1f9b8bb1941 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java @@ -8,6 +8,8 @@ import org.elasticsearch.common.CheckedSupplier; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.watcher.actions.ActionWrapperResult; import org.elasticsearch.xpack.core.watcher.condition.Condition; import org.elasticsearch.xpack.core.watcher.history.WatchRecord; @@ -43,6 +45,7 @@ public abstract class WatchExecutionContext { private Transform.Result transformResult; private ConcurrentMap actionsResults = ConcurrentCollections.newConcurrentMap(); private String nodeId; + private String executedBy; public WatchExecutionContext(String watchId, DateTime executionTime, TriggerEvent triggerEvent, TimeValue defaultThrottlePeriod) { this.id = new Wid(watchId, executionTime); @@ -85,6 +88,14 @@ public Watch watch() { public void ensureWatchExists(CheckedSupplier supplier) throws Exception { if (watch == null) { watch = supplier.get(); + // now that the watch exists, extract out the authentication + if (watch.status() != null && watch.status().getHeaders() != null) { + String header = watch.status().getHeaders().get(AuthenticationField.AUTHENTICATION_KEY); + if (header != null) { + Authentication auth = Authentication.decode(header); + executedBy = auth.getUser().principal(); + } + } } } @@ -137,6 +148,11 @@ public String getNodeId() { return nodeId; } + /** + * @return The user that executes the watch, which will be stored in the watch history + */ + public String getExecutedBy() { return executedBy; } + public void start() { assert phase == ExecutionPhase.AWAITS_EXECUTION; relativeStartTime = System.nanoTime(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java index 74e7b2115faa9..4042d2be08e30 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java @@ -43,12 +43,14 @@ public abstract class WatchRecord implements ToXContentObject { private static final ParseField METADATA = new ParseField("metadata"); private static final ParseField EXECUTION_RESULT = new ParseField("result"); private static final ParseField EXCEPTION = new ParseField("exception"); + private static final ParseField EXECUTED_BY = new ParseField("executed_by"); protected final Wid id; protected final Watch watch; private final String nodeId; protected final TriggerEvent triggerEvent; protected final ExecutionState state; + private final String executedBy; // only emitted to xcontent in "debug" mode protected final Map vars; @@ -60,7 +62,7 @@ public abstract class WatchRecord implements ToXContentObject { private WatchRecord(Wid id, TriggerEvent triggerEvent, ExecutionState state, Map vars, ExecutableInput input, ExecutableCondition condition, Map metadata, Watch watch, WatchExecutionResult executionResult, - String nodeId) { + String nodeId, String executedBy) { this.id = id; this.triggerEvent = triggerEvent; this.state = state; @@ -71,15 +73,16 @@ private WatchRecord(Wid id, TriggerEvent triggerEvent, ExecutionState state, Map this.executionResult = executionResult; this.watch = watch; this.nodeId = nodeId; + this.executedBy = executedBy; } private WatchRecord(Wid id, TriggerEvent triggerEvent, ExecutionState state, String nodeId) { - this(id, triggerEvent, state, Collections.emptyMap(), null, null, null, null, null, nodeId); + this(id, triggerEvent, state, Collections.emptyMap(), null, null, null, null, null, nodeId, null); } private WatchRecord(WatchRecord record, ExecutionState state) { this(record.id, record.triggerEvent, state, record.vars, record.input, record.condition, record.metadata, record.watch, - record.executionResult, record.nodeId); + record.executionResult, record.nodeId, record.executedBy); } private WatchRecord(WatchExecutionContext context, ExecutionState state) { @@ -88,12 +91,13 @@ private WatchRecord(WatchExecutionContext context, ExecutionState state) { context.watch() != null ? context.watch().condition() : null, context.watch() != null ? context.watch().metadata() : null, context.watch(), - null, context.getNodeId()); + null, context.getNodeId(), context.getExecutedBy()); } private WatchRecord(WatchExecutionContext context, WatchExecutionResult executionResult) { this(context.id(), context.triggerEvent(), getState(executionResult), context.vars(), context.watch().input(), - context.watch().condition(), context.watch().metadata(), context.watch(), executionResult, context.getNodeId()); + context.watch().condition(), context.watch().metadata(), context.watch(), executionResult, context.getNodeId(), + context.getExecutedBy()); } public static ExecutionState getState(WatchExecutionResult executionResult) { @@ -179,6 +183,9 @@ public final XContentBuilder toXContent(XContentBuilder builder, Params params) if (executionResult != null) { builder.field(EXECUTION_RESULT.getPreferredName(), executionResult, params); } + if (executedBy != null) { + builder.field(EXECUTED_BY.getPreferredName(), executedBy); + } innerToXContent(builder, params); builder.endObject(); return builder; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherIndexTemplateRegistryField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherIndexTemplateRegistryField.java index 25e2c928d9a57..1343b15161af8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherIndexTemplateRegistryField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherIndexTemplateRegistryField.java @@ -13,8 +13,9 @@ public final class WatcherIndexTemplateRegistryField { // version 6: upgrade to ES 6, removal of _status field // version 7: add full exception stack traces for better debugging // version 8: fix slack attachment property not to be dynamic, causing field type issues + // version 9: add a executed_by field defining which user executed the watch // Note: if you change this, also inform the kibana team around the watcher-ui - public static final String INDEX_TEMPLATE_VERSION = "8"; + public static final String INDEX_TEMPLATE_VERSION = "9"; public static final String HISTORY_TEMPLATE_NAME = ".watch-history-" + INDEX_TEMPLATE_VERSION; public static final String TRIGGERED_TEMPLATE_NAME = ".triggered_watches"; public static final String WATCHES_TEMPLATE_NAME = ".watches"; diff --git a/x-pack/plugin/core/src/main/resources/watch-history.json b/x-pack/plugin/core/src/main/resources/watch-history.json index 86a967fc14fe5..f33ecd973086e 100644 --- a/x-pack/plugin/core/src/main/resources/watch-history.json +++ b/x-pack/plugin/core/src/main/resources/watch-history.json @@ -120,6 +120,9 @@ "messages": { "type": "text" }, + "executed_by": { + "type": "text" + }, "exception" : { "type" : "object", "enabled" : false diff --git a/x-pack/qa/smoke-test-watcher-with-security/roles.yml b/x-pack/qa/smoke-test-watcher-with-security/roles.yml index bebfa883fcb15..b52fe6c5c5914 100644 --- a/x-pack/qa/smoke-test-watcher-with-security/roles.yml +++ b/x-pack/qa/smoke-test-watcher-with-security/roles.yml @@ -21,6 +21,7 @@ watcher_manager: run_as: - powerless_user - watcher_manager + - x_pack_rest_user watcher_monitor: cluster: diff --git a/x-pack/qa/smoke-test-watcher-with-security/src/test/resources/rest-api-spec/test/watcher/watcher_and_security/20_test_run_as_execute_watch.yml b/x-pack/qa/smoke-test-watcher-with-security/src/test/resources/rest-api-spec/test/watcher/watcher_and_security/20_test_run_as_execute_watch.yml index 9bc7724b2c0f4..cd44b4201fd43 100644 --- a/x-pack/qa/smoke-test-watcher-with-security/src/test/resources/rest-api-spec/test/watcher/watcher_and_security/20_test_run_as_execute_watch.yml +++ b/x-pack/qa/smoke-test-watcher-with-security/src/test/resources/rest-api-spec/test/watcher/watcher_and_security/20_test_run_as_execute_watch.yml @@ -74,10 +74,63 @@ teardown: id: "my_watch" - match: { watch_record.watch_id: "my_watch" } - match: { watch_record.state: "executed" } + - match: { watch_record.executed_by: "watcher_manager" } +--- +"Test watch is runas user properly recorded": + - do: + xpack.watcher.put_watch: + id: "my_watch" + body: > + { + "trigger": { + "schedule" : { "cron" : "0 0 0 1 * ? 2099" } + }, + "input": { + "search" : { + "request" : { + "indices" : [ "my_test_index" ], + "body" :{ + "query" : { "match_all": {} } + } + } + } + }, + "condition" : { + "compare" : { + "ctx.payload.hits.total" : { + "gte" : 1 + } + } + }, + "actions": { + "logging": { + "logging": { + "text": "Successfully ran my_watch to test for search input" + } + } + } + } + - match: { _id: "my_watch" } + + - do: + xpack.watcher.get_watch: + id: "my_watch" + - match: { _id: "my_watch" } + - is_false: watch.status.headers + + - do: + headers: { es-security-runas-user: x_pack_rest_user } + xpack.watcher.execute_watch: + id: "my_watch" + - match: { watch_record.watch_id: "my_watch" } + - match: { watch_record.state: "executed" } + - match: { watch_record.executed_by: "x_pack_rest_user" } + + --- "Test watch search input does not work against index user is not allowed to read": @@ -130,6 +183,7 @@ teardown: - match: { watch_record.watch_id: "my_watch" } # because we are not allowed to read the index, there wont be any data - match: { watch_record.state: "execution_not_needed" } + - match: { watch_record.executed_by: "watcher_manager" } --- @@ -272,6 +326,7 @@ teardown: id: "my_watch" - match: { watch_record.watch_id: "my_watch" } - match: { watch_record.state: "executed" } + - match: { watch_record.executed_by: "watcher_manager" } - do: get: @@ -320,6 +375,7 @@ teardown: id: "my_watch" - match: { watch_record.watch_id: "my_watch" } - match: { watch_record.state: "executed" } + - match: { watch_record.executed_by: "watcher_manager" } - do: get: From 45ad8b74e03aa5859fc964aa7d2a46726395ec3d Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Mon, 9 Jul 2018 11:51:56 -0500 Subject: [PATCH 2/7] Update executed_by to user --- .../execution/WatchExecutionContext.java | 6 +++--- .../core/watcher/history/WatchRecord.java | 18 +++++++++--------- .../WatcherIndexTemplateRegistryField.java | 2 +- .../core/src/main/resources/watch-history.json | 2 +- .../20_test_run_as_execute_watch.yml | 10 +++++----- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java index 6c1f9b8bb1941..c2c0877af23de 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java @@ -45,7 +45,7 @@ public abstract class WatchExecutionContext { private Transform.Result transformResult; private ConcurrentMap actionsResults = ConcurrentCollections.newConcurrentMap(); private String nodeId; - private String executedBy; + private String user; public WatchExecutionContext(String watchId, DateTime executionTime, TriggerEvent triggerEvent, TimeValue defaultThrottlePeriod) { this.id = new Wid(watchId, executionTime); @@ -93,7 +93,7 @@ public void ensureWatchExists(CheckedSupplier supplier) throws String header = watch.status().getHeaders().get(AuthenticationField.AUTHENTICATION_KEY); if (header != null) { Authentication auth = Authentication.decode(header); - executedBy = auth.getUser().principal(); + user = auth.getUser().principal(); } } } @@ -151,7 +151,7 @@ public String getNodeId() { /** * @return The user that executes the watch, which will be stored in the watch history */ - public String getExecutedBy() { return executedBy; } + public String getUser() { return user; } public void start() { assert phase == ExecutionPhase.AWAITS_EXECUTION; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java index 4042d2be08e30..c8d6607489869 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java @@ -43,14 +43,14 @@ public abstract class WatchRecord implements ToXContentObject { private static final ParseField METADATA = new ParseField("metadata"); private static final ParseField EXECUTION_RESULT = new ParseField("result"); private static final ParseField EXCEPTION = new ParseField("exception"); - private static final ParseField EXECUTED_BY = new ParseField("executed_by"); + private static final ParseField USER = new ParseField("user"); protected final Wid id; protected final Watch watch; private final String nodeId; protected final TriggerEvent triggerEvent; protected final ExecutionState state; - private final String executedBy; + private final String user; // only emitted to xcontent in "debug" mode protected final Map vars; @@ -62,7 +62,7 @@ public abstract class WatchRecord implements ToXContentObject { private WatchRecord(Wid id, TriggerEvent triggerEvent, ExecutionState state, Map vars, ExecutableInput input, ExecutableCondition condition, Map metadata, Watch watch, WatchExecutionResult executionResult, - String nodeId, String executedBy) { + String nodeId, String user) { this.id = id; this.triggerEvent = triggerEvent; this.state = state; @@ -73,7 +73,7 @@ private WatchRecord(Wid id, TriggerEvent triggerEvent, ExecutionState state, Map this.executionResult = executionResult; this.watch = watch; this.nodeId = nodeId; - this.executedBy = executedBy; + this.user = user; } private WatchRecord(Wid id, TriggerEvent triggerEvent, ExecutionState state, String nodeId) { @@ -82,7 +82,7 @@ private WatchRecord(Wid id, TriggerEvent triggerEvent, ExecutionState state, Str private WatchRecord(WatchRecord record, ExecutionState state) { this(record.id, record.triggerEvent, state, record.vars, record.input, record.condition, record.metadata, record.watch, - record.executionResult, record.nodeId, record.executedBy); + record.executionResult, record.nodeId, record.user); } private WatchRecord(WatchExecutionContext context, ExecutionState state) { @@ -91,13 +91,13 @@ private WatchRecord(WatchExecutionContext context, ExecutionState state) { context.watch() != null ? context.watch().condition() : null, context.watch() != null ? context.watch().metadata() : null, context.watch(), - null, context.getNodeId(), context.getExecutedBy()); + null, context.getNodeId(), context.getUser()); } private WatchRecord(WatchExecutionContext context, WatchExecutionResult executionResult) { this(context.id(), context.triggerEvent(), getState(executionResult), context.vars(), context.watch().input(), context.watch().condition(), context.watch().metadata(), context.watch(), executionResult, context.getNodeId(), - context.getExecutedBy()); + context.getUser()); } public static ExecutionState getState(WatchExecutionResult executionResult) { @@ -183,8 +183,8 @@ public final XContentBuilder toXContent(XContentBuilder builder, Params params) if (executionResult != null) { builder.field(EXECUTION_RESULT.getPreferredName(), executionResult, params); } - if (executedBy != null) { - builder.field(EXECUTED_BY.getPreferredName(), executedBy); + if (user != null) { + builder.field(USER.getPreferredName(), user); } innerToXContent(builder, params); builder.endObject(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherIndexTemplateRegistryField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherIndexTemplateRegistryField.java index 1343b15161af8..b42506b81b3d4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherIndexTemplateRegistryField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherIndexTemplateRegistryField.java @@ -13,7 +13,7 @@ public final class WatcherIndexTemplateRegistryField { // version 6: upgrade to ES 6, removal of _status field // version 7: add full exception stack traces for better debugging // version 8: fix slack attachment property not to be dynamic, causing field type issues - // version 9: add a executed_by field defining which user executed the watch + // version 9: add a user field defining which user executed the watch // Note: if you change this, also inform the kibana team around the watcher-ui public static final String INDEX_TEMPLATE_VERSION = "9"; public static final String HISTORY_TEMPLATE_NAME = ".watch-history-" + INDEX_TEMPLATE_VERSION; diff --git a/x-pack/plugin/core/src/main/resources/watch-history.json b/x-pack/plugin/core/src/main/resources/watch-history.json index f33ecd973086e..9a4a96409b043 100644 --- a/x-pack/plugin/core/src/main/resources/watch-history.json +++ b/x-pack/plugin/core/src/main/resources/watch-history.json @@ -120,7 +120,7 @@ "messages": { "type": "text" }, - "executed_by": { + "user": { "type": "text" }, "exception" : { diff --git a/x-pack/qa/smoke-test-watcher-with-security/src/test/resources/rest-api-spec/test/watcher/watcher_and_security/20_test_run_as_execute_watch.yml b/x-pack/qa/smoke-test-watcher-with-security/src/test/resources/rest-api-spec/test/watcher/watcher_and_security/20_test_run_as_execute_watch.yml index cd44b4201fd43..7a0634f5187b1 100644 --- a/x-pack/qa/smoke-test-watcher-with-security/src/test/resources/rest-api-spec/test/watcher/watcher_and_security/20_test_run_as_execute_watch.yml +++ b/x-pack/qa/smoke-test-watcher-with-security/src/test/resources/rest-api-spec/test/watcher/watcher_and_security/20_test_run_as_execute_watch.yml @@ -74,7 +74,7 @@ teardown: id: "my_watch" - match: { watch_record.watch_id: "my_watch" } - match: { watch_record.state: "executed" } - - match: { watch_record.executed_by: "watcher_manager" } + - match: { watch_record.user: "watcher_manager" } @@ -128,7 +128,7 @@ teardown: id: "my_watch" - match: { watch_record.watch_id: "my_watch" } - match: { watch_record.state: "executed" } - - match: { watch_record.executed_by: "x_pack_rest_user" } + - match: { watch_record.user: "x_pack_rest_user" } --- @@ -183,7 +183,7 @@ teardown: - match: { watch_record.watch_id: "my_watch" } # because we are not allowed to read the index, there wont be any data - match: { watch_record.state: "execution_not_needed" } - - match: { watch_record.executed_by: "watcher_manager" } + - match: { watch_record.user: "watcher_manager" } --- @@ -326,7 +326,7 @@ teardown: id: "my_watch" - match: { watch_record.watch_id: "my_watch" } - match: { watch_record.state: "executed" } - - match: { watch_record.executed_by: "watcher_manager" } + - match: { watch_record.user: "watcher_manager" } - do: get: @@ -375,7 +375,7 @@ teardown: id: "my_watch" - match: { watch_record.watch_id: "my_watch" } - match: { watch_record.state: "executed" } - - match: { watch_record.executed_by: "watcher_manager" } + - match: { watch_record.user: "watcher_manager" } - do: get: From db6d20706018a2a870a18ea1469988256bb75e46 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Tue, 10 Jul 2018 13:18:38 -0500 Subject: [PATCH 3/7] Move method and create test for the context --- .../execution/WatchExecutionContext.java | 25 ++++++++----- .../execution/ExecutionServiceTests.java | 35 +++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java index c2c0877af23de..304f7d6d7c39a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.core.watcher.watch.Watch; import org.joda.time.DateTime; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -88,14 +89,7 @@ public Watch watch() { public void ensureWatchExists(CheckedSupplier supplier) throws Exception { if (watch == null) { watch = supplier.get(); - // now that the watch exists, extract out the authentication - if (watch.status() != null && watch.status().getHeaders() != null) { - String header = watch.status().getHeaders().get(AuthenticationField.AUTHENTICATION_KEY); - if (header != null) { - Authentication auth = Authentication.decode(header); - user = auth.getUser().principal(); - } - } + user = WatchExecutionContext.getUsernameFromWatch(watch); } } @@ -259,4 +253,19 @@ public WatchRecord finish() { public WatchExecutionSnapshot createSnapshot(Thread executionThread) { return new WatchExecutionSnapshot(this, executionThread.getStackTrace()); } + + /** + * Given a watch, this extracts and decodes the relevant auth header and returns the principal of the user that is + * executing the watch. + */ + public static String getUsernameFromWatch(Watch watch) throws IOException { + if (watch != null && watch.status() != null && watch.status().getHeaders() != null) { + String header = watch.status().getHeaders().get(AuthenticationField.AUTHENTICATION_KEY); + if (header != null) { + Authentication auth = Authentication.decode(header); + return auth.getUser().principal(); + } + } + return null; + } } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java index 73f0e82072055..3a881320fe77f 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java @@ -31,6 +31,9 @@ import org.elasticsearch.index.get.GetResult; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.watcher.actions.Action; import org.elasticsearch.xpack.core.watcher.actions.ActionStatus; import org.elasticsearch.xpack.core.watcher.actions.ActionWrapper; @@ -76,6 +79,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -85,6 +89,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonMap; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; @@ -1072,6 +1077,36 @@ public void testManualWatchExecutionContextGetsAlwaysExecuted() throws Exception assertThat(watchRecord.state(), is(ExecutionState.EXECUTED)); } + public void testLoadingWatchExecutionUser() throws Exception { + DateTime now = now(UTC); + Watch watch = mock(Watch.class); + WatchStatus status = mock(WatchStatus.class); + ScheduleTriggerEvent event = new ScheduleTriggerEvent("_id", now, now); + Authentication authentication = new Authentication(new User("joe", "admin"), + new Authentication.RealmRef("native_realm", "native", "node1"), null); + String encoded = authentication.encode(); + + // Should be null + TriggeredExecutionContext context = new TriggeredExecutionContext(watch.id(), now, event, timeValueSeconds(5)); + context.ensureWatchExists(() -> watch); + assertNull(context.getUser()); + + // Should still be null, header is not yet set + when(watch.status()).thenReturn(status); + context = new TriggeredExecutionContext(watch.id(), now, event, timeValueSeconds(5)); + context.ensureWatchExists(() -> watch); + assertNull(context.getUser()); + + Map headers = new HashMap<>(); + headers.put(AuthenticationField.AUTHENTICATION_KEY, encoded); + + // Should no longer be null now that the proper header is set + when(status.getHeaders()).thenReturn(headers); + context = new TriggeredExecutionContext(watch.id(), now, event, timeValueSeconds(5)); + context.ensureWatchExists(() -> watch); + assertThat(context.getUser(), equalTo("joe")); + } + private WatchExecutionContext createMockWatchExecutionContext(String watchId, DateTime executionTime) { WatchExecutionContext ctx = mock(WatchExecutionContext.class); when(ctx.id()).thenReturn(new Wid(watchId, executionTime)); From dcd734dfe27a4e87eaf6613036594d82e4a80db5 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Wed, 11 Jul 2018 10:52:51 -0500 Subject: [PATCH 4/7] Cleanup test --- .../xpack/watcher/execution/ExecutionServiceTests.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java index 3a881320fe77f..d3f46d3d452f7 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java @@ -79,7 +79,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -1082,9 +1081,6 @@ public void testLoadingWatchExecutionUser() throws Exception { Watch watch = mock(Watch.class); WatchStatus status = mock(WatchStatus.class); ScheduleTriggerEvent event = new ScheduleTriggerEvent("_id", now, now); - Authentication authentication = new Authentication(new User("joe", "admin"), - new Authentication.RealmRef("native_realm", "native", "node1"), null); - String encoded = authentication.encode(); // Should be null TriggeredExecutionContext context = new TriggeredExecutionContext(watch.id(), now, event, timeValueSeconds(5)); @@ -1097,11 +1093,11 @@ public void testLoadingWatchExecutionUser() throws Exception { context.ensureWatchExists(() -> watch); assertNull(context.getUser()); - Map headers = new HashMap<>(); - headers.put(AuthenticationField.AUTHENTICATION_KEY, encoded); + Authentication authentication = new Authentication(new User("joe", "admin"), + new Authentication.RealmRef("native_realm", "native", "node1"), null); // Should no longer be null now that the proper header is set - when(status.getHeaders()).thenReturn(headers); + when(status.getHeaders()).thenReturn(Collections.singletonMap(AuthenticationField.AUTHENTICATION_KEY, authentication.encode())); context = new TriggeredExecutionContext(watch.id(), now, event, timeValueSeconds(5)); context.ensureWatchExists(() -> watch); assertThat(context.getUser(), equalTo("joe")); From 5a467d06748c5b20c01c7044d9e3ffd0e4755c22 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Wed, 11 Jul 2018 11:47:52 -0500 Subject: [PATCH 5/7] Fix docs test --- x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc b/x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc index 91cd89bca6d41..97cc355e10975 100644 --- a/x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc +++ b/x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc @@ -263,7 +263,8 @@ This is an example of the output: "type": "index" } ] - } + }, + "user": "test_admin" <4> } } -------------------------------------------------- @@ -281,6 +282,7 @@ This is an example of the output: <1> The id of the watch record as it would be stored in the `.watcher-history` index. <2> The watch record document as it would be stored in the `.watcher-history` index. <3> The watch execution results. +<4> The user that executed the watch. You can set a different execution mode for every action by associating the mode name with the action id: From a1ea27d518725c595ce5565e1a64e18c499465e1 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Wed, 11 Jul 2018 11:59:42 -0500 Subject: [PATCH 6/7] Cleanup wording --- x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc b/x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc index 97cc355e10975..ec2c60c543bab 100644 --- a/x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc +++ b/x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc @@ -282,7 +282,7 @@ This is an example of the output: <1> The id of the watch record as it would be stored in the `.watcher-history` index. <2> The watch record document as it would be stored in the `.watcher-history` index. <3> The watch execution results. -<4> The user that executed the watch. +<4> The user used to execute the watch. You can set a different execution mode for every action by associating the mode name with the action id: From 0dc862fe6c054924c5767bb02ec24f2c2145b005 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Fri, 13 Jul 2018 11:54:20 -0500 Subject: [PATCH 7/7] Move username --- .../xpack/core/watcher/history/WatchRecord.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java index c8d6607489869..2b28c2f15c9c7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java @@ -156,6 +156,9 @@ public final XContentBuilder toXContent(XContentBuilder builder, Params params) builder.field(NODE.getPreferredName(), nodeId); builder.field(STATE.getPreferredName(), state.id()); + if (user != null) { + builder.field(USER.getPreferredName(), user); + } if (watch != null && watch.status() != null) { builder.field(STATUS.getPreferredName(), watch.status(), params); } @@ -183,9 +186,6 @@ public final XContentBuilder toXContent(XContentBuilder builder, Params params) if (executionResult != null) { builder.field(EXECUTION_RESULT.getPreferredName(), executionResult, params); } - if (user != null) { - builder.field(USER.getPreferredName(), user); - } innerToXContent(builder, params); builder.endObject(); return builder;