From 2861f8ce218216acd75e66f7d04252e35901f828 Mon Sep 17 00:00:00 2001
From: Martijn van Groningen <martijn.v.groningen@gmail.com>
Date: Tue, 19 May 2015 17:50:53 +0200
Subject: [PATCH] test: Add more Shield related tests for testing the Watcher
 roles.

Original commit: elastic/x-pack-elasticsearch@482d8fe65c2ba97ae703d069e5a064a68292fbe2
---
 .../test/AbstractWatcherIntegrationTests.java |  45 +++--
 .../test/integration/BasicShieldTests.java    | 176 ++++++++++++++++++
 2 files changed, 208 insertions(+), 13 deletions(-)
 create mode 100644 src/test/java/org/elasticsearch/watcher/test/integration/BasicShieldTests.java

diff --git a/src/test/java/org/elasticsearch/watcher/test/AbstractWatcherIntegrationTests.java b/src/test/java/org/elasticsearch/watcher/test/AbstractWatcherIntegrationTests.java
index bb43b73cf88..689c8bf2011 100644
--- a/src/test/java/org/elasticsearch/watcher/test/AbstractWatcherIntegrationTests.java
+++ b/src/test/java/org/elasticsearch/watcher/test/AbstractWatcherIntegrationTests.java
@@ -35,6 +35,7 @@ import org.elasticsearch.test.ElasticsearchIntegrationTest;
 import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
 import org.elasticsearch.test.InternalTestCluster;
 import org.elasticsearch.test.TestCluster;
+import org.elasticsearch.watcher.WatcherLifeCycleService;
 import org.elasticsearch.watcher.WatcherPlugin;
 import org.elasticsearch.watcher.WatcherService;
 import org.elasticsearch.watcher.WatcherState;
@@ -155,7 +156,7 @@ public abstract class AbstractWatcherIntegrationTests extends ElasticsearchInteg
         if (checkWatcherRunningOnlyOnce()) {
             ensureWatcherOnlyRunningOnce();
         }
-        stopWatcher();
+        stopWatcher(false);
     }
 
     @AfterClass
