Skip to content

Commit a23b35c

Browse files
Rishav9852KumarRishav Kumar
andauthored
Selective User Cache Invalidation Enhancement (#5337)
Signed-off-by: Rishav Kumar <[email protected]> Co-authored-by: Rishav Kumar <[email protected]>
1 parent a4beb72 commit a23b35c

File tree

12 files changed

+533
-50
lines changed

12 files changed

+533
-50
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
- [Resource Permissions] Introduces Centralized Resource Access Control Framework ([#5281](https://github.com/opensearch-project/security/pull/5281))
99
- Github workflow for changelog verification ([#5318](https://github.com/opensearch-project/security/pull/5318))
1010
- Register cluster settings listener for `plugins.security.cache.ttl_minutes` ([#5324](https://github.com/opensearch-project/security/pull/5324))
11+
- Add flush cache endpoint for individual user ([#5337](https://github.com/opensearch-project/security/pull/5337))
1112

1213
### Changed
1314
- Use extendedPlugins in integrationTest framework for sample resource plugin testing ([#5322](https://github.com/opensearch-project/security/pull/5322))

src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import org.opensearch.test.framework.cluster.TestRestClient;
1717

1818
import static org.hamcrest.MatcherAssert.assertThat;
19-
import static org.hamcrest.core.Is.is;
2019
import static org.opensearch.security.api.PatchPayloadHelper.patch;
2120
import static org.opensearch.security.api.PatchPayloadHelper.replaceOp;
2221

@@ -93,18 +92,6 @@ private void verifyAuthInfoApi(final TestRestClient client) throws Exception {
9392

9493
}
9594

96-
@Test
97-
public void flushCache() throws Exception {
98-
withUser(NEW_USER, client -> { forbidden(() -> client.delete(apiPath("cache"))); });
99-
withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> {
100-
methodNotAllowed(() -> client.get(apiPath("cache")));
101-
methodNotAllowed(() -> client.postJson(apiPath("cache"), EMPTY_BODY));
102-
methodNotAllowed(() -> client.putJson(apiPath("cache"), EMPTY_BODY));
103-
final var response = ok(() -> client.delete(apiPath("cache")));
104-
assertThat(response.getBody(), response.getTextFromJsonBody("/message"), is("Cache flushed successfully."));
105-
});
106-
}
107-
10895
@Test
10996
public void reloadSSLCertsNotAvailable() throws Exception {
11097
withUser(NEW_USER, client -> {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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.api;
13+
14+
import org.junit.Test;
15+
16+
import static org.hamcrest.CoreMatchers.is;
17+
import static org.hamcrest.MatcherAssert.assertThat;
18+
import static org.opensearch.security.dlic.rest.support.Utils.PLUGINS_PREFIX;
19+
20+
public class FlushCacheApiIntegrationTest extends AbstractApiIntegrationTest {
21+
private final static String TEST_USER = "testuser";
22+
23+
private String cachePath() {
24+
return super.apiPath("cache");
25+
}
26+
27+
private String cachePath(String user) {
28+
return super.apiPath("cache", "user", user);
29+
}
30+
31+
@Override
32+
protected String apiPathPrefix() {
33+
return PLUGINS_PREFIX;
34+
}
35+
36+
@Test
37+
public void testFlushCache() throws Exception {
38+
withUser(NEW_USER, client -> {
39+
forbidden(() -> client.delete(cachePath()));
40+
forbidden(() -> client.delete(cachePath(TEST_USER)));
41+
});
42+
withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> {
43+
methodNotAllowed(() -> client.get(cachePath()));
44+
methodNotAllowed(() -> client.postJson(cachePath(), EMPTY_BODY));
45+
methodNotAllowed(() -> client.putJson(cachePath(), EMPTY_BODY));
46+
final var deleteAllCacheResponse = ok(() -> client.delete(cachePath()));
47+
assertThat(
48+
deleteAllCacheResponse.getBody(),
49+
deleteAllCacheResponse.getTextFromJsonBody("/message"),
50+
is("Cache flushed successfully.")
51+
);
52+
final var deleteUserCacheResponse = ok(() -> client.delete(cachePath(TEST_USER)));
53+
assertThat(
54+
deleteUserCacheResponse.getBody(),
55+
deleteUserCacheResponse.getTextFromJsonBody("/message"),
56+
is("Cache invalidated for user: " + TEST_USER)
57+
);
58+
});
59+
}
60+
}

src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class DirectoryInformationTrees {
3838

3939
public static final String CN_GROUP_ADMIN = "admin";
4040
public static final String CN_GROUP_CREW = "crew";
41+
public static final String CN_GROUP_ENTERPRISE = "enterprise";
4142
public static final String CN_GROUP_BRIDGE = "bridge";
4243

4344
public static final String USER_SEARCH = "(uid={0})";
@@ -120,4 +121,87 @@ class DirectoryInformationTrees {
120121
.classes("groupofuniquenames", "top")
121122
.buildRecord()
122123
.buildLdif();
124+
125+
static final LdifData LDIF_DATA_UPDATED_BACKEND_ROLES = new LdifBuilder().root("o=test.org")
126+
.dc("TEST")
127+
.classes("top", "domain")
128+
.newRecord(DN_PEOPLE_TEST_ORG)
129+
.ou("people")
130+
.classes("organizationalUnit", "top")
131+
.newRecord(DN_OPEN_SEARCH_PEOPLE_TEST_ORG)
132+
.classes("inetOrgPerson")
133+
.cn("Open Search")
134+
.sn("Search")
135+
.uid(USER_OPENS)
136+
.userPassword(PASSWORD_OPEN_SEARCH)
137+
138+
.ou("Human Resources")
139+
.newRecord(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG)
140+
.classes("inetOrgPerson")
141+
.cn("Captain Spock")
142+
.sn(USER_SPOCK)
143+
.uid(USER_SPOCK)
144+
.userPassword(PASSWORD_SPOCK)
145+
146+
.ou("Human Resources")
147+
.newRecord(DN_KIRK_PEOPLE_TEST_ORG)
148+
.classes("inetOrgPerson")
149+
.cn("Kirk")
150+
.sn("Kirk")
151+
.uid(USER_KIRK)
152+
.userPassword(PASSWORD_KIRK)
153+
154+
.ou("Human Resources")
155+
.newRecord(DN_CHRISTPHER_PEOPLE_TEST_ORG)
156+
.classes("inetOrgPerson")
157+
.cn("Christpher")
158+
.sn("Christpher")
159+
.uid("christpher")
160+
.userPassword(PASSWORD_CHRISTPHER)
161+
162+
.ou("Human Resources")
163+
.newRecord(DN_LEONARD_PEOPLE_TEST_ORG)
164+
.classes("inetOrgPerson")
165+
.cn("Leonard")
166+
.sn("Leonard")
167+
.uid(USER_LEONARD)
168+
.userPassword(PASSWORD_LEONARD)
169+
170+
.ou("Human Resources")
171+
.newRecord(DN_JEAN_PEOPLE_TEST_ORG)
172+
.classes("inetOrgPerson")
173+
.cn("Jean")
174+
.sn("Jean")
175+
.uid(USER_JEAN)
176+
.userPassword(PASSWORD_JEAN)
177+
178+
.ou("Human Resources")
179+
.newRecord(DN_GROUPS_TEST_ORG)
180+
.ou("groups")
181+
.cn("groupsRoot")
182+
.classes("groupofuniquenames", "top")
183+
.newRecord("cn=admin,ou=groups,o=test.org")
184+
.ou("groups")
185+
.cn(CN_GROUP_ADMIN)
186+
.uniqueMember(DN_KIRK_PEOPLE_TEST_ORG)
187+
.classes("groupofuniquenames", "top")
188+
.newRecord("cn=crew,ou=groups,o=test.org")
189+
.ou("groups")
190+
.cn(CN_GROUP_CREW)
191+
.uniqueMember(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG)
192+
.uniqueMember(DN_CHRISTPHER_PEOPLE_TEST_ORG)
193+
.uniqueMember(DN_BRIDGE_GROUPS_TEST_ORG)
194+
.classes("groupofuniquenames", "top")
195+
.newRecord("cn=enterprise,ou=groups,o=test.org")
196+
.cn(CN_GROUP_ENTERPRISE)
197+
.uniqueMember(DN_KIRK_PEOPLE_TEST_ORG)
198+
.uniqueMember(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG)
199+
.classes("groupofuniquenames", "top")
200+
.newRecord(DN_BRIDGE_GROUPS_TEST_ORG)
201+
.ou("groups")
202+
.cn(CN_GROUP_BRIDGE)
203+
.uniqueMember(DN_JEAN_PEOPLE_TEST_ORG)
204+
.classes("groupofuniquenames", "top")
205+
.buildRecord()
206+
.buildLdif();
123207
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* The OpenSearch Contributors require contributions made to
6+
* this file be licensed under the Apache-2.0 license or a
7+
* compatible open source license.
8+
*
9+
*/
10+
package org.opensearch.security.http;
11+
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
16+
import org.junit.ClassRule;
17+
import org.junit.Rule;
18+
import org.junit.Test;
19+
import org.junit.rules.RuleChain;
20+
import org.junit.runner.RunWith;
21+
22+
import org.opensearch.security.support.ConfigConstants;
23+
import org.opensearch.test.framework.AuthorizationBackend;
24+
import org.opensearch.test.framework.AuthzDomain;
25+
import org.opensearch.test.framework.LdapAuthenticationConfigBuilder;
26+
import org.opensearch.test.framework.LdapAuthorizationConfigBuilder;
27+
import org.opensearch.test.framework.TestSecurityConfig;
28+
import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain;
29+
import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend;
30+
import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator;
31+
import org.opensearch.test.framework.certificate.TestCertificates;
32+
import org.opensearch.test.framework.cluster.ClusterManager;
33+
import org.opensearch.test.framework.cluster.LocalCluster;
34+
import org.opensearch.test.framework.cluster.TestRestClient;
35+
import org.opensearch.test.framework.ldap.EmbeddedLDAPServer;
36+
import org.opensearch.test.framework.log.LogsRule;
37+
38+
import static org.hamcrest.CoreMatchers.not;
39+
import static org.hamcrest.MatcherAssert.assertThat;
40+
import static org.hamcrest.Matchers.contains;
41+
import static org.opensearch.security.http.DirectoryInformationTrees.CN_GROUP_ADMIN;
42+
import static org.opensearch.security.http.DirectoryInformationTrees.DN_GROUPS_TEST_ORG;
43+
import static org.opensearch.security.http.DirectoryInformationTrees.DN_OPEN_SEARCH_PEOPLE_TEST_ORG;
44+
import static org.opensearch.security.http.DirectoryInformationTrees.DN_PEOPLE_TEST_ORG;
45+
import static org.opensearch.security.http.DirectoryInformationTrees.LDIF_DATA;
46+
import static org.opensearch.security.http.DirectoryInformationTrees.LDIF_DATA_UPDATED_BACKEND_ROLES;
47+
import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_KIRK;
48+
import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_OPEN_SEARCH;
49+
import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_SPOCK;
50+
import static org.opensearch.security.http.DirectoryInformationTrees.USERNAME_ATTRIBUTE;
51+
import static org.opensearch.security.http.DirectoryInformationTrees.USER_KIRK;
52+
import static org.opensearch.security.http.DirectoryInformationTrees.USER_SEARCH;
53+
import static org.opensearch.security.http.DirectoryInformationTrees.USER_SPOCK;
54+
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED;
55+
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED;
56+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
57+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER;
58+
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
59+
60+
/**
61+
* Test uses plain (non TLS) connection between OpenSearch and LDAP server.
62+
*/
63+
@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
64+
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
65+
public class LdapAuthenticationCacheTest {
66+
67+
private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS);
68+
69+
private static final TestCertificates TEST_CERTIFICATES = new TestCertificates();
70+
71+
public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer(
72+
TEST_CERTIFICATES.getRootCertificateData(),
73+
TEST_CERTIFICATES.getLdapCertificateData(),
74+
LDIF_DATA
75+
);
76+
77+
public static LocalCluster cluster = new LocalCluster.Builder().testCertificates(TEST_CERTIFICATES)
78+
.clusterManager(ClusterManager.SINGLENODE)
79+
.anonymousAuth(false)
80+
.nodeSettings(
81+
Map.of(
82+
ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "." + ADMIN_USER.getName(),
83+
List.of(USER_KIRK),
84+
SECURITY_RESTAPI_ROLES_ENABLED,
85+
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
86+
SECURITY_RESTAPI_ADMIN_ENABLED,
87+
true
88+
)
89+
)
90+
.authc(
91+
new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true).httpAuthenticator(new HttpAuthenticator("basic").challenge(false))
92+
.backend(
93+
new AuthenticationBackend("ldap").config(
94+
() -> LdapAuthenticationConfigBuilder.config()
95+
// this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used to
96+
// postpone
97+
// execution of the code in this block.
98+
.enableSsl(false)
99+
.enableStartTls(false)
100+
.hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort()))
101+
.bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG)
102+
.password(PASSWORD_OPEN_SEARCH)
103+
.userBase(DN_PEOPLE_TEST_ORG)
104+
.userSearch(USER_SEARCH)
105+
.usernameAttribute(USERNAME_ATTRIBUTE)
106+
.build()
107+
)
108+
)
109+
)
110+
.authc(AUTHC_HTTPBASIC_INTERNAL)
111+
.users(ADMIN_USER)
112+
.rolesMapping(new TestSecurityConfig.RoleMapping(ALL_ACCESS.getName()).backendRoles(CN_GROUP_ADMIN))
113+
.authz(
114+
new AuthzDomain("ldap_roles").httpEnabled(true)
115+
.authorizationBackend(
116+
new AuthorizationBackend("ldap").config(
117+
() -> new LdapAuthorizationConfigBuilder().hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort()))
118+
.enableSsl(false)
119+
.bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG)
120+
.password(PASSWORD_OPEN_SEARCH)
121+
.userBase(DN_PEOPLE_TEST_ORG)
122+
.userSearch(USER_SEARCH)
123+
.usernameAttribute(USERNAME_ATTRIBUTE)
124+
.roleBase(DN_GROUPS_TEST_ORG)
125+
.roleSearch("(uniqueMember={0})")
126+
.userRoleAttribute(null)
127+
.userRoleName("disabled")
128+
.roleName("cn")
129+
.resolveNestedRoles(true)
130+
.build()
131+
)
132+
)
133+
)
134+
.build();
135+
136+
@ClassRule
137+
public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster);
138+
139+
@Rule
140+
public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend");
141+
142+
@Test
143+
public void shouldAuthenticateUserWithLdap_positive() {
144+
try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) {
145+
TestRestClient.HttpResponse response = client.getAuthInfo();
146+
147+
response.assertStatusCode(200);
148+
149+
assertThat(response.getTextArrayFromJsonBody("/backend_roles"), contains("crew"));
150+
assertThat(response.getTextArrayFromJsonBody("/backend_roles"), not(contains("enterprise")));
151+
}
152+
153+
try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) {
154+
TestRestClient.HttpResponse response = client.getAuthInfo();
155+
156+
response.assertStatusCode(200);
157+
158+
assertThat(response.getTextArrayFromJsonBody("/backend_roles"), contains("admin"));
159+
assertThat(response.getTextArrayFromJsonBody("/backend_roles"), not(contains("enterprise")));
160+
}
161+
162+
embeddedLDAPServer.loadLdifData(LDIF_DATA_UPDATED_BACKEND_ROLES);
163+
164+
try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) {
165+
TestRestClient.HttpResponse response = client.delete("_plugins/_security/api/cache/user/spock");
166+
167+
response.assertStatusCode(200);
168+
}
169+
170+
try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) {
171+
TestRestClient.HttpResponse response = client.getAuthInfo();
172+
173+
response.assertStatusCode(200);
174+
175+
assertThat(response.getTextArrayFromJsonBody("/backend_roles"), contains("enterprise", "crew"));
176+
}
177+
178+
try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) {
179+
TestRestClient.HttpResponse response = client.getAuthInfo();
180+
181+
response.assertStatusCode(200);
182+
183+
assertThat(response.getTextArrayFromJsonBody("/backend_roles"), contains("admin"));
184+
assertThat(response.getTextArrayFromJsonBody("/backend_roles"), not(contains("enterprise")));
185+
}
186+
}
187+
}

src/integrationTest/java/org/opensearch/test/framework/ldap/EmbeddedLDAPServer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ protected void after() {
4646
}
4747
}
4848

49+
public void loadLdifData(LdifData ldifData) {
50+
try {
51+
server.loadLdifData(ldifData);
52+
} catch (Exception e) {
53+
throw new RuntimeException("Cannot reload LDIF data.", e);
54+
}
55+
}
56+
4957
public int getLdapNonTlsPort() {
5058
return server.getLdapNonTlsPort();
5159
}

0 commit comments

Comments
 (0)