Skip to content

Commit 44a5014

Browse files
authored
[Backport 2.19] Log request body to audit logs (#5071) (#5199) (#5208)
1 parent ff00eef commit 44a5014

File tree

5 files changed

+315
-15
lines changed

5 files changed

+315
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*
8+
* Modifications Copyright OpenSearch Contributors. See
9+
* GitHub history for details.
10+
*/
11+
12+
package org.opensearch.security.rest;
13+
14+
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
15+
import org.apache.http.HttpStatus;
16+
import org.junit.ClassRule;
17+
import org.junit.Rule;
18+
import org.junit.Test;
19+
import org.junit.runner.RunWith;
20+
21+
import org.opensearch.test.framework.AuditCompliance;
22+
import org.opensearch.test.framework.AuditConfiguration;
23+
import org.opensearch.test.framework.AuditFilters;
24+
import org.opensearch.test.framework.TestSecurityConfig;
25+
import org.opensearch.test.framework.TestSecurityConfig.Role;
26+
import org.opensearch.test.framework.audit.AuditLogsRule;
27+
import org.opensearch.test.framework.cluster.ClusterManager;
28+
import org.opensearch.test.framework.cluster.LocalCluster;
29+
import org.opensearch.test.framework.cluster.TestRestClient;
30+
import org.opensearch.test.framework.testplugins.dummy.CustomLegacyTestPlugin;
31+
import org.opensearch.test.framework.testplugins.dummyprotected.CustomRestProtectedTestPlugin;
32+
33+
import static org.hamcrest.MatcherAssert.assertThat;
34+
import static org.hamcrest.Matchers.equalTo;
35+
import static org.opensearch.rest.RestRequest.Method.GET;
36+
import static org.opensearch.rest.RestRequest.Method.POST;
37+
import static org.opensearch.security.auditlog.impl.AuditCategory.AUTHENTICATED;
38+
import static org.opensearch.security.auditlog.impl.AuditCategory.FAILED_LOGIN;
39+
import static org.opensearch.security.auditlog.impl.AuditCategory.GRANTED_PRIVILEGES;
40+
import static org.opensearch.security.auditlog.impl.AuditCategory.MISSING_PRIVILEGES;
41+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
42+
import static org.opensearch.test.framework.audit.AuditMessagePredicate.privilegePredicateRESTLayer;
43+
import static org.opensearch.test.framework.audit.AuditMessagePredicate.privilegePredicateTransportLayer;
44+
45+
@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
46+
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
47+
public class AuthZinRestLayerTests {
48+
protected final static TestSecurityConfig.User REST_ONLY = new TestSecurityConfig.User("rest_only").roles(
49+
new Role("rest_only_role").clusterPermissions("security:dummy_protected/get").clusterPermissions("cluster:admin/dummy_plugin/dummy")
50+
);
51+
52+
protected final static TestSecurityConfig.User TRANSPORT_ONLY = new TestSecurityConfig.User("transport_only").roles(
53+
new Role("transport_only_role").clusterPermissions("cluster:admin/dummy_plugin/dummy")
54+
);
55+
56+
protected final static TestSecurityConfig.User REST_PLUS_TRANSPORT = new TestSecurityConfig.User("rest_plus_transport").roles(
57+
new Role("rest_plus_transport_role").clusterPermissions("security:dummy_protected/get")
58+
.clusterPermissions("cluster:admin/dummy_plugin/dummy", "cluster:admin/dummy_protected_plugin/dummy/get")
59+
);
60+
61+
protected final static TestSecurityConfig.User NO_PERM = new TestSecurityConfig.User("no_perm").roles(new Role("no_perm_role"));
62+
63+
protected final static TestSecurityConfig.User UNREGISTERED = new TestSecurityConfig.User("unregistered");
64+
65+
public static final String UNPROTECTED_BASE_ENDPOINT = "_plugins/_dummy";
66+
public static final String PROTECTED_BASE_ENDPOINT = "_plugins/_dummy_protected";
67+
public static final String UNPROTECTED_API = UNPROTECTED_BASE_ENDPOINT + "/dummy";
68+
public static final String PROTECTED_API = PROTECTED_BASE_ENDPOINT + "/dummy";
69+
70+
@ClassRule
71+
public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS)
72+
.authc(AUTHC_HTTPBASIC_INTERNAL)
73+
.users(REST_ONLY, REST_PLUS_TRANSPORT, TRANSPORT_ONLY, NO_PERM)
74+
.plugin(CustomLegacyTestPlugin.class)
75+
.plugin(CustomRestProtectedTestPlugin.class)
76+
.audit(
77+
new AuditConfiguration(true).compliance(new AuditCompliance().enabled(true))
78+
.filters(new AuditFilters().enabledRest(true).enabledTransport(true).resolveBulkRequests(true))
79+
)
80+
.build();
81+
82+
@Rule
83+
public AuditLogsRule auditLogsRule = new AuditLogsRule();
84+
85+
/** Basic Access check */
86+
87+
@Test
88+
public void testShouldNotAllowUnregisteredUsers() {
89+
try (TestRestClient client = cluster.getRestClient(UNREGISTERED)) {
90+
// Legacy plugin
91+
assertThat(client.get(UNPROTECTED_API).getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED));
92+
auditLogsRule.assertExactlyOne(privilegePredicateRESTLayer(FAILED_LOGIN, UNREGISTERED, GET, "/" + UNPROTECTED_API));
93+
94+
// Protected Routes plugin
95+
assertThat(client.get(PROTECTED_API).getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED));
96+
auditLogsRule.assertExactlyOne(privilegePredicateRESTLayer(FAILED_LOGIN, UNREGISTERED, GET, "/" + PROTECTED_API));
97+
}
98+
}
99+
100+
@Test
101+
public void testAccessDeniedForUserWithNoPermissions() {
102+
try (TestRestClient client = cluster.getRestClient(NO_PERM)) {
103+
// fail at Transport (won't have a rest authz success audit log since this is not a protected endpoint)
104+
assertThat(client.get(UNPROTECTED_API).getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN));
105+
auditLogsRule.assertExactlyOne(
106+
privilegePredicateTransportLayer(MISSING_PRIVILEGES, NO_PERM, "DummyRequest", "cluster:admin/dummy_plugin/dummy")
107+
);
108+
109+
// fail at REST
110+
assertThat(client.get(PROTECTED_API).getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED));
111+
auditLogsRule.assertExactlyOne(privilegePredicateRESTLayer(MISSING_PRIVILEGES, NO_PERM, GET, "/" + PROTECTED_API));
112+
}
113+
}
114+
115+
@Test
116+
public void testShouldFailWithoutPermForPathWithoutLeadingSlashes() {
117+
try (TestRestClient client = cluster.getRestClient(NO_PERM)) {
118+
119+
// Protected Routes plugin
120+
assertThat(client.getWithoutLeadingSlash(PROTECTED_API).getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED));
121+
}
122+
}
123+
124+
/** AuthZ in REST Layer check */
125+
126+
@Test
127+
public void testShouldAllowAtRestAndBlockAtTransport() {
128+
try (TestRestClient client = cluster.getRestClient(REST_ONLY)) {
129+
assertThat(client.get(PROTECTED_API).getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN));
130+
// granted at Rest layer
131+
auditLogsRule.assertExactlyOne(privilegePredicateRESTLayer(GRANTED_PRIVILEGES, REST_ONLY, GET, "/" + PROTECTED_API));
132+
// missing at Transport layer
133+
auditLogsRule.assertExactlyOne(
134+
privilegePredicateTransportLayer(
135+
MISSING_PRIVILEGES,
136+
REST_ONLY,
137+
"DummyRequest",
138+
"cluster:admin/dummy_protected_plugin/dummy/get"
139+
)
140+
);
141+
}
142+
}
143+
144+
@Test
145+
public void testRequestBodyIsAuditLogged() {
146+
try (TestRestClient client = cluster.getRestClient(REST_PLUS_TRANSPORT)) {
147+
String dummyBody = "{\"hello\": \"world\"}";
148+
client.postJson(PROTECTED_API, dummyBody);
149+
auditLogsRule.assertExactlyOne(
150+
privilegePredicateRESTLayer(AUTHENTICATED, REST_PLUS_TRANSPORT, POST, "/" + PROTECTED_API).withRequestBody(dummyBody)
151+
);
152+
}
153+
}
154+
155+
@Test
156+
public void testShouldAllowAtRestAndTransport() {
157+
try (TestRestClient client = cluster.getRestClient(REST_PLUS_TRANSPORT)) {
158+
assertOKResponseFromProtectedPlugin(client);
159+
160+
auditLogsRule.assertExactlyOne(privilegePredicateRESTLayer(GRANTED_PRIVILEGES, REST_PLUS_TRANSPORT, GET, "/" + PROTECTED_API));
161+
auditLogsRule.assertExactlyOne(
162+
privilegePredicateTransportLayer(
163+
GRANTED_PRIVILEGES,
164+
REST_PLUS_TRANSPORT,
165+
"DummyRequest",
166+
"cluster:admin/dummy_protected_plugin/dummy/get"
167+
)
168+
);
169+
}
170+
}
171+
172+
@Test
173+
public void testShouldBlockAccessToEndpointForWhichUserHasNoPermission() {
174+
try (TestRestClient client = cluster.getRestClient(REST_ONLY)) {
175+
assertThat(client.post(PROTECTED_API).getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED));
176+
177+
auditLogsRule.assertExactlyOne(privilegePredicateRESTLayer(MISSING_PRIVILEGES, REST_ONLY, POST, "/" + PROTECTED_API));
178+
}
179+
180+
try (TestRestClient client = cluster.getRestClient(REST_PLUS_TRANSPORT)) {
181+
assertThat(client.post(PROTECTED_API).getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED));
182+
183+
auditLogsRule.assertExactlyOne(privilegePredicateRESTLayer(MISSING_PRIVILEGES, REST_PLUS_TRANSPORT, POST, "/" + PROTECTED_API));
184+
}
185+
}
186+
187+
/** Backwards compatibility check */
188+
189+
@Test
190+
public void testBackwardsCompatibility() {
191+
192+
// TRANSPORT_ONLY should have access to legacy endpoint, but not protected endpoint
193+
try (TestRestClient client = cluster.getRestClient(TRANSPORT_ONLY)) {
194+
TestRestClient.HttpResponse res = client.get(PROTECTED_API);
195+
assertThat(res.getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED));
196+
auditLogsRule.assertExactlyOne(privilegePredicateRESTLayer(MISSING_PRIVILEGES, TRANSPORT_ONLY, GET, "/" + PROTECTED_API));
197+
198+
assertOKResponseFromLegacyPlugin(client);
199+
// check that there is no log for REST layer AuthZ since this is an unprotected endpoint
200+
auditLogsRule.assertExactly(0, privilegePredicateRESTLayer(GRANTED_PRIVILEGES, TRANSPORT_ONLY, GET, UNPROTECTED_API));
201+
// check that there is exactly 1 message for Transport Layer privilege evaluation
202+
auditLogsRule.assertExactlyOne(
203+
privilegePredicateTransportLayer(GRANTED_PRIVILEGES, TRANSPORT_ONLY, "DummyRequest", "cluster:admin/dummy_plugin/dummy")
204+
);
205+
}
206+
207+
// REST_ONLY should have access to legacy endpoint (protected endpoint already tested above)
208+
try (TestRestClient client = cluster.getRestClient(REST_ONLY)) {
209+
assertOKResponseFromLegacyPlugin(client);
210+
auditLogsRule.assertExactly(0, privilegePredicateRESTLayer(GRANTED_PRIVILEGES, REST_ONLY, GET, UNPROTECTED_API));
211+
auditLogsRule.assertExactlyOne(
212+
privilegePredicateTransportLayer(GRANTED_PRIVILEGES, REST_ONLY, "DummyRequest", "cluster:admin/dummy_plugin/dummy")
213+
);
214+
}
215+
216+
// DUMMY_WITH_TRANSPORT_PERM should have access to legacy endpoint (protected endpoint already tested above)
217+
try (TestRestClient client = cluster.getRestClient(REST_PLUS_TRANSPORT)) {
218+
assertOKResponseFromLegacyPlugin(client);
219+
auditLogsRule.assertExactly(0, privilegePredicateRESTLayer(GRANTED_PRIVILEGES, REST_PLUS_TRANSPORT, GET, UNPROTECTED_API));
220+
auditLogsRule.assertExactlyOne(
221+
privilegePredicateTransportLayer(
222+
GRANTED_PRIVILEGES,
223+
REST_PLUS_TRANSPORT,
224+
"DummyRequest",
225+
"cluster:admin/dummy_plugin/dummy"
226+
)
227+
);
228+
}
229+
230+
// NO_PERM should not have access to legacy endpoint (protected endpoint already tested above)
231+
try (TestRestClient client = cluster.getRestClient(NO_PERM)) {
232+
assertThat(client.get(UNPROTECTED_API).getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN));
233+
auditLogsRule.assertExactly(0, privilegePredicateRESTLayer(MISSING_PRIVILEGES, NO_PERM, GET, UNPROTECTED_API));
234+
auditLogsRule.assertExactlyOne(
235+
privilegePredicateTransportLayer(MISSING_PRIVILEGES, NO_PERM, "DummyRequest", "cluster:admin/dummy_plugin/dummy")
236+
);
237+
}
238+
239+
// UNREGISTERED should not have access to legacy endpoint (protected endpoint already tested above)
240+
try (TestRestClient client = cluster.getRestClient(UNREGISTERED)) {
241+
assertThat(client.get(UNPROTECTED_API).getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED));
242+
auditLogsRule.assertExactly(0, privilegePredicateRESTLayer(MISSING_PRIVILEGES, UNREGISTERED, GET, UNPROTECTED_API));
243+
auditLogsRule.assertExactly(
244+
0,
245+
privilegePredicateTransportLayer(MISSING_PRIVILEGES, UNREGISTERED, "DummyRequest", "cluster:admin/dummy_plugin/dummy")
246+
);
247+
auditLogsRule.assertExactly(0, privilegePredicateRESTLayer(FAILED_LOGIN, UNREGISTERED, GET, UNPROTECTED_API));
248+
}
249+
}
250+
251+
/** Helper Methods */
252+
private void assertOKResponseFromLegacyPlugin(TestRestClient client) {
253+
String expectedResponseFromLegacyPlugin = "{\"response_string\":\"Hello from dummy plugin\"}";
254+
TestRestClient.HttpResponse res = client.get(UNPROTECTED_API);
255+
assertThat(res.getStatusCode(), equalTo(HttpStatus.SC_OK));
256+
assertThat(res.getBody(), equalTo(expectedResponseFromLegacyPlugin));
257+
}
258+
259+
private void assertOKResponseFromProtectedPlugin(TestRestClient client) {
260+
String expectedResponseFromProtectedPlugin = "{\"response_string\":\"Hello from dummy protected plugin\"}";
261+
TestRestClient.HttpResponse res = client.get(PROTECTED_API);
262+
assertThat(res.getStatusCode(), equalTo(HttpStatus.SC_OK));
263+
assertThat(res.getBody(), equalTo(expectedResponseFromProtectedPlugin));
264+
}
265+
}

0 commit comments

Comments
 (0)