diff --git a/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java index f3b701dc38..7ac2262899 100644 --- a/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java @@ -16,7 +16,6 @@ import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; import static org.opensearch.security.api.PatchPayloadHelper.patch; import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; @@ -93,23 +92,6 @@ private void verifyAuthInfoApi(final TestRestClient client) throws Exception { } - @Test - public void flushCache() throws Exception { - withUser(NEW_USER, client -> { - forbidden(() -> client.get(apiPath("cache"))); - forbidden(() -> client.postJson(apiPath("cache"), EMPTY_BODY)); - forbidden(() -> client.putJson(apiPath("cache"), EMPTY_BODY)); - forbidden(() -> client.delete(apiPath("cache"))); - }); - withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { - notImplemented(() -> client.get(apiPath("cache"))); - notImplemented(() -> client.postJson(apiPath("cache"), EMPTY_BODY)); - notImplemented(() -> client.putJson(apiPath("cache"), EMPTY_BODY)); - final var response = ok(() -> client.delete(apiPath("cache"))); - assertThat(response.getBody(), response.getTextFromJsonBody("/message"), is("Cache flushed successfully.")); - }); - } - @Test public void reloadSSLCertsNotAvailable() throws Exception { withUser(NEW_USER, client -> { diff --git a/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java new file mode 100644 index 0000000000..048078badb --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/FlushCacheApiIntegrationTest.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.api; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class FlushCacheApiIntegrationTest extends AbstractApiIntegrationTest { + private final static String TEST_USER = "testuser"; + + private String cachePath() { + return super.apiPath("cache"); + } + + private String cachePath(String user) { + return super.apiPath("cache", "user", user); + } + + @Test + public void testFlushCache() throws Exception { + withUser(NEW_USER, client -> { + forbidden(() -> client.get(cachePath())); + forbidden(() -> client.postJson(cachePath(), EMPTY_BODY)); + forbidden(() -> client.putJson(cachePath(), EMPTY_BODY)); + forbidden(() -> client.delete(cachePath())); + forbidden(() -> client.delete(cachePath(TEST_USER))); + }); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { + notImplemented(() -> client.get(cachePath())); + notImplemented(() -> client.postJson(cachePath(), EMPTY_BODY)); + notImplemented(() -> client.putJson(cachePath(), EMPTY_BODY)); + final var deleteAllCacheResponse = ok(() -> client.delete(cachePath())); + assertThat( + deleteAllCacheResponse.getBody(), + deleteAllCacheResponse.getTextFromJsonBody("/message"), + is("Cache flushed successfully.") + ); + final var deleteUserCacheResponse = ok(() -> client.delete(cachePath(TEST_USER))); + assertThat( + deleteUserCacheResponse.getBody(), + deleteAllCacheResponse.getTextFromJsonBody("/message"), + is("Cache invalidated for user: " + TEST_USER) + ); + }); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java b/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java index 3f9c220923..bf8144d0bd 100644 --- a/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java +++ b/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java @@ -38,6 +38,7 @@ class DirectoryInformationTrees { public static final String CN_GROUP_ADMIN = "admin"; public static final String CN_GROUP_CREW = "crew"; + public static final String CN_GROUP_ENTERPRISE = "enterprise"; public static final String CN_GROUP_BRIDGE = "bridge"; public static final String USER_SEARCH = "(uid={0})"; @@ -120,4 +121,87 @@ class DirectoryInformationTrees { .classes("groupofuniquenames", "top") .buildRecord() .buildLdif(); + + static final LdifData LDIF_DATA_UPDATED_BACKEND_ROLES = new LdifBuilder().root("o=test.org") + .dc("TEST") + .classes("top", "domain") + .newRecord(DN_PEOPLE_TEST_ORG) + .ou("people") + .classes("organizationalUnit", "top") + .newRecord(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Open Search") + .sn("Search") + .uid(USER_OPENS) + .userPassword(PASSWORD_OPEN_SEARCH) + .mail("open.search@example.com") + .ou("Human Resources") + .newRecord(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Captain Spock") + .sn(USER_SPOCK) + .uid(USER_SPOCK) + .userPassword(PASSWORD_SPOCK) + .mail("spock@example.com") + .ou("Human Resources") + .newRecord(DN_KIRK_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Kirk") + .sn("Kirk") + .uid(USER_KIRK) + .userPassword(PASSWORD_KIRK) + .mail("spock@example.com") + .ou("Human Resources") + .newRecord(DN_CHRISTPHER_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Christpher") + .sn("Christpher") + .uid("christpher") + .userPassword(PASSWORD_CHRISTPHER) + .mail("christpher@example.com") + .ou("Human Resources") + .newRecord(DN_LEONARD_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Leonard") + .sn("Leonard") + .uid(USER_LEONARD) + .userPassword(PASSWORD_LEONARD) + .mail("leonard@example.com") + .ou("Human Resources") + .newRecord(DN_JEAN_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Jean") + .sn("Jean") + .uid(USER_JEAN) + .userPassword(PASSWORD_JEAN) + .mail("jean@example.com") + .ou("Human Resources") + .newRecord(DN_GROUPS_TEST_ORG) + .ou("groups") + .cn("groupsRoot") + .classes("groupofuniquenames", "top") + .newRecord("cn=admin,ou=groups,o=test.org") + .ou("groups") + .cn(CN_GROUP_ADMIN) + .uniqueMember(DN_KIRK_PEOPLE_TEST_ORG) + .classes("groupofuniquenames", "top") + .newRecord("cn=crew,ou=groups,o=test.org") + .ou("groups") + .cn(CN_GROUP_CREW) + .uniqueMember(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .uniqueMember(DN_CHRISTPHER_PEOPLE_TEST_ORG) + .uniqueMember(DN_BRIDGE_GROUPS_TEST_ORG) + .classes("groupofuniquenames", "top") + .newRecord("cn=enterprise,ou=groups,o=test.org") + .cn(CN_GROUP_ENTERPRISE) + .uniqueMember(DN_KIRK_PEOPLE_TEST_ORG) + .uniqueMember(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .classes("groupofuniquenames", "top") + .newRecord(DN_BRIDGE_GROUPS_TEST_ORG) + .ou("groups") + .cn(CN_GROUP_BRIDGE) + .uniqueMember(DN_JEAN_PEOPLE_TEST_ORG) + .classes("groupofuniquenames", "top") + .buildRecord() + .buildLdif(); } diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationCacheTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationCacheTest.java new file mode 100644 index 0000000000..24411e3a07 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationCacheTest.java @@ -0,0 +1,188 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security.http; + +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.AuthorizationBackend; +import org.opensearch.test.framework.AuthzDomain; +import org.opensearch.test.framework.LdapAuthenticationConfigBuilder; +import org.opensearch.test.framework.LdapAuthorizationConfigBuilder; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; +import org.opensearch.test.framework.TestSecurityConfig.RoleMapping; +import org.opensearch.test.framework.certificate.TestCertificates; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.ldap.EmbeddedLDAPServer; +import org.opensearch.test.framework.log.LogsRule; + +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.opensearch.security.http.DirectoryInformationTrees.CN_GROUP_ADMIN; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_GROUPS_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_OPEN_SEARCH_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.LDIF_DATA; +import static org.opensearch.security.http.DirectoryInformationTrees.LDIF_DATA_UPDATED_BACKEND_ROLES; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_KIRK; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_OPEN_SEARCH; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_SPOCK; +import static org.opensearch.security.http.DirectoryInformationTrees.USERNAME_ATTRIBUTE; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_KIRK; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_SEARCH; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_SPOCK; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +/** +* Test uses plain (non TLS) connection between OpenSearch and LDAP server. +*/ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class LdapAuthenticationCacheTest { + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + + public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer( + TEST_CERTIFICATES.getRootCertificateData(), + TEST_CERTIFICATES.getLdapCertificateData(), + LDIF_DATA + ); + + public static LocalCluster cluster = new LocalCluster.Builder().testCertificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .nodeSettings( + Map.of( + ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "." + ADMIN_USER.getName(), + List.of(USER_KIRK), + SECURITY_RESTAPI_ROLES_ENABLED, + List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_RESTAPI_ADMIN_ENABLED, + true + ) + ) + .authc( + new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true).httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) + .backend( + new AuthenticationBackend("ldap").config( + () -> LdapAuthenticationConfigBuilder.config() + // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used to + // postpone + // execution of the code in this block. + .enableSsl(false) + .enableStartTls(false) + .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort())) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .build() + ) + ) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .rolesMapping(new RoleMapping(ALL_ACCESS.getName()).backendRoles(CN_GROUP_ADMIN)) + .authz( + new AuthzDomain("ldap_roles").httpEnabled(true) + .authorizationBackend( + new AuthorizationBackend("ldap").config( + () -> new LdapAuthorizationConfigBuilder().hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort())) + .enableSsl(false) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .roleBase(DN_GROUPS_TEST_ORG) + .roleSearch("(uniqueMember={0})") + .userRoleAttribute(null) + .userRoleName("disabled") + .roleName("cn") + .resolveNestedRoles(true) + .build() + ) + ) + ) + .build(); + + @ClassRule + public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); + + @Test + public void shouldAuthenticateUserWithLdap_positive() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + + assertThat(response.getTextArrayFromJsonBody("/backend_roles"), contains("crew")); + assertThat(response.getTextArrayFromJsonBody("/backend_roles"), not(contains("enterprise"))); + } + + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + + assertThat(response.getTextArrayFromJsonBody("/backend_roles"), contains("admin")); + assertThat(response.getTextArrayFromJsonBody("/backend_roles"), not(contains("enterprise"))); + } + + embeddedLDAPServer.loadLdifData(LDIF_DATA_UPDATED_BACKEND_ROLES); + + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + TestRestClient.HttpResponse response = client.delete("_plugins/_security/api/cache/user/spock"); + + response.assertStatusCode(200); + } + + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + + assertThat(response.getTextArrayFromJsonBody("/backend_roles"), contains("enterprise", "crew")); + } + + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + + assertThat(response.getTextArrayFromJsonBody("/backend_roles"), contains("admin")); + assertThat(response.getTextArrayFromJsonBody("/backend_roles"), not(contains("enterprise"))); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/ldap/EmbeddedLDAPServer.java b/src/integrationTest/java/org/opensearch/test/framework/ldap/EmbeddedLDAPServer.java index 583a0cdaeb..0a1d6a658a 100755 --- a/src/integrationTest/java/org/opensearch/test/framework/ldap/EmbeddedLDAPServer.java +++ b/src/integrationTest/java/org/opensearch/test/framework/ldap/EmbeddedLDAPServer.java @@ -46,6 +46,14 @@ protected void after() { } } + public void loadLdifData(LdifData ldifData) { + try { + server.loadLdifData(ldifData); + } catch (Exception e) { + throw new RuntimeException("Cannot reload LDIF data.", e); + } + } + public int getLdapNonTlsPort() { return server.getLdapNonTlsPort(); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/ldap/LdapServer.java b/src/integrationTest/java/org/opensearch/test/framework/ldap/LdapServer.java index dece74f1e5..7cd7ebecd9 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/ldap/LdapServer.java +++ b/src/integrationTest/java/org/opensearch/test/framework/ldap/LdapServer.java @@ -212,7 +212,8 @@ public void stop() throws InterruptedException { } } - private void loadLdifData(LdifData ldifData) throws Exception { + public void loadLdifData(LdifData ldifData) throws Exception { + server.clear(); try (LDIFReader r = new LDIFReader(new BufferedReader(new StringReader(ldifData.getContent())))) { Entry entry; while ((entry = r.readEntry()) != null) { diff --git a/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequest.java b/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequest.java index d4e860569d..3a83fb551a 100644 --- a/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequest.java +++ b/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequest.java @@ -37,9 +37,12 @@ public class ConfigUpdateRequest extends BaseNodesRequest { private String[] configTypes; + private String[] entityNames; + public ConfigUpdateRequest(StreamInput in) throws IOException { super(in); this.configTypes = in.readStringArray(); + this.entityNames = in.readOptionalStringArray(); } public ConfigUpdateRequest() { @@ -51,10 +54,17 @@ public ConfigUpdateRequest(String[] configTypes) { setConfigTypes(configTypes); } + public ConfigUpdateRequest(String configType, String[] entityNames) { + this(); + setConfigTypes(new String[] { configType }); + setEntityNames(entityNames); + } + @Override public void writeTo(final StreamOutput out) throws IOException { super.writeTo(out); out.writeStringArray(configTypes); + out.writeOptionalStringArray(entityNames); } public String[] getConfigTypes() { @@ -65,10 +75,20 @@ public void setConfigTypes(final String[] configTypes) { this.configTypes = configTypes; } + public String[] getEntityNames() { + return entityNames; + } + + public void setEntityNames(final String[] entityNames) { + this.entityNames = entityNames; + } + @Override public ActionRequestValidationException validate() { if (configTypes == null || configTypes.length == 0) { return new ActionRequestValidationException(); + } else if (configTypes.length > 1 && (entityNames != null && entityNames.length > 1)) { + return new ActionRequestValidationException(); } return null; } diff --git a/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java b/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java index 6f1f99a434..7fb3f13298 100644 --- a/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java +++ b/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java @@ -27,6 +27,7 @@ package org.opensearch.security.action.configupdate; import java.io.IOException; +import java.util.Arrays; import java.util.List; import org.apache.logging.log4j.LogManager; @@ -125,8 +126,14 @@ protected ConfigUpdateResponse newResponse( @Override protected ConfigUpdateNodeResponse nodeOperation(final NodeConfigUpdateRequest request) { - boolean didReload = configurationRepository.reloadConfiguration(CType.fromStringValues((request.request.getConfigTypes()))); - if (didReload) { + if (request.request.getConfigTypes() != null + && request.request.getEntityNames() != null + && request.request.getConfigTypes().length == 1 + && Arrays.asList(request.request.getConfigTypes()).contains(CType.INTERNALUSERS.toLCString()) + && request.request.getEntityNames().length > 0) { + backendRegistry.get().invalidateUserCache(request.request.getEntityNames()); + } else { + configurationRepository.reloadConfiguration(CType.fromStringValues((request.request.getConfigTypes()))); backendRegistry.get().invalidateCache(); } return new ConfigUpdateNodeResponse(clusterService.localNode(), request.request.getConfigTypes(), null); diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 97c060be35..2fd9a40209 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -28,6 +28,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -168,6 +169,31 @@ public void invalidateCache() { restRoleCache.invalidateAll(); } + public void invalidateUserCache(String[] usernames) { + if (usernames == null || usernames.length == 0) { + log.debug("No usernames given, not invalidating user cache."); + return; + } + + List usernamesAsList = Arrays.asList(usernames); + + // Invalidate entries in the userCache by iterating over the keys and matching the username. + userCache.asMap() + .keySet() + .stream() + .filter(authCreds -> usernamesAsList.contains(authCreds.getUsername())) + .forEach(userCache::invalidate); + + // Invalidate entries in the restImpersonationCache directly since it uses the username as the key. + restImpersonationCache.invalidateAll(usernamesAsList); + + // Invalidate entries in the restRoleCache by iterating over the keys and matching the username. + restRoleCache.asMap().keySet().stream().filter(user -> usernamesAsList.contains(user.getName())).forEach(restRoleCache::invalidate); + + // If the user isn't found it still says this, which could be bad + log.debug("Invalidated cache for users {}", String.join(", ", usernames)); + } + @Subscribe public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java index 2f579ecbd9..e7f0f23433 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java @@ -41,7 +41,8 @@ public class FlushCacheApiAction extends AbstractApiAction { new Route(Method.DELETE, "/cache"), new Route(Method.GET, "/cache"), new Route(Method.PUT, "/cache"), - new Route(Method.POST, "/cache") + new Route(Method.POST, "/cache"), + new Route(Method.DELETE, "/cache/user/{username}") ) ); @@ -61,37 +62,44 @@ public List routes() { } private void flushCacheApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { - requestHandlersBuilder.allMethodsNotImplemented() - .override( - Method.DELETE, - (channel, request, client) -> client.execute( - ConfigUpdateAction.INSTANCE, - new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0])), - new ActionListener<>() { - - @Override - public void onResponse(ConfigUpdateResponse configUpdateResponse) { - if (configUpdateResponse.hasFailures()) { - LOGGER.error("Cannot flush cache due to", configUpdateResponse.failures().get(0)); - internalServerError( - channel, - "Cannot flush cache due to " + configUpdateResponse.failures().get(0).getMessage() + "." - ); - return; - } - LOGGER.debug("cache flushed successfully"); - ok(channel, "Cache flushed successfully."); - } - - @Override - public void onFailure(final Exception e) { - LOGGER.error("Cannot flush cache due to", e); - internalServerError(channel, "Cannot flush cache due to " + e.getMessage() + "."); - } - + requestHandlersBuilder.allMethodsNotImplemented().override(Method.DELETE, (channel, request, client) -> { + final ConfigUpdateRequest configUpdateRequest; + if (request.path().contains("/user/")) { + // Extract the username from the request + final String username = request.param("username"); + if (username == null || username.isEmpty()) { + internalServerError(channel, "No username provided for cache invalidation."); + return; + } + // Validate and handle user-specific cache invalidation + configUpdateRequest = new ConfigUpdateRequest(CType.INTERNALUSERS.toLCString(), new String[] { username }); + } else { + configUpdateRequest = new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0])); + } + client.execute(ConfigUpdateAction.INSTANCE, configUpdateRequest, new ActionListener<>() { + + @Override + public void onResponse(ConfigUpdateResponse configUpdateResponse) { + if (configUpdateResponse.hasFailures()) { + LOGGER.error("Cannot flush cache due to", configUpdateResponse.failures().get(0)); + internalServerError( + channel, + "Cannot flush cache due to " + configUpdateResponse.failures().get(0).getMessage() + "." + ); + return; } - ) - ); + LOGGER.debug("cache flushed successfully"); + ok(channel, "Cache flushed successfully."); + } + + @Override + public void onFailure(final Exception e) { + LOGGER.error("Cannot flush cache due to", e); + internalServerError(channel, "Cannot flush cache due to " + e.getMessage() + "."); + } + + }); + }); } @Override @@ -101,6 +109,8 @@ protected CType getConfigType() { @Override protected void consumeParameters(final RestRequest request) { - // not needed + if (request.path().contains("/user/")) { + request.param("username"); + } } }