Skip to content

Commit b982e1a

Browse files
committed
Watcher: Store username on watch execution (#31873)
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 "user". Closes #31772
1 parent 5942ae7 commit b982e1a

File tree

10 files changed

+142
-10
lines changed

10 files changed

+142
-10
lines changed

x-pack/docs/en/rest-api/watcher/execute-watch.asciidoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@ This is an example of the output:
263263
"type": "index"
264264
}
265265
]
266-
}
266+
},
267+
"user": "test_admin" <4>
267268
}
268269
}
269270
--------------------------------------------------
@@ -281,6 +282,7 @@ This is an example of the output:
281282
<1> The id of the watch record as it would be stored in the `.watcher-history` index.
282283
<2> The watch record document as it would be stored in the `.watcher-history` index.
283284
<3> The watch execution results.
285+
<4> The user used to execute the watch.
284286

285287
You can set a different execution mode for every action by associating the mode
286288
name with the action id:

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,17 @@ static Authentication deserializeHeaderAndPutInContext(String header, ThreadCont
8888
throws IOException, IllegalArgumentException {
8989
assert ctx.getTransient(AuthenticationField.AUTHENTICATION_KEY) == null;
9090

91+
Authentication authentication = decode(header);
92+
ctx.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
93+
return authentication;
94+
}
95+
96+
public static Authentication decode(String header) throws IOException {
9197
byte[] bytes = Base64.getDecoder().decode(header);
9298
StreamInput input = StreamInput.wrap(bytes);
9399
Version version = Version.readVersion(input);
94100
input.setVersion(version);
95-
Authentication authentication = new Authentication(input);
96-
ctx.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
97-
return authentication;
101+
return new Authentication(input);
98102
}
99103

100104
/**

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import org.elasticsearch.common.CheckedSupplier;
99
import org.elasticsearch.common.unit.TimeValue;
1010
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
11+
import org.elasticsearch.xpack.core.security.authc.Authentication;
12+
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
1113
import org.elasticsearch.xpack.core.watcher.actions.ActionWrapperResult;
1214
import org.elasticsearch.xpack.core.watcher.condition.Condition;
1315
import org.elasticsearch.xpack.core.watcher.history.WatchRecord;
@@ -18,6 +20,7 @@
1820
import org.elasticsearch.xpack.core.watcher.watch.Watch;
1921
import org.joda.time.DateTime;
2022

23+
import java.io.IOException;
2124
import java.util.Collections;
2225
import java.util.HashMap;
2326
import java.util.Map;
@@ -42,6 +45,7 @@ public abstract class WatchExecutionContext {
4245
private Transform.Result transformResult;
4346
private ConcurrentMap<String, ActionWrapperResult> actionsResults = ConcurrentCollections.newConcurrentMap();
4447
private String nodeId;
48+
private String user;
4549

4650
public WatchExecutionContext(String watchId, DateTime executionTime, TriggerEvent triggerEvent, TimeValue defaultThrottlePeriod) {
4751
this.id = new Wid(watchId, executionTime);
@@ -84,6 +88,7 @@ public Watch watch() {
8488
public final void ensureWatchExists(CheckedSupplier<Watch, Exception> supplier) throws Exception {
8589
if (watch == null) {
8690
watch = supplier.get();
91+
user = WatchExecutionContext.getUsernameFromWatch(watch);
8792
}
8893
}
8994

@@ -136,6 +141,11 @@ public String getNodeId() {
136141
return nodeId;
137142
}
138143

144+
/**
145+
* @return The user that executes the watch, which will be stored in the watch history
146+
*/
147+
public String getUser() { return user; }
148+
139149
public void start() {
140150
assert phase == ExecutionPhase.AWAITS_EXECUTION;
141151
startTimestamp = System.currentTimeMillis();
@@ -242,4 +252,19 @@ public WatchRecord finish() {
242252
public WatchExecutionSnapshot createSnapshot(Thread executionThread) {
243253
return new WatchExecutionSnapshot(this, executionThread.getStackTrace());
244254
}
255+
256+
/**
257+
* Given a watch, this extracts and decodes the relevant auth header and returns the principal of the user that is
258+
* executing the watch.
259+
*/
260+
public static String getUsernameFromWatch(Watch watch) throws IOException {
261+
if (watch != null && watch.status() != null && watch.status().getHeaders() != null) {
262+
String header = watch.status().getHeaders().get(AuthenticationField.AUTHENTICATION_KEY);
263+
if (header != null) {
264+
Authentication auth = Authentication.decode(header);
265+
return auth.getUser().principal();
266+
}
267+
}
268+
return null;
269+
}
245270
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/history/WatchRecord.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ public abstract class WatchRecord implements ToXContentObject {
4343
private static final ParseField METADATA = new ParseField("metadata");
4444
private static final ParseField EXECUTION_RESULT = new ParseField("result");
4545
private static final ParseField EXCEPTION = new ParseField("exception");
46+
private static final ParseField USER = new ParseField("user");
4647

4748
protected final Wid id;
4849
protected final Watch watch;
4950
private final String nodeId;
5051
protected final TriggerEvent triggerEvent;
5152
protected final ExecutionState state;
53+
private final String user;
5254

5355
// only emitted to xcontent in "debug" mode
5456
protected final Map<String, Object> vars;
@@ -60,7 +62,7 @@ public abstract class WatchRecord implements ToXContentObject {
6062

6163
private WatchRecord(Wid id, TriggerEvent triggerEvent, ExecutionState state, Map<String, Object> vars, ExecutableInput input,
6264
ExecutableCondition condition, Map<String, Object> metadata, Watch watch, WatchExecutionResult executionResult,
63-
String nodeId) {
65+
String nodeId, String user) {
6466
this.id = id;
6567
this.triggerEvent = triggerEvent;
6668
this.state = state;
@@ -71,15 +73,16 @@ private WatchRecord(Wid id, TriggerEvent triggerEvent, ExecutionState state, Map
7173
this.executionResult = executionResult;
7274
this.watch = watch;
7375
this.nodeId = nodeId;
76+
this.user = user;
7477
}
7578

7679
private WatchRecord(Wid id, TriggerEvent triggerEvent, ExecutionState state, String nodeId) {
77-
this(id, triggerEvent, state, Collections.emptyMap(), null, null, null, null, null, nodeId);
80+
this(id, triggerEvent, state, Collections.emptyMap(), null, null, null, null, null, nodeId, null);
7881
}
7982

8083
private WatchRecord(WatchRecord record, ExecutionState state) {
8184
this(record.id, record.triggerEvent, state, record.vars, record.input, record.condition, record.metadata, record.watch,
82-
record.executionResult, record.nodeId);
85+
record.executionResult, record.nodeId, record.user);
8386
}
8487

8588
private WatchRecord(WatchExecutionContext context, ExecutionState state) {
@@ -88,12 +91,13 @@ private WatchRecord(WatchExecutionContext context, ExecutionState state) {
8891
context.watch() != null ? context.watch().condition() : null,
8992
context.watch() != null ? context.watch().metadata() : null,
9093
context.watch(),
91-
null, context.getNodeId());
94+
null, context.getNodeId(), context.getUser());
9295
}
9396

9497
private WatchRecord(WatchExecutionContext context, WatchExecutionResult executionResult) {
9598
this(context.id(), context.triggerEvent(), getState(executionResult), context.vars(), context.watch().input(),
96-
context.watch().condition(), context.watch().metadata(), context.watch(), executionResult, context.getNodeId());
99+
context.watch().condition(), context.watch().metadata(), context.watch(), executionResult, context.getNodeId(),
100+
context.getUser());
97101
}
98102

99103
public static ExecutionState getState(WatchExecutionResult executionResult) {
@@ -152,6 +156,9 @@ public final XContentBuilder toXContent(XContentBuilder builder, Params params)
152156
builder.field(NODE.getPreferredName(), nodeId);
153157
builder.field(STATE.getPreferredName(), state.id());
154158

159+
if (user != null) {
160+
builder.field(USER.getPreferredName(), user);
161+
}
155162
if (watch != null && watch.status() != null) {
156163
builder.field(STATUS.getPreferredName(), watch.status(), params);
157164
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherIndexTemplateRegistryField.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ public final class WatcherIndexTemplateRegistryField {
1313
// version 6: upgrade to ES 6, removal of _status field
1414
// version 7: add full exception stack traces for better debugging
1515
// version 8: fix slack attachment property not to be dynamic, causing field type issues
16+
// version 9: add a user field defining which user executed the watch
1617
// Note: if you change this, also inform the kibana team around the watcher-ui
17-
public static final String INDEX_TEMPLATE_VERSION = "8";
18+
public static final String INDEX_TEMPLATE_VERSION = "9";
1819
public static final String HISTORY_TEMPLATE_NAME = ".watch-history-" + INDEX_TEMPLATE_VERSION;
1920
public static final String TRIGGERED_TEMPLATE_NAME = ".triggered_watches";
2021
public static final String WATCHES_TEMPLATE_NAME = ".watches";

x-pack/plugin/core/src/main/resources/watch-history.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@
120120
"messages": {
121121
"type": "text"
122122
},
123+
"user": {
124+
"type": "text"
125+
},
123126
"exception" : {
124127
"type" : "object",
125128
"enabled" : false

x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
import org.elasticsearch.index.get.GetResult;
3232
import org.elasticsearch.test.ESTestCase;
3333
import org.elasticsearch.threadpool.ThreadPool;
34+
import org.elasticsearch.xpack.core.security.authc.Authentication;
35+
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
36+
import org.elasticsearch.xpack.core.security.user.User;
3437
import org.elasticsearch.xpack.core.watcher.actions.Action;
3538
import org.elasticsearch.xpack.core.watcher.actions.ActionStatus;
3639
import org.elasticsearch.xpack.core.watcher.actions.ActionWrapper;
@@ -85,6 +88,7 @@
8588
import static java.util.Arrays.asList;
8689
import static java.util.Collections.singletonMap;
8790
import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds;
91+
import static org.hamcrest.Matchers.equalTo;
8892
import static org.hamcrest.Matchers.hasSize;
8993
import static org.hamcrest.Matchers.instanceOf;
9094
import static org.hamcrest.Matchers.is;
@@ -1061,6 +1065,33 @@ public void testManualWatchExecutionContextGetsAlwaysExecuted() throws Exception
10611065
assertThat(watchRecord.state(), is(ExecutionState.EXECUTED));
10621066
}
10631067

1068+
public void testLoadingWatchExecutionUser() throws Exception {
1069+
DateTime now = now(UTC);
1070+
Watch watch = mock(Watch.class);
1071+
WatchStatus status = mock(WatchStatus.class);
1072+
ScheduleTriggerEvent event = new ScheduleTriggerEvent("_id", now, now);
1073+
1074+
// Should be null
1075+
TriggeredExecutionContext context = new TriggeredExecutionContext(watch.id(), now, event, timeValueSeconds(5));
1076+
context.ensureWatchExists(() -> watch);
1077+
assertNull(context.getUser());
1078+
1079+
// Should still be null, header is not yet set
1080+
when(watch.status()).thenReturn(status);
1081+
context = new TriggeredExecutionContext(watch.id(), now, event, timeValueSeconds(5));
1082+
context.ensureWatchExists(() -> watch);
1083+
assertNull(context.getUser());
1084+
1085+
Authentication authentication = new Authentication(new User("joe", "admin"),
1086+
new Authentication.RealmRef("native_realm", "native", "node1"), null);
1087+
1088+
// Should no longer be null now that the proper header is set
1089+
when(status.getHeaders()).thenReturn(Collections.singletonMap(AuthenticationField.AUTHENTICATION_KEY, authentication.encode()));
1090+
context = new TriggeredExecutionContext(watch.id(), now, event, timeValueSeconds(5));
1091+
context.ensureWatchExists(() -> watch);
1092+
assertThat(context.getUser(), equalTo("joe"));
1093+
}
1094+
10641095
private WatchExecutionContext createMockWatchExecutionContext(String watchId, DateTime executionTime) {
10651096
WatchExecutionContext ctx = mock(WatchExecutionContext.class);
10661097
when(ctx.id()).thenReturn(new Wid(watchId, executionTime));

x-pack/qa/smoke-test-watcher-with-security/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ integTestCluster {
2626
extraConfigFile 'roles.yml', 'roles.yml'
2727
setupCommand 'setupTestAdminUser',
2828
'bin/elasticsearch-users', 'useradd', 'test_admin', '-p', 'x-pack-test-password', '-r', 'superuser'
29+
setupCommand 'setupXpackUserForTests',
30+
'bin/elasticsearch-users', 'useradd', 'x_pack_rest_user', '-p', 'x-pack-test-password', '-r', 'watcher_manager'
2931
setupCommand 'setupWatcherManagerUser',
3032
'bin/elasticsearch-users', 'useradd', 'watcher_manager', '-p', 'x-pack-test-password', '-r', 'watcher_manager'
3133
setupCommand 'setupPowerlessUser',

x-pack/qa/smoke-test-watcher-with-security/roles.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ watcher_manager:
2121
run_as:
2222
- powerless_user
2323
- watcher_manager
24+
- x_pack_rest_user
2425

2526
watcher_monitor:
2627
cluster:

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,63 @@ teardown:
7474
id: "my_watch"
7575
- match: { watch_record.watch_id: "my_watch" }
7676
- match: { watch_record.state: "executed" }
77+
- match: { watch_record.user: "watcher_manager" }
7778

7879

7980

8081

82+
---
83+
"Test watch is runas user properly recorded":
84+
- do:
85+
xpack.watcher.put_watch:
86+
id: "my_watch"
87+
body: >
88+
{
89+
"trigger": {
90+
"schedule" : { "cron" : "0 0 0 1 * ? 2099" }
91+
},
92+
"input": {
93+
"search" : {
94+
"request" : {
95+
"indices" : [ "my_test_index" ],
96+
"body" :{
97+
"query" : { "match_all": {} }
98+
}
99+
}
100+
}
101+
},
102+
"condition" : {
103+
"compare" : {
104+
"ctx.payload.hits.total" : {
105+
"gte" : 1
106+
}
107+
}
108+
},
109+
"actions": {
110+
"logging": {
111+
"logging": {
112+
"text": "Successfully ran my_watch to test for search input"
113+
}
114+
}
115+
}
116+
}
117+
- match: { _id: "my_watch" }
118+
119+
- do:
120+
xpack.watcher.get_watch:
121+
id: "my_watch"
122+
- match: { _id: "my_watch" }
123+
- is_false: watch.status.headers
124+
125+
- do:
126+
headers: { es-security-runas-user: x_pack_rest_user }
127+
xpack.watcher.execute_watch:
128+
id: "my_watch"
129+
- match: { watch_record.watch_id: "my_watch" }
130+
- match: { watch_record.state: "executed" }
131+
- match: { watch_record.user: "x_pack_rest_user" }
132+
133+
81134
---
82135
"Test watch search input does not work against index user is not allowed to read":
83136

@@ -130,6 +183,7 @@ teardown:
130183
- match: { watch_record.watch_id: "my_watch" }
131184
# because we are not allowed to read the index, there wont be any data
132185
- match: { watch_record.state: "execution_not_needed" }
186+
- match: { watch_record.user: "watcher_manager" }
133187

134188

135189
---
@@ -272,6 +326,7 @@ teardown:
272326
id: "my_watch"
273327
- match: { watch_record.watch_id: "my_watch" }
274328
- match: { watch_record.state: "executed" }
329+
- match: { watch_record.user: "watcher_manager" }
275330

276331
- do:
277332
get:
@@ -320,6 +375,7 @@ teardown:
320375
id: "my_watch"
321376
- match: { watch_record.watch_id: "my_watch" }
322377
- match: { watch_record.state: "executed" }
378+
- match: { watch_record.user: "watcher_manager" }
323379

324380
- do:
325381
get:

0 commit comments

Comments
 (0)