@@ -189,15 +190,15 @@ public abstract class AbstractWatcherIntegrationTests extends ElasticsearchInteg
     private void startWatcherIfNodesExist() throws Exception {
         if (internalTestCluster().size() > 0) {
             ensureLicenseEnabled();
-            WatcherStatsResponse response = watcherClient().prepareWatcherStats().get();
-            if (response.getWatcherState() == WatcherState.STOPPED) {
+            WatcherState state = getInstanceFromMaster(WatcherService.class).state();
+            if (state == WatcherState.STOPPED) {
                 logger.info("[{}#{}]: starting watcher", getTestClass().getSimpleName(), getTestName());
-                startWatcher();
-            } else if (response.getWatcherState() == WatcherState.STARTING) {
+                startWatcher(false);
+            } else if (state == WatcherState.STARTING) {
                 logger.info("[{}#{}]: watcher is starting, waiting for it to get in a started state", getTestClass().getSimpleName(), getTestName());
                 ensureWatcherStarted(false);
             } else {
-                logger.info("[{}#{}]: not starting watcher, because watcher is in state [{}]", getTestClass().getSimpleName(), getTestName(), response.getWatcherState());
+                logger.info("[{}#{}]: not starting watcher, because watcher is in state [{}]", getTestClass().getSimpleName(), getTestName(), state);
             }
         } else {
             logger.info("[{}#{}]: not starting watcher, because test cluster has no nodes", getTestClass().getSimpleName(), getTestName());
@@ -427,13 +428,29 @@ public abstract class AbstractWatcherIntegrationTests extends ElasticsearchInteg
     }
 
     protected void startWatcher() throws Exception {
-        watcherClient().prepareWatchService().start().get();
-        ensureWatcherStarted(false);
+        startWatcher(true);
     }
 
     protected void stopWatcher() throws Exception {
-        watcherClient().prepareWatchService().stop().get();
-        ensureWatcherStopped(false);
+        stopWatcher(true);
+    }
+
+    protected void startWatcher(boolean useClient) throws Exception {
+        if (useClient) {
+            watcherClient().prepareWatchService().start().get();
+        } else {
+            getInstanceFromMaster(WatcherLifeCycleService.class).start();
+        }
+        ensureWatcherStarted(useClient);
+    }
+
+    protected void stopWatcher(boolean useClient) throws Exception {
+        if (useClient) {
+            watcherClient().prepareWatchService().stop().get();
+        } else {
+            getInstanceFromMaster(WatcherLifeCycleService.class).stop();
+        }
+        ensureWatcherStopped(useClient);
     }
 
     protected void ensureWatcherOnlyRunningOnce() {
@@ -587,8 +604,6 @@ public abstract class AbstractWatcherIntegrationTests extends ElasticsearchInteg
 
         public static final String TEST_USERNAME = "test";
         public static final String TEST_PASSWORD = "changeme";
-        public static final String TEST_BASIC_AUTH_TOKEN = UsernamePasswordToken.basicAuthHeaderValue(TEST_USERNAME, new SecuredString(TEST_PASSWORD.toCharArray()));
-        public static final String ADMIN_BASIC_AUTH_TOKEN = UsernamePasswordToken.basicAuthHeaderValue("admin", new SecuredString("changeme".toCharArray()));
 
         static boolean auditLogsEnabled = SystemPropertyUtil.getBoolean("tests.audit_logs", true);
         static byte[] systemKey = generateKey(); // must be the same for all nodes
@@ -596,23 +611,27 @@ public abstract class AbstractWatcherIntegrationTests extends ElasticsearchInteg
         public static final String IP_FILTER = "allow: all\n";
 
         public static final String USERS =
+                "transport_client:{plain}changeme\n" +
                 TEST_USERNAME + ":{plain}" + TEST_PASSWORD + "\n" +
                 "admin:{plain}changeme\n" +
                 "monitor:{plain}changeme";
 
         public static final String USER_ROLES =
+                "transport_client:transport_client\n" +
                 "test:test\n" +
                 "admin:admin\n" +
                 "monitor:monitor";
 
         public static final String ROLES =
                 "test:\n" + // a user for the test infra.
-                "  cluster: all\n" +
+                "  cluster: cluster:monitor/state, cluster:monitor/health, indices:admin/template/delete, cluster:admin/repository/delete, indices:admin/template/put, cluster:monitor/stats\n" +
                 "  indices:\n" +
                 "    '*': all\n" +
                 "\n" +
                 "admin:\n" +
                 "  cluster: manage_watcher, cluster:monitor/nodes/info\n" +
+                "transport_client:\n" +
+                "  cluster: cluster:monitor/nodes/info\n" +
                 "\n" +
                 "monitor:\n" +
                 "  cluster: monitor_watcher, cluster:monitor/nodes/info\n"
diff --git a/src/test/java/org/elasticsearch/watcher/test/integration/BasicShieldTests.java b/src/test/java/org/elasticsearch/watcher/test/integration/BasicShieldTests.java
new file mode 100644
index 00000000000..b4899442d66
--- /dev/null
+++ b/src/test/java/org/elasticsearch/watcher/test/integration/BasicShieldTests.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.watcher.test.integration;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.joda.time.DateTime;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.shield.ShieldPlugin;
+import org.elasticsearch.shield.authc.AuthenticationException;
+import org.elasticsearch.shield.authc.support.SecuredString;
+import org.elasticsearch.shield.authz.AuthorizationException;
+import org.elasticsearch.watcher.WatcherPlugin;
+import org.elasticsearch.watcher.WatcherState;
+import org.elasticsearch.watcher.client.WatchSourceBuilders;
+import org.elasticsearch.watcher.condition.ConditionBuilders;
+import org.elasticsearch.watcher.test.AbstractWatcherIntegrationTests;
+import org.elasticsearch.watcher.transport.actions.delete.DeleteWatchResponse;
+import org.elasticsearch.watcher.transport.actions.execute.ExecuteWatchResponse;
+import org.elasticsearch.watcher.transport.actions.get.GetWatchResponse;
+import org.elasticsearch.watcher.transport.actions.put.PutWatchResponse;
+import org.elasticsearch.watcher.transport.actions.stats.WatcherStatsResponse;
+import org.elasticsearch.watcher.trigger.TriggerBuilders;
+import org.elasticsearch.watcher.trigger.TriggerEvent;
+import org.elasticsearch.watcher.trigger.schedule.IntervalSchedule;
+import org.elasticsearch.watcher.trigger.schedule.ScheduleTriggerEvent;
+import org.junit.Test;
+
+import static org.elasticsearch.common.joda.time.DateTimeZone.UTC;
+import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
+import static org.elasticsearch.watcher.client.WatchSourceBuilders.watchBuilder;
+import static org.elasticsearch.watcher.trigger.TriggerBuilders.schedule;
+import static org.elasticsearch.watcher.trigger.schedule.Schedules.interval;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.core.Is.is;
+
+public class BasicShieldTests extends AbstractWatcherIntegrationTests {
+
+    @Override
+    protected boolean enableShield() {
+        return true;
+    }
+
+    @Override
+    protected Settings transportClientSettings() {
+        return ImmutableSettings.builder()
+                .put("client.transport.sniff", false)
+                .put("plugin.types", ShieldPlugin.class.getName() + "," + WatcherPlugin.class.getName())
+                // Use just the transport user here, so we can test Watcher roles specifically
+                .put("shield.user", "transport_client:changeme")
+                .build();
+    }
+
+    @Test
+    public void testNoAuthorization() throws Exception {
+        try {
+            watcherClient().prepareWatcherStats().get();
+            fail("authentication failure should have occurred");
+        } catch (AuthorizationException e) {
+            // transport_client is the default user
+            assertThat(e.getMessage(), equalTo("action [cluster:monitor/watcher/stats] is unauthorized for user [transport_client]"));
+        }
+    }
+
+    @Test
+    public void testWatcherMonitorRole() throws Exception {
+        // stats and get watch apis require at least monitor role:
+        String token = basicAuthHeaderValue("test", new SecuredString("changeme".toCharArray()));
+        try {
+            watcherClient().prepareWatcherStats()
+                    .putHeader("Authorization", token)
+                    .get();
+            fail("authentication failure should have occurred");
+        } catch (AuthorizationException e) {
+            assertThat(e.getMessage(), equalTo("action [cluster:monitor/watcher/stats] is unauthorized for user [test]"));
+        }
+
+        try {
+            watcherClient().prepareGetWatch("_id")
+                    .putHeader("Authorization", token)
+                    .get();
+            fail("authentication failure should have occurred");
+        } catch (AuthorizationException e) {
+            assertThat(e.getMessage(), equalTo("action [cluster:monitor/watcher/watch/get] is unauthorized for user [test]"));
+        }
+
+        // stats and get watch are allowed by role monitor:
+        token = basicAuthHeaderValue("monitor", new SecuredString("changeme".toCharArray()));
+        WatcherStatsResponse statsResponse = watcherClient().prepareWatcherStats()
+                .putHeader("Authorization", token)
+                .get();
+        assertThat(statsResponse.getWatcherState(), equalTo(WatcherState.STARTED));
+        GetWatchResponse getWatchResponse = watcherClient().prepareGetWatch("_id")
+                .putHeader("Authorization", token)
+                .get();
+        assertThat(getWatchResponse.isFound(), is(false));
+
+        // but put watch isn't allowed by monitor:
+        try {
+            watcherClient().preparePutWatch("_id")
+                    .setSource(watchBuilder().trigger(schedule(interval(5, IntervalSchedule.Interval.Unit.SECONDS))))
+                    .putHeader("Authorization", token)
+                    .get();
+            fail("authentication failure should have occurred");
+        } catch (AuthorizationException e) {
+            assertThat(e.getMessage(), equalTo("action [cluster:admin/watcher/watch/put] is unauthorized for user [monitor]"));
+        }
+    }
+
+    @Test
+    public void testWatcherAdminRole() throws Exception {
+        // put, execute and delete watch apis requires watcher admin role:
+        String token = basicAuthHeaderValue("test", new SecuredString("changeme".toCharArray()));
+        try {
+            watcherClient().preparePutWatch("_id")
+                    .setSource(watchBuilder().trigger(schedule(interval(5, IntervalSchedule.Interval.Unit.SECONDS))))
+                    .putHeader("Authorization", token)
+                    .get();
+            fail("authentication failure should have occurred");
+        } catch (AuthorizationException e) {
+            assertThat(e.getMessage(), equalTo("action [cluster:admin/watcher/watch/put] is unauthorized for user [test]"));
+        }
+
+        TriggerEvent triggerEvent = new ScheduleTriggerEvent(new DateTime(UTC), new DateTime(UTC));
+        try {
+            watcherClient().prepareExecuteWatch("_id")
+                    .setTriggerEvent(triggerEvent)
+                    .putHeader("Authorization", token)
+                    .get();
+            fail("authentication failure should have occurred");
+        } catch (AuthorizationException e) {
+            assertThat(e.getMessage(), equalTo("action [cluster:admin/watcher/watch/execute] is unauthorized for user [test]"));
+        }
+
+        try {
+            watcherClient().prepareDeleteWatch("_id")
+                    .putHeader("Authorization", token)
+                    .get();
+            fail("authentication failure should have occurred");
+        } catch (AuthorizationException e) {
+            assertThat(e.getMessage(), equalTo("action [cluster:admin/watcher/watch/delete] is unauthorized for user [test]"));
+        }
+
+        // put, execute and delete watch apis are allowed by role admin:
+        token = basicAuthHeaderValue("admin", new SecuredString("changeme".toCharArray()));
+        PutWatchResponse putWatchResponse = watcherClient().preparePutWatch("_id")
+                .setSource(watchBuilder().trigger(schedule(interval(5, IntervalSchedule.Interval.Unit.SECONDS))))
+                .putHeader("Authorization", token)
+                .get();
+        assertThat(putWatchResponse.getVersion(), equalTo(1l));
+        ExecuteWatchResponse executeWatchResponse = watcherClient().prepareExecuteWatch("_id")
+                .setTriggerEvent(triggerEvent)
+                .putHeader("Authorization", token)
+                .get();
+        DeleteWatchResponse deleteWatchResponse = watcherClient().prepareDeleteWatch("_id")
+                .putHeader("Authorization", token)
+                .get();
+        assertThat(deleteWatchResponse.getVersion(), equalTo(2l));
+        assertThat(deleteWatchResponse.isFound(), is(true));
+
+        // stats and get watch are also allowed by role monitor:
+        token = basicAuthHeaderValue("monitor", new SecuredString("changeme".toCharArray()));
+        WatcherStatsResponse statsResponse = watcherClient().prepareWatcherStats()
+                .putHeader("Authorization", token)
+                .get();
+        assertThat(statsResponse.getWatcherState(), equalTo(WatcherState.STARTED));
+        GetWatchResponse getWatchResponse = watcherClient().prepareGetWatch("_id")
+                .putHeader("Authorization", token)
+                .get();
+        assertThat(getWatchResponse.isFound(), is(false));
+    }
+
+}