|
53 | 53 |
|
54 | 54 | import static org.hamcrest.Matchers.containsInAnyOrder;
|
55 | 55 | import static org.hamcrest.Matchers.containsString;
|
| 56 | +import static org.hamcrest.Matchers.empty; |
56 | 57 | import static org.hamcrest.Matchers.equalTo;
|
57 | 58 | import static org.hamcrest.Matchers.hasItem;
|
| 59 | +import static org.hamcrest.Matchers.in; |
58 | 60 | import static org.hamcrest.Matchers.is;
|
| 61 | +import static org.hamcrest.Matchers.iterableWithSize; |
59 | 62 | import static org.hamcrest.Matchers.notNullValue;
|
60 | 63 |
|
61 | 64 | public class FailureStoreSecurityRestIT extends ESRestTestCase {
|
@@ -1921,6 +1924,335 @@ public void testFailureStoreAccess() throws Exception {
|
1921 | 1924 | }
|
1922 | 1925 | }
|
1923 | 1926 |
|
| 1927 | + public void testModifyingFailureStoreBackingIndices() throws Exception { |
| 1928 | + setupDataStream(); |
| 1929 | + Tuple<String, String> backingIndices = getSingleDataAndFailureIndices("test1"); |
| 1930 | + String dataIndexName = backingIndices.v1(); |
| 1931 | + String failureIndexName = backingIndices.v2(); |
| 1932 | + |
| 1933 | + createUser(MANAGE_ACCESS, PASSWORD, MANAGE_ACCESS); |
| 1934 | + createOrUpdateRoleAndApiKey(MANAGE_ACCESS, MANAGE_ACCESS, """ |
| 1935 | + { |
| 1936 | + "cluster": ["all"], |
| 1937 | + "indices": [{"names": ["test*"], "privileges": ["manage"]}] |
| 1938 | + }"""); |
| 1939 | + assertOK(addFailureStoreBackingIndex(MANAGE_ACCESS, "test1", failureIndexName)); |
| 1940 | + assertDataStreamHasDataAndFailureIndices("test1", dataIndexName, failureIndexName); |
| 1941 | + |
| 1942 | + // remove the failure index |
| 1943 | + assertOK(removeFailureStoreBackingIndex(MANAGE_ACCESS, "test1", failureIndexName)); |
| 1944 | + assertDataStreamHasNoFailureIndices("test1", dataIndexName); |
| 1945 | + |
| 1946 | + // adding it will fail because the user has no direct access to the backing failure index (.fs*) |
| 1947 | + expectThrows(() -> addFailureStoreBackingIndex(MANAGE_ACCESS, "test1", failureIndexName), 403); |
| 1948 | + |
| 1949 | + // let's change that |
| 1950 | + createOrUpdateRoleAndApiKey(MANAGE_ACCESS, MANAGE_ACCESS, """ |
| 1951 | + { |
| 1952 | + "cluster": ["all"], |
| 1953 | + "indices": [{"names": ["test*", ".fs*"], "privileges": ["manage"]}] |
| 1954 | + }"""); |
| 1955 | + |
| 1956 | + // adding should succeed now |
| 1957 | + assertOK(addFailureStoreBackingIndex(MANAGE_ACCESS, "test1", failureIndexName)); |
| 1958 | + assertDataStreamHasDataAndFailureIndices("test1", dataIndexName, failureIndexName); |
| 1959 | + |
| 1960 | + createUser(MANAGE_FAILURE_STORE_ACCESS, PASSWORD, MANAGE_FAILURE_STORE_ACCESS); |
| 1961 | + createOrUpdateRoleAndApiKey(MANAGE_FAILURE_STORE_ACCESS, MANAGE_FAILURE_STORE_ACCESS, """ |
| 1962 | + { |
| 1963 | + "cluster": ["all"], |
| 1964 | + "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] |
| 1965 | + }"""); |
| 1966 | + |
| 1967 | + // manage_failure_store can only remove the failure backing index, but not add it |
| 1968 | + assertOK(removeFailureStoreBackingIndex(MANAGE_FAILURE_STORE_ACCESS, "test1", failureIndexName)); |
| 1969 | + assertDataStreamHasNoFailureIndices("test1", dataIndexName); |
| 1970 | + expectThrows(() -> addFailureStoreBackingIndex(MANAGE_FAILURE_STORE_ACCESS, "test1", failureIndexName), 403); |
| 1971 | + |
| 1972 | + // not even with access to .fs* |
| 1973 | + createOrUpdateRoleAndApiKey(MANAGE_FAILURE_STORE_ACCESS, MANAGE_FAILURE_STORE_ACCESS, """ |
| 1974 | + { |
| 1975 | + "cluster": ["all"], |
| 1976 | + "indices": [{"names": ["test*", ".fs*"], "privileges": ["manage_failure_store"]}] |
| 1977 | + }"""); |
| 1978 | + expectThrows(() -> addFailureStoreBackingIndex(MANAGE_FAILURE_STORE_ACCESS, "test1", failureIndexName), 403); |
| 1979 | + } |
| 1980 | + |
| 1981 | + private void assertDataStreamHasDataAndFailureIndices(String dataStreamName, String dataIndexName, String failureIndexName) |
| 1982 | + throws IOException { |
| 1983 | + Tuple<List<String>, List<String>> indices = getDataAndFailureIndices(dataStreamName); |
| 1984 | + assertThat(indices.v1(), containsInAnyOrder(dataIndexName)); |
| 1985 | + assertThat(indices.v2(), containsInAnyOrder(failureIndexName)); |
| 1986 | + } |
| 1987 | + |
| 1988 | + private void assertDataStreamHasNoFailureIndices(String dataStreamName, String dataIndexName) throws IOException { |
| 1989 | + Tuple<List<String>, List<String>> indices = getDataAndFailureIndices(dataStreamName); |
| 1990 | + assertThat(indices.v1(), containsInAnyOrder(dataIndexName)); |
| 1991 | + assertThat(indices.v2(), is(empty())); |
| 1992 | + } |
| 1993 | + |
| 1994 | + private Response addFailureStoreBackingIndex(String user, String dataStreamName, String failureIndexName) throws IOException { |
| 1995 | + return modifyFailureStoreBackingIndex(user, "add_backing_index", dataStreamName, failureIndexName); |
| 1996 | + } |
| 1997 | + |
| 1998 | + private Response removeFailureStoreBackingIndex(String user, String dataStreamName, String failureIndexName) throws IOException { |
| 1999 | + return modifyFailureStoreBackingIndex(user, "remove_backing_index", dataStreamName, failureIndexName); |
| 2000 | + } |
| 2001 | + |
| 2002 | + private Response modifyFailureStoreBackingIndex(String user, String action, String dataStreamName, String failureIndexName) |
| 2003 | + throws IOException { |
| 2004 | + Request request = new Request("POST", "/_data_stream/_modify"); |
| 2005 | + request.setJsonEntity(Strings.format(""" |
| 2006 | + { |
| 2007 | + "actions": [ |
| 2008 | + { |
| 2009 | + "%s": { |
| 2010 | + "data_stream": "%s", |
| 2011 | + "index": "%s", |
| 2012 | + "failure_store" : true |
| 2013 | + } |
| 2014 | + } |
| 2015 | + ] |
| 2016 | + } |
| 2017 | + """, action, dataStreamName, failureIndexName)); |
| 2018 | + return performRequest(user, request); |
| 2019 | + } |
| 2020 | + |
| 2021 | + public void testDataStreamApis() throws Exception { |
| 2022 | + setupDataStream(); |
| 2023 | + setupOtherDataStream(); |
| 2024 | + |
| 2025 | + final String username = "user"; |
| 2026 | + final String roleName = "role"; |
| 2027 | + createUser(username, PASSWORD, roleName); |
| 2028 | + { |
| 2029 | + // manage_failure_store does not grant access to _data_stream APIs |
| 2030 | + createOrUpdateRoleAndApiKey(username, roleName, """ |
| 2031 | + { |
| 2032 | + "cluster": ["all"], |
| 2033 | + "indices": [ |
| 2034 | + { |
| 2035 | + "names": ["test1", "other1"], |
| 2036 | + "privileges": ["manage_failure_store"] |
| 2037 | + } |
| 2038 | + ] |
| 2039 | + } |
| 2040 | + """); |
| 2041 | + |
| 2042 | + expectThrows(() -> performRequest(username, new Request("GET", "/_data_stream/test1")), 403); |
| 2043 | + expectThrows(() -> performRequest(username, new Request("GET", "/_data_stream/test1/_stats")), 403); |
| 2044 | + expectThrows(() -> performRequest(username, new Request("GET", "/_data_stream/test1/_options")), 403); |
| 2045 | + expectThrows(() -> performRequest(username, new Request("GET", "/_data_stream/test1/_lifecycle")), 403); |
| 2046 | + expectThrows(() -> putDataStreamLifecycle(username, "test1", """ |
| 2047 | + { |
| 2048 | + "data_retention": "7d" |
| 2049 | + }"""), 403); |
| 2050 | + expectEmptyDataStreamStats(username, new Request("GET", "/_data_stream/_stats")); |
| 2051 | + expectEmptyDataStreamStats(username, new Request("GET", "/_data_stream/" + randomFrom("test*", "*") + "/_stats")); |
| 2052 | + } |
| 2053 | + { |
| 2054 | + createOrUpdateRoleAndApiKey(username, roleName, """ |
| 2055 | + { |
| 2056 | + "cluster": ["all"], |
| 2057 | + "indices": [ |
| 2058 | + { |
| 2059 | + "names": ["test1"], |
| 2060 | + "privileges": ["manage"] |
| 2061 | + } |
| 2062 | + ] |
| 2063 | + } |
| 2064 | + """); |
| 2065 | + |
| 2066 | + expectDataStreamStats(username, new Request("GET", "/_data_stream/_stats"), "test1", 2); |
| 2067 | + expectDataStreamStats( |
| 2068 | + username, |
| 2069 | + new Request("GET", "/_data_stream/" + randomFrom("test1", "test*", "*") + "/_stats"), |
| 2070 | + "test1", |
| 2071 | + 2 |
| 2072 | + ); |
| 2073 | + expectDataStreams(username, new Request("GET", "/_data_stream/" + randomFrom("test1", "test*", "*")), "test1"); |
| 2074 | + putDataStreamLifecycle(username, "test1", """ |
| 2075 | + { |
| 2076 | + "data_retention": "7d" |
| 2077 | + }"""); |
| 2078 | + |
| 2079 | + var lifecycleResponse = assertOKAndCreateObjectPath( |
| 2080 | + performRequest(username, new Request("GET", "/_data_stream/" + randomFrom("test1", "test*", "*") + "/_lifecycle")) |
| 2081 | + ); |
| 2082 | + assertThat(lifecycleResponse.evaluate("data_streams"), iterableWithSize(1)); |
| 2083 | + assertThat(lifecycleResponse.evaluate("data_streams.0.name"), equalTo("test1")); |
| 2084 | + |
| 2085 | + var optionsResponse = assertOKAndCreateObjectPath( |
| 2086 | + performRequest(username, new Request("GET", "/_data_stream/" + randomFrom("test1", "test*", "*") + "/_options")) |
| 2087 | + ); |
| 2088 | + assertThat(optionsResponse.evaluate("data_streams"), iterableWithSize(1)); |
| 2089 | + assertThat(optionsResponse.evaluate("data_streams.0.name"), equalTo("test1")); |
| 2090 | + } |
| 2091 | + } |
| 2092 | + |
| 2093 | + private void putDataStreamLifecycle(String user, String dataStreamName, String lifecyclePolicy) throws IOException { |
| 2094 | + Request request = new Request("PUT", "/_data_stream/" + dataStreamName + "/_lifecycle"); |
| 2095 | + request.setJsonEntity(lifecyclePolicy); |
| 2096 | + assertOK(performRequest(user, request)); |
| 2097 | + } |
| 2098 | + |
| 2099 | + private void expectDataStreams(String user, Request dataStreamRequest, String dataStreamName) throws IOException { |
| 2100 | + Response response = performRequest(user, dataStreamRequest); |
| 2101 | + ObjectPath path = assertOKAndCreateObjectPath(response); |
| 2102 | + List<?> dataStreams = path.evaluate("data_streams"); |
| 2103 | + assertThat(dataStreams.size(), equalTo(1)); |
| 2104 | + assertThat(path.evaluate("data_streams.0.name"), equalTo(dataStreamName)); |
| 2105 | + } |
| 2106 | + |
| 2107 | + private void expectDataStreamStats(String user, Request statsRequest, String dataStreamName, int backingIndices) throws IOException { |
| 2108 | + Response response = performRequest(user, statsRequest); |
| 2109 | + ObjectPath path = assertOKAndCreateObjectPath(response); |
| 2110 | + assertThat(path.evaluate("data_stream_count"), equalTo(1)); |
| 2111 | + assertThat(path.evaluate("backing_indices"), equalTo(backingIndices)); |
| 2112 | + assertThat(path.evaluate("data_streams.0.data_stream"), equalTo(dataStreamName)); |
| 2113 | + } |
| 2114 | + |
| 2115 | + private void expectEmptyDataStreamStats(String user, Request request) throws IOException { |
| 2116 | + Response response = performRequest(user, request); |
| 2117 | + ObjectPath path = assertOKAndCreateObjectPath(response); |
| 2118 | + assertThat(path.evaluate("data_stream_count"), equalTo(0)); |
| 2119 | + assertThat(path.evaluate("backing_indices"), equalTo(0)); |
| 2120 | + } |
| 2121 | + |
| 2122 | + public void testWatcher() throws Exception { |
| 2123 | + setupDataStream(); |
| 2124 | + setupOtherDataStream(); |
| 2125 | + |
| 2126 | + final Tuple<String, String> backingIndices = getSingleDataAndFailureIndices("test1"); |
| 2127 | + final String dataIndexName = backingIndices.v1(); |
| 2128 | + final String failureIndexName = backingIndices.v2(); |
| 2129 | + |
| 2130 | + final String watchId = "failures-watch"; |
| 2131 | + final String username = "user"; |
| 2132 | + final String roleName = "role"; |
| 2133 | + createUser(username, PASSWORD, roleName); |
| 2134 | + |
| 2135 | + { |
| 2136 | + // grant access to the failure store |
| 2137 | + createOrUpdateRoleAndApiKey(username, roleName, """ |
| 2138 | + { |
| 2139 | + "cluster": ["manage_security", "manage_watcher"], |
| 2140 | + "indices": [ |
| 2141 | + { |
| 2142 | + "names": ["test1"], |
| 2143 | + "privileges": ["read_failure_store"] |
| 2144 | + } |
| 2145 | + ] |
| 2146 | + } |
| 2147 | + """); |
| 2148 | + |
| 2149 | + // searching the failure store should return only test1 failure indices |
| 2150 | + createOrUpdateWatcher(username, watchId, Strings.format(""" |
| 2151 | + { |
| 2152 | + "trigger": { "schedule": { "interval": "60m"}}, |
| 2153 | + "input": { |
| 2154 | + "search": { |
| 2155 | + "request": { |
| 2156 | + "indices": [ "%s" ], |
| 2157 | + "body": {"query": {"match_all": {}}} |
| 2158 | + } |
| 2159 | + } |
| 2160 | + } |
| 2161 | + }""", randomFrom("test1::failures", "test*::failures", "*::failures", failureIndexName))); |
| 2162 | + executeWatchAndAssertResults(username, watchId, failureIndexName); |
| 2163 | + |
| 2164 | + // searching the data should return empty results |
| 2165 | + createOrUpdateWatcher(username, watchId, Strings.format(""" |
| 2166 | + { |
| 2167 | + "trigger": { "schedule": { "interval": "60m"}}, |
| 2168 | + "input": { |
| 2169 | + "search": { |
| 2170 | + "request": { |
| 2171 | + "indices": [ "%s" ], |
| 2172 | + "body": {"query": {"match_all": {}}} |
| 2173 | + } |
| 2174 | + } |
| 2175 | + } |
| 2176 | + }""", randomFrom(dataIndexName, "*", "test*", "test1", "test1::data"))); |
| 2177 | + executeWatchAndAssertEmptyResults(username, watchId); |
| 2178 | + } |
| 2179 | + |
| 2180 | + { |
| 2181 | + // remove read_failure_store and add read |
| 2182 | + createOrUpdateRoleAndApiKey(username, roleName, """ |
| 2183 | + { |
| 2184 | + "cluster": ["manage_security", "manage_watcher"], |
| 2185 | + "indices": [ |
| 2186 | + { |
| 2187 | + "names": ["test1"], |
| 2188 | + "privileges": ["read"] |
| 2189 | + } |
| 2190 | + ] |
| 2191 | + } |
| 2192 | + """); |
| 2193 | + |
| 2194 | + // searching the failure store should return empty results |
| 2195 | + createOrUpdateWatcher(username, watchId, Strings.format(""" |
| 2196 | + { |
| 2197 | + "trigger": { "schedule": { "interval": "60m"}}, |
| 2198 | + "input": { |
| 2199 | + "search": { |
| 2200 | + "request": { |
| 2201 | + "indices": [ "%s" ], |
| 2202 | + "body": {"query": {"match_all": {}}} |
| 2203 | + } |
| 2204 | + } |
| 2205 | + } |
| 2206 | + }""", randomFrom("test1::failures", "test*::failures", "*::failures", failureIndexName))); |
| 2207 | + executeWatchAndAssertEmptyResults(username, watchId); |
| 2208 | + |
| 2209 | + // searching the data should return single result |
| 2210 | + createOrUpdateWatcher(username, watchId, Strings.format(""" |
| 2211 | + { |
| 2212 | + "trigger": { "schedule": { "interval": "60m"}}, |
| 2213 | + "input": { |
| 2214 | + "search": { |
| 2215 | + "request": { |
| 2216 | + "indices": [ "%s" ], |
| 2217 | + "body": {"query": {"match_all": {}}} |
| 2218 | + } |
| 2219 | + } |
| 2220 | + } |
| 2221 | + }""", randomFrom("*", "test*", "test1", "test1::data", dataIndexName))); |
| 2222 | + executeWatchAndAssertResults(username, watchId, dataIndexName); |
| 2223 | + } |
| 2224 | + } |
| 2225 | + |
| 2226 | + private void executeWatchAndAssertResults(String user, String watchId, final String... expectedIndices) throws IOException { |
| 2227 | + Request request = new Request("POST", "_watcher/watch/" + watchId + "/_execute"); |
| 2228 | + Response response = performRequest(user, request); |
| 2229 | + ObjectPath path = assertOKAndCreateObjectPath(response); |
| 2230 | + assertThat(path.evaluate("watch_record.user"), equalTo(user)); |
| 2231 | + assertThat(path.evaluate("watch_record.state"), equalTo("executed")); |
| 2232 | + assertThat(path.evaluate("watch_record.result.input.status"), equalTo("success")); |
| 2233 | + assertThat(path.evaluate("watch_record.result.input.payload.hits.total"), equalTo(expectedIndices.length)); |
| 2234 | + List<Map<String, ?>> hits = path.evaluate("watch_record.result.input.payload.hits.hits"); |
| 2235 | + hits.stream().map(hit -> hit.get("_index")).forEach(index -> { assertThat(index, is(in(expectedIndices))); }); |
| 2236 | + } |
| 2237 | + |
| 2238 | + private void executeWatchAndAssertEmptyResults(String user, String watchId) throws IOException { |
| 2239 | + Request request = new Request("POST", "_watcher/watch/" + watchId + "/_execute"); |
| 2240 | + Response response = performRequest(user, request); |
| 2241 | + ObjectPath path = assertOKAndCreateObjectPath(response); |
| 2242 | + assertThat(path.evaluate("watch_record.user"), equalTo(user)); |
| 2243 | + assertThat(path.evaluate("watch_record.state"), equalTo("executed")); |
| 2244 | + assertThat(path.evaluate("watch_record.result.input.status"), equalTo("success")); |
| 2245 | + assertThat(path.evaluate("watch_record.result.input.payload.hits.total"), equalTo(0)); |
| 2246 | + List<Map<String, ?>> hits = path.evaluate("watch_record.result.input.payload.hits.hits"); |
| 2247 | + assertThat(hits.size(), equalTo(0)); |
| 2248 | + } |
| 2249 | + |
| 2250 | + private void createOrUpdateWatcher(String user, String watchId, String watch) throws IOException { |
| 2251 | + Request request = new Request("PUT", "/_watcher/watch/" + watchId); |
| 2252 | + request.setJsonEntity(watch); |
| 2253 | + assertOK(performRequest(user, request)); |
| 2254 | + } |
| 2255 | + |
1924 | 2256 | public void testAliasBasedAccess() throws Exception {
|
1925 | 2257 | List<String> docIds = setupDataStream();
|
1926 | 2258 | assertThat(docIds.size(), equalTo(2));
|
|
0 commit comments