Skip to content

Commit

Permalink
add flush cache for individual user
Browse files Browse the repository at this point in the history
Signed-off-by: David Lin <dlin2028@gmail.com>
  • Loading branch information
dlin2028 authored and prabhask5 committed Jul 16, 2024
1 parent cfb5525 commit ad255cb
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 13 deletions.
23 changes: 23 additions & 0 deletions src/main/java/org/opensearch/security/auth/BackendRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,29 @@ public void invalidateCache() {
restRoleCache.invalidateAll();
}

public void invalidateUserCache(String username) {
if (username == null || username.isEmpty()) {
log.debug("No username given, not invalidating user cache.");
return;
}

// Invalidate entries in the userCache by iterating over the keys and matching the username.
userCache.asMap().keySet().stream()
.filter(authCreds -> username.equals(authCreds.getUsername()))
.forEach(userCache::invalidate);

// Invalidate entries in the restImpersonationCache directly since it uses the username as the key.
restImpersonationCache.invalidate(username);

// Invalidate entries in the restRoleCache by iterating over the keys and matching the username.
restRoleCache.asMap().keySet().stream()
.filter(user -> username.equals(user.getName()))
.forEach(restRoleCache::invalidate);

// If the user isn't found it still says this, which could be bad
log.debug("Invalidated cache for user {}", username);
}

@Subscribe
public void onDynamicConfigModelChanged(DynamicConfigModel dcm) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.inject.Inject;
import org.opensearch.core.action.ActionListener;
import org.opensearch.rest.RestChannel;
import org.opensearch.rest.RestRequest;
import org.opensearch.rest.RestRequest.Method;
import org.opensearch.security.action.configupdate.ConfigUpdateAction;
Expand All @@ -41,7 +42,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}")
)
);

Expand All @@ -64,11 +66,33 @@ private void flushCacheApiRequestHandlers(RequestHandler.RequestHandlersBuilder
requestHandlersBuilder.allMethodsNotImplemented()
.override(
Method.DELETE,
(channel, request, client) -> client.execute(
ConfigUpdateAction.INSTANCE,
new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0])),
new ActionListener<>() {

(channel, request, client) -> {
if (request.path().contains("/user/")) {
// Extract the username from the request
final String username = request.param("username");
// Validate and handle user-specific cache invalidation
handleUserCacheInvalidation(channel, username);
}
else
{
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));
internalSeverError(
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 onResponse(ConfigUpdateResponse configUpdateResponse) {
if (configUpdateResponse.hasFailures()) {
Expand All @@ -83,17 +107,34 @@ public void onResponse(ConfigUpdateResponse configUpdateResponse) {
ok(channel, "Cache flushed successfully.");
}

@Override
public void onFailure(final Exception e) {
LOGGER.error("Cannot flush cache due to", e);
internalSeverError(channel, "Cannot flush cache due to " + e.getMessage() + ".");
}
@Override
public void onFailure(final Exception e) {
LOGGER.error("Cannot flush cache due to", e);
internalServerError(channel, "Cannot flush cache due to " + e.getMessage() + ".");
}

}
);
}
)
}
);
}

private void handleUserCacheInvalidation(RestChannel channel, String username) {
if (username == null || username.isEmpty()) {
internalSeverError(channel, "No username provided for cache invalidation.");
return;
}
// Use BackendRegistry's method to invalidate cache for the specific user
securityApiDependencies.backendRegistry().invalidateUserCache(username);
ok(channel, "Cache invalidated for user: " + username);
}

