blob: b98557e4efd5cbf79bcb4a41e052ae0351d06987 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.geode.internal.protocol.protobuf.v1;
import static org.apache.geode.test.awaitility.GeodeAwaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.RestoreSystemProperties;
import org.junit.experimental.categories.Category;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.internal.AvailablePortHelper;
import org.apache.geode.internal.protocol.protobuf.v1.serializer.ProtobufProtocolSerializer;
import org.apache.geode.internal.protocol.protobuf.v1.serializer.exception.InvalidProtocolMessageException;
import org.apache.geode.internal.protocol.protobuf.v1.utilities.ProtobufUtilities;
import org.apache.geode.management.internal.security.ResourceConstants;
import org.apache.geode.management.internal.security.ResourcePermissions;
import org.apache.geode.security.AuthenticationFailedException;
import org.apache.geode.security.ResourcePermission;
import org.apache.geode.security.SecurityManager;
import org.apache.geode.test.junit.categories.ClientServerTest;
@Category({ClientServerTest.class})
public class AuthorizationIntegrationTest {
private static final String TEST_USERNAME = "bob";
private static final String TEST_PASSWORD = "bobspassword";
private static final String TEST_REGION1 = "testRegion1";
private static final String TEST_REGION2 = "testRegion2";
private static final String TEST_KEY1 = "testKey1";
private static final String TEST_KEY2 = "testKey2";
private final String OQLTwoRegionTestQuery =
"select * from /" + TEST_REGION1 + " one, /" + TEST_REGION2 + " two where one.id = two.id";
@Rule
public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
private Cache cache;
private int cacheServerPort;
private CacheServer cacheServer;
private Socket socket;
private OutputStream outputStream;
private ProtobufSerializationService serializationService;
private InputStream inputStream;
private ProtobufProtocolSerializer protobufProtocolSerializer;
private Object securityPrincipal;
private TestSecurityManager securityManager;
// Read access to all regions, all keys
public static final ResourcePermission READ_PERMISSION = ResourcePermissions.DATA_READ;
// Write access to all regions, all keys
public static final ResourcePermission WRITE_PERMISSION = ResourcePermissions.DATA_WRITE;
private class TestSecurityManager implements SecurityManager {
private Set<ResourcePermission> allowedPermissions = new HashSet<>();
void addAllowedPermission(ResourcePermission permission) {
allowedPermissions.add(permission);
}
@Override
public Object authenticate(Properties credentials) throws AuthenticationFailedException {
return securityPrincipal;
}
@Override
public boolean authorize(Object principal, ResourcePermission permission) {
// Only allow data operations and only from the expected principal
if (!principal.equals(securityPrincipal)
|| permission.getResource() != ResourcePermission.Resource.DATA) {
return false;
}
// Succeed if user has permission for all regions and all keys for the given operation
if (allowedPermissions.contains(
new ResourcePermission(ResourcePermission.Resource.DATA, permission.getOperation()))) {
return true;
}
// Succeed if user has permission for all keys in the given region for the given operation
if (allowedPermissions.contains(new ResourcePermission(ResourcePermission.Resource.DATA,
permission.getOperation(), permission.getTarget()))) {
return true;
}
return allowedPermissions.contains(permission);
}
}
@Before
public void setUp() throws IOException, InvalidProtocolMessageException {
Properties expectedAuthProperties = new Properties();
expectedAuthProperties.setProperty(ResourceConstants.USER_NAME, TEST_USERNAME);
expectedAuthProperties.setProperty(ResourceConstants.PASSWORD, TEST_PASSWORD);
securityPrincipal = "mockSecurityPrincipal";
securityManager = new TestSecurityManager();
Properties properties = new Properties();
CacheFactory cacheFactory = new CacheFactory(properties);
cacheFactory.set("mcast-port", "0"); // sometimes it isn't due to other tests.
cacheFactory.setSecurityManager(securityManager);
cache = cacheFactory.create();
cacheServer = cache.addCacheServer();
cacheServerPort = AvailablePortHelper.getRandomAvailableTCPPort();
cacheServer.setPort(cacheServerPort);
cacheServer.start();
cache.createRegionFactory().create(TEST_REGION1);
cache.createRegionFactory().create(TEST_REGION2);
System.setProperty("geode.feature-protobuf-protocol", "true");
System.setProperty("geode.protocol-authentication-mode", "SIMPLE");
socket = new Socket("localhost", cacheServerPort);
await().until(socket::isConnected);
outputStream = socket.getOutputStream();
inputStream = socket.getInputStream();
serializationService = new ProtobufSerializationService();
protobufProtocolSerializer = new ProtobufProtocolSerializer();
MessageUtil.performAndVerifyHandshake(socket);
ClientProtocol.Message authenticationRequest = ClientProtocol.Message.newBuilder()
.setHandshakeRequest(ConnectionAPI.HandshakeRequest.newBuilder()
.putCredentials(ResourceConstants.USER_NAME, TEST_USERNAME)
.putCredentials(ResourceConstants.PASSWORD, TEST_PASSWORD))
.build();
authenticationRequest.writeDelimitedTo(outputStream);
ClientProtocol.Message responseMessage = ClientProtocol.Message.parseDelimitedFrom(inputStream);
assertEquals(ClientProtocol.Message.HANDSHAKERESPONSE_FIELD_NUMBER,
responseMessage.getMessageTypeCase().getNumber());
ConnectionAPI.HandshakeResponse authenticationResponse = responseMessage.getHandshakeResponse();
assertTrue(authenticationResponse.getAuthenticated());
}
@After
public void shutDown() throws IOException {
cache.close();
socket.close();
}
@Test
public void validateNoPermissions() throws Exception {
verifyLocatorOperation(false);
verifyOperations(false, false, TEST_REGION1, TEST_KEY1);
}
@Test
public void validateWritePermission() throws Exception {
securityManager.addAllowedPermission(WRITE_PERMISSION);
verifyLocatorOperation(false);
verifyOperations(false, true, TEST_REGION1, TEST_KEY1);
}
@Test
public void validateReadPermission() throws Exception {
securityManager.addAllowedPermission(READ_PERMISSION);
verifyLocatorOperation(true);
verifyOperations(true, false, TEST_REGION1, TEST_KEY1);
}
@Test
public void validateReadAndWritePermission() throws Exception {
securityManager.addAllowedPermission(WRITE_PERMISSION);
securityManager.addAllowedPermission(READ_PERMISSION);
verifyLocatorOperation(true);
verifyOperations(true, true, TEST_REGION1, TEST_KEY1);
}
@Test
public void validateRegionLevelPermissions() throws Exception {
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.WRITE, TEST_REGION1));
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.READ, TEST_REGION2));
verifyOperations(false, true, TEST_REGION1, TEST_KEY1);
verifyOperations(false, true, TEST_REGION1, TEST_KEY2);
verifyOperations(true, false, TEST_REGION2, TEST_KEY1);
verifyOperations(true, false, TEST_REGION2, TEST_KEY2);
}
@Test
public void validateKeyLevelPermissions() throws Exception {
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.WRITE, TEST_REGION1, TEST_KEY1));
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.READ, TEST_REGION2, TEST_KEY2));
verifyOperations(false, true, TEST_REGION1, TEST_KEY1);
verifyOperations(false, false, TEST_REGION1, TEST_KEY2);
verifyOperations(false, false, TEST_REGION2, TEST_KEY1);
verifyOperations(true, false, TEST_REGION2, TEST_KEY2);
}
@Test
public void validateRegionLevelPermissionsOnBatchOperations() throws Exception {
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.WRITE, TEST_REGION1));
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.READ, TEST_REGION2));
verifyBatchOperation(true, TEST_REGION1, false, false);
verifyBatchOperation(false, TEST_REGION1, true, true);
verifyBatchOperation(true, TEST_REGION2, true, true);
verifyBatchOperation(false, TEST_REGION2, false, false);
}
@Test
public void validateKeyLevelPermissionsOnBatchOperations() throws Exception {
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.WRITE, TEST_REGION1, TEST_KEY1));
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.READ, TEST_REGION2, TEST_KEY2));
verifyBatchOperation(true, TEST_REGION1, false, false);
verifyBatchOperation(false, TEST_REGION1, true, false);
verifyBatchOperation(true, TEST_REGION2, false, true);
verifyBatchOperation(false, TEST_REGION2, false, false);
}
@Test
public void testOQLAuthorization() throws Exception {
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.READ, TEST_REGION1));
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.READ, TEST_REGION2));
ClientProtocol.Message request = ClientProtocol.Message.newBuilder()
.setOqlQueryRequest(RegionAPI.OQLQueryRequest.newBuilder().setQuery(OQLTwoRegionTestQuery))
.build();
protobufProtocolSerializer.serialize(request, outputStream);
ClientProtocol.Message response = protobufProtocolSerializer.deserialize(inputStream);
assertEquals(ClientProtocol.Message.MessageTypeCase.OQLQUERYRESPONSE,
response.getMessageTypeCase());
}
@Test
public void testOQLRequiresAuthorizationForMultipleRegions() throws Exception {
securityManager.addAllowedPermission(new ResourcePermission(ResourcePermission.Resource.DATA,
ResourcePermission.Operation.READ, TEST_REGION1));
ClientProtocol.Message request = ClientProtocol.Message.newBuilder()
.setOqlQueryRequest(RegionAPI.OQLQueryRequest.newBuilder().setQuery(OQLTwoRegionTestQuery))
.build();
protobufProtocolSerializer.serialize(request, outputStream);
ClientProtocol.Message response = protobufProtocolSerializer.deserialize(inputStream);
assertEquals(ClientProtocol.Message.MessageTypeCase.ERRORRESPONSE,
response.getMessageTypeCase());
assertEquals(BasicTypes.ErrorCode.AUTHORIZATION_FAILED,
response.getErrorResponse().getError().getErrorCode());
}
private void verifyBatchOperation(boolean testRead, String region, boolean expectedKey1Success,
boolean expectedKey2Success) throws Exception {
ClientProtocol.Message request;
if (testRead) {
request = ClientProtocol.Message.newBuilder()
.setGetAllRequest(RegionAPI.GetAllRequest.newBuilder().setRegionName(region)
.addKey(serializationService.encode(TEST_KEY1))
.addKey(serializationService.encode(TEST_KEY2)))
.build();
} else {
request = ClientProtocol.Message.newBuilder().setPutAllRequest(RegionAPI.PutAllRequest
.newBuilder().setRegionName(region)
.addEntry(ProtobufUtilities.createEntry(serializationService, TEST_KEY1, "TEST_VALUE"))
.addEntry(ProtobufUtilities.createEntry(serializationService, TEST_KEY2, "TEST_VALUE")))
.build();
}
protobufProtocolSerializer.serialize(request, outputStream);
ClientProtocol.Message response = protobufProtocolSerializer.deserialize(inputStream);
assertNotEquals(ClientProtocol.Message.MessageTypeCase.ERRORRESPONSE,
response.getMessageTypeCase());
List<BasicTypes.KeyedError> keyedErrors =
testRead ? response.getGetAllResponse().getFailuresList()
: response.getPutAllResponse().getFailedKeysList();
String operation = testRead ? "getAll" : "putAll";
if (errorListContainsKey(keyedErrors, serializationService.encode(TEST_KEY1))) {
if (expectedKey1Success) {
fail("Unexpectedly failed " + operation + " operation for key " + TEST_KEY1);
}
} else if (!expectedKey1Success) {
fail("Unexpected success in " + operation + " operation for key " + TEST_KEY1);
}
if (errorListContainsKey(keyedErrors, serializationService.encode(TEST_KEY2))) {
if (expectedKey2Success) {
fail("Unexpectedly failed " + operation + " operation for key " + TEST_KEY2);
}
} else if (!expectedKey2Success) {
fail("Unexpected success in " + operation + " operation for key " + TEST_KEY2);
}
}
private boolean errorListContainsKey(List<BasicTypes.KeyedError> errors,
BasicTypes.EncodedValue key) {
for (BasicTypes.KeyedError error : errors) {
if (error.getKey().equals(key)) {
return true;
}
}
return false;
}
private void verifyLocatorOperation(boolean readAllowed) throws Exception {
ClientProtocol.Message getRegionsMessage = ClientProtocol.Message.newBuilder()
.setGetRegionNamesRequest(RegionAPI.GetRegionNamesRequest.newBuilder()).build();
validateOperationAuthorized(getRegionsMessage, inputStream, outputStream,
readAllowed ? ClientProtocol.Message.MessageTypeCase.GETREGIONNAMESRESPONSE
: ClientProtocol.Message.MessageTypeCase.ERRORRESPONSE);
}
private void verifyOperations(boolean readAllowed, boolean writeAllowed, String region,
String key) throws Exception {
ClientProtocol.Message getMessage =
ClientProtocol.Message.newBuilder().setGetRequest(RegionAPI.GetRequest.newBuilder()
.setRegionName(region).setKey(serializationService.encode(key))).build();
validateOperationAuthorized(getMessage, inputStream, outputStream,
readAllowed ? ClientProtocol.Message.MessageTypeCase.GETRESPONSE
: ClientProtocol.Message.MessageTypeCase.ERRORRESPONSE);
ClientProtocol.Message putMessage = ClientProtocol.Message.newBuilder()
.setPutRequest(RegionAPI.PutRequest.newBuilder().setRegionName(region)
.setEntry(ProtobufUtilities.createEntry(serializationService, key, "TEST_VALUE")))
.build();
validateOperationAuthorized(putMessage, inputStream, outputStream,
writeAllowed ? ClientProtocol.Message.MessageTypeCase.PUTRESPONSE
: ClientProtocol.Message.MessageTypeCase.ERRORRESPONSE);
ClientProtocol.Message removeMessage =
ClientProtocol.Message.newBuilder().setRemoveRequest(RegionAPI.RemoveRequest.newBuilder()
.setRegionName(region).setKey(serializationService.encode(key))).build();
validateOperationAuthorized(removeMessage, inputStream, outputStream,
writeAllowed ? ClientProtocol.Message.MessageTypeCase.REMOVERESPONSE
: ClientProtocol.Message.MessageTypeCase.ERRORRESPONSE);
}
private void validateOperationAuthorized(ClientProtocol.Message message, InputStream inputStream,
OutputStream outputStream, ClientProtocol.Message.MessageTypeCase expectedResponseType)
throws Exception {
protobufProtocolSerializer.serialize(message, outputStream);
ClientProtocol.Message response = protobufProtocolSerializer.deserialize(inputStream);
assertEquals(expectedResponseType, response.getMessageTypeCase());
if (expectedResponseType == ClientProtocol.Message.MessageTypeCase.ERRORRESPONSE) {
Assert.assertEquals(BasicTypes.ErrorCode.AUTHORIZATION_FAILED,
response.getErrorResponse().getError().getErrorCode());
}
}
}