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)); + } + +}