@Override
protected CType getConfigType() {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import org.opensearch.common.settings.Settings;
import org.opensearch.security.auditlog.AuditLog;
import org.opensearch.security.auth.BackendRegistry;
import org.opensearch.security.configuration.AdminDNs;
import org.opensearch.security.configuration.ConfigurationRepository;
import org.opensearch.security.privileges.PrivilegesEvaluator;
Expand All @@ -23,6 +24,7 @@ public class SecurityApiDependencies {
private final ConfigurationRepository configurationRepository;
private final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator;
private final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator;
private final BackendRegistry backendRegistry;
private final AuditLog auditLog;
private final Settings settings;

Expand All @@ -35,7 +37,8 @@ public SecurityApiDependencies(
final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator,
final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator,
final AuditLog auditLog,
final Settings settings
final Settings settings,
final BackendRegistry backendRegistry
) {
this.adminDNs = adminDNs;
this.configurationRepository = configurationRepository;
Expand All @@ -44,6 +47,7 @@ public SecurityApiDependencies(
this.restApiAdminPrivilegesEvaluator = restApiAdminPrivilegesEvaluator;
this.auditLog = auditLog;
this.settings = settings;
this.backendRegistry = backendRegistry;
}

public AdminDNs adminDNs() {
Expand Down Expand Up @@ -74,6 +78,10 @@ public Settings settings() {
return settings;
}

public BackendRegistry backendRegistry() {
return backendRegistry;
}

public String securityIndexName() {
return settings().get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.opensearch.rest.RestController;
import org.opensearch.rest.RestHandler;
import org.opensearch.security.auditlog.AuditLog;
import org.opensearch.security.auth.BackendRegistry;
import org.opensearch.security.configuration.AdminDNs;
import org.opensearch.security.configuration.ConfigurationRepository;
import org.opensearch.security.hasher.PasswordHasher;
Expand Down Expand Up @@ -63,7 +64,8 @@ public static Collection<RestHandler> getHandler(
settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false)
),
auditLog,
settings
settings,
backendRegistry
);
return List.of(
new InternalUsersApiAction(clusterService, threadPool, userService, securityApiDependencies, passwordHasher),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public void setup() {
null,
restApiAdminPrivilegesEvaluator,
null,
Settings.EMPTY
Settings.EMPTY,
null
);

passwordHasher = PasswordHasherFactory.createPasswordHasher(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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.dlic.rest.api;

import org.apache.hc.core5.http.Header;
import org.apache.http.HttpStatus;
import org.junit.Assert;
import org.junit.Test;

import org.opensearch.common.settings.Settings;
import org.opensearch.common.xcontent.XContentType;
import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse;

import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX;

public class FlushCacheApiTest extends AbstractRestApiUnitTest {
private final String ENDPOINT;

protected String getEndpointPrefix() {
return PLUGINS_PREFIX;
}

public FlushCacheApiTest() {
ENDPOINT = getEndpointPrefix() + "/api/cache";
}

@Test
public void testFlushCache() throws Exception {

setup();

// Only DELETE is allowed for flush cache
rh.keystore = "restapi/kirk-keystore.jks";
rh.sendAdminCertificate = true;

// Username to test cache invalidation
String username = "testuser";

// GET
HttpResponse response = rh.executeGetRequest(ENDPOINT);
Assert.assertEquals(HttpStatus.SC_NOT_IMPLEMENTED, response.getStatusCode());
Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build();
Assert.assertEquals(settings.get("message"), "Method GET not supported for this action.");

// PUT
response = rh.executePutRequest(ENDPOINT, "{}", new Header[0]);
Assert.assertEquals(HttpStatus.SC_NOT_IMPLEMENTED, response.getStatusCode());
settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build();
Assert.assertEquals(settings.get("message"), "Method PUT not supported for this action.");

// POST
response = rh.executePostRequest(ENDPOINT, "{}", new Header[0]);
Assert.assertEquals(HttpStatus.SC_NOT_IMPLEMENTED, response.getStatusCode());
settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build();
Assert.assertEquals(settings.get("message"), "Method POST not supported for this action.");

// DELETE
response = rh.executeDeleteRequest(ENDPOINT, new Header[0]);
Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode());
settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build();
Assert.assertEquals(settings.get("message"), "Cache flushed successfully.");

// DELETE request for a specific user's cache
String userEndpoint = ENDPOINT + "/user/" + username;
response = rh.executeDeleteRequest(userEndpoint, new Header[0]);
Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode());
settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build();
Assert.assertEquals(settings.get("message"), "Cache invalidated for user: " + username);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void accessHandlerForDefaultSettings() {
final var securityConfigApiAction = new SecurityConfigApiAction(
clusterService,
threadPool,
new SecurityApiDependencies(null, configurationRepository, null, null, restApiAdminPrivilegesEvaluator, null, Settings.EMPTY)
new SecurityApiDependencies(null, configurationRepository, null, null, restApiAdminPrivilegesEvaluator, null, Settings.EMPTY, null)
);
assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build()));
assertFalse(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.PUT).build()));
Expand All @@ -49,7 +49,8 @@ public void accessHandlerForUnsupportedSetting() {
null,
restApiAdminPrivilegesEvaluator,
null,
Settings.builder().put(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build()
Settings.builder().put(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build(),
null
)
);
assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build()));
Expand All @@ -70,7 +71,8 @@ public void accessHandlerForRestAdmin() {
null,
restApiAdminPrivilegesEvaluator,
null,
Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()
Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build(),
null
)
);
assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build()));
Expand Down

0 comments on commit ad255cb

Please sign in to comment.