/**
* diqube: Distributed Query Base.
*
* Copyright (C) 2015 Bastian Gloeckle
*
* This file is part of diqube.
*
* diqube is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diqube.itest.tests;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import org.apache.thrift.TException;
import org.diqube.itest.AbstractDiqubeIntegrationTest;
import org.diqube.itest.annotations.NeedsServer;
import org.diqube.itest.control.ServerControl;
import org.diqube.itest.util.QueryResultServiceTestUtil;
import org.diqube.itest.util.QueryResultServiceTestUtil.TestQueryResultService;
import org.diqube.itest.util.Waiter;
import org.diqube.permission.Permissions;
import org.diqube.server.ControlFileManager;
import org.diqube.thrift.base.thrift.AuthenticationException;
import org.diqube.thrift.base.thrift.AuthorizationException;
import org.diqube.thrift.base.thrift.Ticket;
import org.diqube.thrift.base.util.RUuidUtil;
import org.diqube.tool.im.AddPermissionActualIdentityToolFunction;
import org.diqube.tool.im.ChangePasswordActualIdentityToolFunction;
import org.diqube.tool.im.CreateUserActualIdentityToolFunction;
import org.diqube.tool.im.DeletePermissionActualIdentityToolFunction;
import org.diqube.tool.im.DeleteUserActualIdentityToolFunction;
import org.diqube.tool.im.IdentityToolFunction;
import org.diqube.util.Holder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.Test;
/**
* Tests that services can only be accessed by users that have corresponding permissions.
*
* <p>
* Uses diqube-tools {@link IdentityToolFunction} to adjust permissions and also tests the vital functions of that.
*
* @author Bastian Gloeckle
*/
public class TableTicketIntegrationTest extends AbstractDiqubeIntegrationTest {
private static final Logger logger = LoggerFactory.getLogger(TableTicketIntegrationTest.class);
private static final String FIRST_TABLE = "age";
private static final String FIRST_CONTROL_FILE =
"/" + TableTicketIntegrationTest.class.getSimpleName() + "/age" + ControlFileManager.CONTROL_FILE_EXTENSION;
private static final String SECOND_TABLE = "age2";
private static final String SECOND_CONTROL_FILE =
"/" + TableTicketIntegrationTest.class.getSimpleName() + "/age2" + ControlFileManager.CONTROL_FILE_EXTENSION;
private static final String JSON_FILE = "/" + MergeDeployIntegrationTest.class.getSimpleName() + "/age.json";
private static final String USER = "testUser";
private static final String PWD = "testPassword";
private static final String SECOND_PWD = "testPassword2";
private static final String EMAIL = "a@b.c";
@Test
@NeedsServer(servers = 1)
public void queryUserAllowed() throws IOException {
// GIVEN
Ticket t = deployCreateUserWithPermissionsAndLogin();
// execute queries
try (TestQueryResultService queryResults = QueryResultServiceTestUtil.createQueryResultService()) {
UUID queryUuid = UUID.randomUUID();
logger.info("Executing query {}", queryUuid);
serverControl.get(0).getSerivceTestUtil()
.queryService(queryService -> queryService.asyncExecuteQuery(t, RUuidUtil.toRUuid(queryUuid),
"select age from " + FIRST_TABLE, true, queryResults.getThisServicesAddr().toRNodeAddress()));
// we have access, so this should succeed!
new Waiter().waitUntil("Final result of query received", 10, 500,
() -> queryResults.check() && queryResults.getFinalUpdate() != null);
// we do NOT have access, so exception is expected.
try {
serverControl.get(0).getSerivceTestUtil()
.queryServiceThrowException(queryService -> queryService.asyncExecuteQuery(t, RUuidUtil.toRUuid(queryUuid),
"select age from " + SECOND_TABLE, true, queryResults.getThisServicesAddr().toRNodeAddress()));
Assert.fail("Expected to receive an exception since we do not have permission to access the table!");
} catch (TException e) {
// swallow, as this is expected!
logger.info("Received exception {}", e);
Assert.assertTrue(e instanceof AuthorizationException,
"Received exception should be an AuthorizationException");
}
}
}
@Test
@NeedsServer(servers = 1)
public void cancelQueryUserAllowed() throws IOException {
// GIVEN
Ticket t = deployCreateUserWithPermissionsAndLogin();
Ticket rootTicket = serverControl.get(0).loginSuperuser();
// execute queries
try (TestQueryResultService queryResults = QueryResultServiceTestUtil.createQueryResultService()) {
UUID queryUuid = UUID.randomUUID();
logger.info("Executing query {}", queryUuid);
serverControl.get(0).getSerivceTestUtil()
.queryService(queryService -> queryService.asyncExecuteQuery(rootTicket, RUuidUtil.toRUuid(queryUuid),
"select age from " + FIRST_TABLE, true, queryResults.getThisServicesAddr().toRNodeAddress()));
// we do NOT have access with non-root-ticket
try {
serverControl.get(0).getSerivceTestUtil().queryServiceThrowException(
queryService -> queryService.cancelQueryExecution(t, RUuidUtil.toRUuid(queryUuid)));
Assert.fail("Expected to receive an exception since we do not have permission to cancel the query!");
} catch (TException e) {
// swallow, as this is expected!
logger.info("Received exception {}", e);
Assert.assertTrue(e instanceof AuthorizationException,
"Received exception should be an AuthorizationException");
}
}
}
@Test
@NeedsServer(servers = 1)
public void tableListOnlyContainsTablesWithPermission() throws IOException {
// GIVEN
Ticket t = deployCreateUserWithPermissionsAndLogin();
Holder<List<String>> h = new Holder<>();
serverControl.get(0).getSerivceTestUtil()
.clusterInfoService(infoService -> h.setValue(infoService.getAvailableTables(t)));
new Waiter().waitUntil("Received list of tables", 5, 100, () -> h.getValue() != null);
Assert.assertEquals(h.getValue(), Arrays.asList(FIRST_TABLE),
"Expected to receive correct (permission-filtered) list of tables");
}
@Test
@NeedsServer(servers = 1)
public void flattenOnlyAllowed() throws IOException {
// GIVEN
Ticket t = deployCreateUserWithPermissionsAndLogin();
try {
serverControl.get(0).getSerivceTestUtil().flattenPreparationServiceThrowException(
flattenPrepServ -> flattenPrepServ.prepareForQueriesOnFlattenedTable(t, SECOND_TABLE, "a"));
Assert.fail("Expected to get exception when trying to flatten table that we do not have access to.");
} catch (TException e) {
logger.info("Received exception {}", e);
Assert.assertTrue(e instanceof AuthorizationException, "Received exception should be an AuthorizationException");
}
try {
// try to flatten on table we have access to. We can't actually flatten the table since the field is not repeated,
// but anyway, we should not receive an exception (because the server identifies asynchronously that it cannot
// flatten by that field)
serverControl.get(0).getSerivceTestUtil().flattenPreparationServiceThrowException(
flattenPrepServ -> flattenPrepServ.prepareForQueriesOnFlattenedTable(t, FIRST_TABLE, "a"));
} catch (TException e) {
logger.info("Received exception {}", e);
Assert.fail("Received exception although not expected");
}
}
@Test
@NeedsServer(servers = 1)
public void permissionRemovedAgain() throws IOException {
// GIVEN
Ticket t = deployCreateUserWithPermissionsAndLogin();
// remove permission from user
toolControl.im(serverControl.get(0).getAddr(), DeletePermissionActualIdentityToolFunction.FUNCTION_NAME, //
ServerControl.ROOT_USER, ServerControl.ROOT_PASSWORD, //
USER, // paramUser
null, // paramPassword
null, // paramEmail
Permissions.TABLE_ACCESS, // paramPermission
FIRST_TABLE // paramPermissionObject
);
Holder<List<String>> h = new Holder<>();
serverControl.get(0).getSerivceTestUtil()
.clusterInfoService(infoService -> h.setValue(infoService.getAvailableTables(t)));
new Waiter().waitUntil("Received list of tables", 5, 100, () -> h.getValue() != null);
Assert.assertEquals(h.getValue(), Arrays.asList(), // empty list!
"Expected to receive correct (permission-filtered) list of tables");
}
@Test
@NeedsServer(servers = 1)
public void userChangePasswordSucceeds() throws IOException {
// GIVEN
deployCreateUserWithPermissionsAndLogin();
// remove permission from user
toolControl.im(serverControl.get(0).getAddr(), ChangePasswordActualIdentityToolFunction.FUNCTION_NAME, //
ServerControl.ROOT_USER, ServerControl.ROOT_PASSWORD, //
USER, // paramUser
SECOND_PWD, // paramPassword
null, // paramEmail
null, // paramPermission
null // paramPermissionObject
);
Ticket t = serverControl.get(0).login(USER, SECOND_PWD);
Holder<List<String>> h = new Holder<>();
serverControl.get(0).getSerivceTestUtil()
.clusterInfoService(infoService -> h.setValue(infoService.getAvailableTables(t)));
new Waiter().waitUntil("Received list of tables", 5, 100, () -> h.getValue() != null);
Assert.assertEquals(h.getValue(), Arrays.asList(FIRST_TABLE), // we did not change permissions.
"Expected to receive correct (permission-filtered) list of tables");
}
@Test
@NeedsServer(servers = 1)
public void deleteUser() throws IOException {
// GIVEN
deployCreateUserWithPermissionsAndLogin();
// remove permission from user
toolControl.im(serverControl.get(0).getAddr(), DeleteUserActualIdentityToolFunction.FUNCTION_NAME, //
ServerControl.ROOT_USER, ServerControl.ROOT_PASSWORD, //
USER, // paramUser
null, // paramPassword
null, // paramEmail
null, // paramPermission
null // paramPermissionObject
);
Holder<AuthenticationException> authException = new Holder<>();
serverControl.get(0).getSerivceTestUtil().identityService(identityService -> {
try {
identityService.login(USER, PWD);
} catch (AuthenticationException e) {
authException.setValue(e);
}
});
new Waiter().waitUntil("AuthenticationException available", 3, 100, () -> authException.getValue() != null);
}
private Ticket deployCreateUserWithPermissionsAndLogin() {
serverControl.get(0).deploy(cp(FIRST_CONTROL_FILE), cp(JSON_FILE));
serverControl.get(0).deploy(cp(SECOND_CONTROL_FILE), cp(JSON_FILE));
// new user
toolControl.im(serverControl.get(0).getAddr(), CreateUserActualIdentityToolFunction.FUNCTION_NAME, //
ServerControl.ROOT_USER, ServerControl.ROOT_PASSWORD, //
USER, // paramUser
PWD, // paramPassword
EMAIL, // paramEmail
null, // paramPermission
null // paramPermissionObject
);
// add permission to user
toolControl.im(serverControl.get(0).getAddr(), AddPermissionActualIdentityToolFunction.FUNCTION_NAME, //
ServerControl.ROOT_USER, ServerControl.ROOT_PASSWORD, //
USER, // paramUser
null, // paramPassword
null, // paramEmail
Permissions.TABLE_ACCESS, // paramPermission
FIRST_TABLE // paramPermissionObject
);
// login
Ticket t = serverControl.get(0).login(USER, PWD);
return t;
}
}