Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[controller] Manual store deletion only can be done by specific user group. #1199

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ public interface AccessController {
*/
boolean isAllowlistUsers(X509Certificate clientCert, String resource, String method);

/**
* Check whether the client is the allowlist admin users for store deletion.
*
* @param clientCert the X509Certificate submitted by client
* @param resource the resource being requested;
* @param method the operation (GET, POST, ...) to perform against the resource
* @return true if the client is admin
*/
boolean isAllowlistUsersForStoreDeletion(X509Certificate clientCert, String resource, String method);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename to hasAccessToDelete?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, we could do that, but wondering why this naming is better?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels more concise to me, while conveying the same information


/**
* Get principal Id from client certificate.
* @param clientCert the X509Certificate submitted by client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ protected boolean isAllowListUser(Request request) {
return accessController.get().isAllowlistUsers(certificate, storeName, HTTP_GET);
}

/**
* Check whether the user is within the admin users allowlist for store deletion.
*/
protected boolean isAllowListUserForStoreDeletion(Request request) {
if (!isAclEnabled()) {
/**
* Grant access if it's not required to check ACL.
* {@link accessController} will be empty if ACL is not enabled.
*/
return true;
Comment on lines +158 to +162
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to do the opposite thing, right? If ACL is not enabled, do not allow the store deletion at all.

Besides, it's cleaner to check whether ACL is enabled in AdminSparkServer - do not register this API if ACL is not enabled:
if (sslEnabled) { httpService.post(DELETE_STORE.getPath(), new VeniceParentControllerRegionStateHandler(admin, storesRoutes.deleteStore(admin))); }
The spike will be fixing the test cases; let's see how difficult that is. WDYT?

}
X509Certificate certificate = getCertificate(request);

String storeName = request.queryParamOrDefault(NAME, STORE_UNKNOWN);
return accessController.get().isAllowlistUsersForStoreDeletion(certificate, storeName, HTTP_GET);
}

/**
* @return whether SSL is enabled
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,12 @@ public Route deleteStore(Admin admin) {
return new VeniceRouteHandler<TrackableControllerResponse>(TrackableControllerResponse.class) {
@Override
public void internalHandle(Request request, TrackableControllerResponse veniceResponse) {
// This is to limit the manual store deletion from admin tool without https to a specific allow list users.
if (!isSslEnabled()
&& !checkIsAllowListUser(request, veniceResponse, () -> isAllowListUserForStoreDeletion(request))) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fail the request and send bad request error code to user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkedIsAllowListUser will mark the request as Bad Request:

  protected boolean checkIsAllowListUser(
      Request request,
      ControllerResponse veniceResponse,
      BooleanSupplier isAllowListUser) {
    if (!isAllowListUser.getAsBoolean()) {
      veniceResponse.setError(ACL_CHECK_FAILURE_WARN_MESSAGE_PREFIX + request.url());
      veniceResponse.setErrorType(ErrorType.BAD_REQUEST);
      return false;
    }
    return true;
  }

}

// Only allow allowlist users to run this command
if (!checkIsAllowListUser(request, veniceResponse, () -> isAllowListUser(request))) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.linkedin.venice.controller.server;

import static com.linkedin.venice.HttpConstants.HTTP_GET;
import static com.linkedin.venice.VeniceConstants.CONTROLLER_SSL_CERTIFICATE_ATTRIBUTE_NAME;
import static com.linkedin.venice.exceptions.ErrorType.INCORRECT_CONTROLLER;
import static com.linkedin.venice.exceptions.ErrorType.STORE_NOT_FOUND;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

import com.linkedin.venice.acl.DynamicAccessController;
import com.linkedin.venice.controller.Admin;
import com.linkedin.venice.controller.VeniceHelixAdmin;
import com.linkedin.venice.controller.VeniceParentHelixAdmin;
Expand All @@ -21,10 +25,12 @@
import com.linkedin.venice.meta.ZKStore;
import com.linkedin.venice.pubsub.PubSubTopicRepository;
import com.linkedin.venice.utils.ObjectMapperFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.testng.Assert;
import org.testng.annotations.Test;
import spark.QueryParamsMap;
Expand Down Expand Up @@ -216,4 +222,43 @@ public void testGetStore() throws Exception {
StoreResponse.class);
Assert.assertEquals(response.getStore().getMaxRecordSizeBytes(), testMaxRecordSizeBytesValue);
}

@Test
public void testDeleteStoreAclChecking() throws Exception {
final Store testStore = new ZKStore(
TEST_STORE_NAME,
"owner",
System.currentTimeMillis(),
PersistenceType.IN_MEMORY,
RoutingStrategy.CONSISTENT_HASH,
ReadStrategy.ANY_OF_ONLINE,
OfflinePushStrategy.WAIT_N_MINUS_ONE_REPLCIA_PER_PARTITION,
1);

final int testMaxRecordSizeBytesValue = 33333;
final Admin mockAdmin = mock(VeniceParentHelixAdmin.class);
doReturn(true).when(mockAdmin).isLeaderControllerFor(TEST_CLUSTER);
doReturn(testStore).when(mockAdmin).getStore(TEST_CLUSTER, TEST_STORE_NAME);

final Request request = mock(Request.class);
doReturn(TEST_CLUSTER).when(request).queryParams(eq(ControllerApiConstants.CLUSTER));
doReturn(TEST_STORE_NAME).when(request).queryParamOrDefault(eq(ControllerApiConstants.NAME), eq("STORE_UNKNOWN"));
HttpServletRequest rawRequest = mock(HttpServletRequest.class);
doReturn(rawRequest).when(request).raw();
X509Certificate[] certificates = new X509Certificate[1];
doReturn(certificates).when(rawRequest).getAttribute(eq(CONTROLLER_SSL_CERTIFICATE_ATTRIBUTE_NAME));

DynamicAccessController accessController = mock(DynamicAccessController.class);

doReturn(false).when(accessController).isAllowlistUsersForStoreDeletion(any(), eq(TEST_STORE_NAME), eq(HTTP_GET));

final StoresRoutes storesRoutes = new StoresRoutes(false, Optional.of(accessController), pubSubTopicRepository);
final StoreResponse response = ObjectMapperFactory.getInstance()
.readValue(
storesRoutes.deleteStore(mockAdmin).handle(request, mock(Response.class)).toString(),
StoreResponse.class);
Assert.assertTrue(response.isError());
Assert.assertTrue(response.getError().startsWith(VeniceRouteHandler.ACL_CHECK_FAILURE_WARN_MESSAGE_PREFIX));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ public boolean isAllowlistUsers(X509Certificate clientCert, String resource, Str
return true;
}

@Override
public boolean isAllowlistUsersForStoreDeletion(X509Certificate clientCert, String resource, String method) {
return isAllowlistUsers(clientCert, resource, method);
}

@Override
public String getPrincipalId(X509Certificate clientCert) {
assertNotNull(clientCert, resourceType.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ public boolean isAllowlistUsers(X509Certificate clientCert, String resource, Str
return true;
}

@Override
public boolean isAllowlistUsersForStoreDeletion(X509Certificate clientCert, String resource, String method) {
return this.isAllowlistUsers(clientCert, resource, method);
}

@Override
public String getPrincipalId(X509Certificate clientCert) {
assertNotNull(clientCert, queryAction.toString());
Expand Down
Loading