YARN-5554. MoveApplicationAcrossQueues does not check user permission on the target queue

(Contributed by Wilfred Spiegelenburg via Daniel Templeton)
This commit is contained in:
Daniel Templeton 2017-01-10 16:32:16 -08:00
parent e648b6e138
commit 7979939428
3 changed files with 359 additions and 7 deletions

View File

@ -1186,20 +1186,35 @@ public class ClientRMService extends AbstractService implements
+ callerUGI.getShortUserName() + " cannot perform operation " + callerUGI.getShortUserName() + " cannot perform operation "
+ ApplicationAccessType.MODIFY_APP.name() + " on " + applicationId)); + ApplicationAccessType.MODIFY_APP.name() + " on " + applicationId));
} }
String targetQueue = request.getTargetQueue();
if (!accessToTargetQueueAllowed(callerUGI, application, targetQueue)) {
RMAuditLogger.logFailure(callerUGI.getShortUserName(),
AuditConstants.MOVE_APP_REQUEST, "Target queue doesn't exist or user"
+ " doesn't have permissions to submit to target queue: "
+ targetQueue, "ClientRMService",
AuditConstants.UNAUTHORIZED_USER, applicationId);
throw RPCUtil.getRemoteException(new AccessControlException("User "
+ callerUGI.getShortUserName() + " cannot submit applications to"
+ " target queue or the target queue doesn't exist: "
+ targetQueue + " while moving " + applicationId));
}
// Moves only allowed when app is in a state that means it is tracked by // Moves only allowed when app is in a state that means it is tracked by
// the scheduler. Introducing SUBMITTED state also to this list as there // the scheduler. Introducing SUBMITTED state also to this list as there
// could be a corner scenario that app may not be in Scheduler in SUBMITTED // could be a corner scenario that app may not be in Scheduler in SUBMITTED
// state. // state.
if (!ACTIVE_APP_STATES.contains(application.getState())) { if (!ACTIVE_APP_STATES.contains(application.getState())) {
String msg = "App in " + application.getState() + " state cannot be moved."; String msg = "App in " + application.getState() +
" state cannot be moved.";
RMAuditLogger.logFailure(callerUGI.getShortUserName(), RMAuditLogger.logFailure(callerUGI.getShortUserName(),
AuditConstants.MOVE_APP_REQUEST, "UNKNOWN", "ClientRMService", msg); AuditConstants.MOVE_APP_REQUEST, "UNKNOWN", "ClientRMService", msg);
throw new YarnException(msg); throw new YarnException(msg);
} }
try { try {
this.rmAppManager.moveApplicationAcrossQueue(applicationId, request.getTargetQueue()); this.rmAppManager.moveApplicationAcrossQueue(applicationId,
request.getTargetQueue());
} catch (YarnException ex) { } catch (YarnException ex) {
RMAuditLogger.logFailure(callerUGI.getShortUserName(), RMAuditLogger.logFailure(callerUGI.getShortUserName(),
AuditConstants.MOVE_APP_REQUEST, "UNKNOWN", "ClientRMService", AuditConstants.MOVE_APP_REQUEST, "UNKNOWN", "ClientRMService",
@ -1214,6 +1229,24 @@ public class ClientRMService extends AbstractService implements
return response; return response;
} }
/**
* Check if the submission of an application to the target queue is allowed.
* @param callerUGI the caller UGI
* @param application the application to move
* @param targetQueue the queue to move the application to
* @return true if submission is allowed, false otherwise
*/
private boolean accessToTargetQueueAllowed(UserGroupInformation callerUGI,
RMApp application, String targetQueue) {
return
queueACLsManager.checkAccess(callerUGI,
QueueACL.SUBMIT_APPLICATIONS, application,
Server.getRemoteAddress(), null, targetQueue) ||
queueACLsManager.checkAccess(callerUGI,
QueueACL.ADMINISTER_QUEUE, application,
Server.getRemoteAddress(), null, targetQueue);
}
private String getRenewerForToken(Token<RMDelegationTokenIdentifier> token) private String getRenewerForToken(Token<RMDelegationTokenIdentifier> token)
throws IOException { throws IOException {
UserGroupInformation user = UserGroupInformation.getCurrentUser(); UserGroupInformation user = UserGroupInformation.getCurrentUser();

View File

@ -32,6 +32,8 @@ import org.apache.hadoop.yarn.server.resourcemanager.scheduler.ResourceScheduler
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.SchedulerUtils; import org.apache.hadoop.yarn.server.resourcemanager.scheduler.SchedulerUtils;
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CSQueue; import org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CSQueue;
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler; import org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler;
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSQueue;
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler;
import java.util.List; import java.util.List;
@ -64,9 +66,11 @@ public class QueueACLsManager {
if (scheduler instanceof CapacityScheduler) { if (scheduler instanceof CapacityScheduler) {
CSQueue queue = ((CapacityScheduler) scheduler).getQueue(app.getQueue()); CSQueue queue = ((CapacityScheduler) scheduler).getQueue(app.getQueue());
if (queue == null) { if (queue == null) {
// Application exists but the associated queue does not exist. // The application exists but the associated queue does not exist.
// This may be due to queue is removed after RM restarts. Here, we choose // This may be due to a queue that is not defined when the RM restarts.
// to allow users to be able to view the apps for removed queue. // At this point we choose to log the fact and allow users to access
// and view the apps in a removed queue. This should only happen on
// application recovery.
LOG.error("Queue " + app.getQueue() + " does not exist for " + app LOG.error("Queue " + app.getQueue() + " does not exist for " + app
.getApplicationId()); .getApplicationId());
return true; return true;
@ -80,4 +84,63 @@ public class QueueACLsManager {
return scheduler.checkAccess(callerUGI, acl, app.getQueue()); return scheduler.checkAccess(callerUGI, acl, app.getQueue());
} }
} }
/**
* Check access to a targetQueue in the case of a move of an application.
* The application cannot contain the destination queue since it has not
* been moved yet, thus need to pass it in separately.
*
* @param callerUGI the caller UGI
* @param acl the acl for the Queue to check
* @param app the application to move
* @param remoteAddress server ip address
* @param forwardedAddresses forwarded adresses
* @param targetQueue the name of the queue to move the application to
* @return true: if submission is allowed and queue exists,
* false: in all other cases (also non existing target queue)
*/
public boolean checkAccess(UserGroupInformation callerUGI, QueueACL acl,
RMApp app, String remoteAddress, List<String> forwardedAddresses,
String targetQueue) {
if (!isACLsEnable) {
return true;
}
// Based on the discussion in YARN-5554 detail on why there are two
// versions:
// The access check inside these calls is currently scheduler dependent.
// This is due to the extra parameters needed for the CS case which are not
// in the version defined in the YarnScheduler interface. The second
// version is added for the moving the application case. The check has
// extra logging to distinguish between the queue not existing in the
// application move request case and the real access denied case.
if (scheduler instanceof CapacityScheduler) {
CSQueue queue = ((CapacityScheduler) scheduler).getQueue(targetQueue);
if (queue == null) {
LOG.warn("Target queue " + targetQueue
+ " does not exist while trying to move "
+ app.getApplicationId());
return false;
}
return authorizer.checkPermission(
new AccessRequest(queue.getPrivilegedEntity(), callerUGI,
SchedulerUtils.toAccessType(acl),
app.getApplicationId().toString(), app.getName(),
remoteAddress, forwardedAddresses));
} else if (scheduler instanceof FairScheduler) {
FSQueue queue = ((FairScheduler) scheduler).getQueueManager().
getQueue(targetQueue);
if (queue == null) {
LOG.warn("Target queue " + targetQueue
+ " does not exist while trying to move "
+ app.getApplicationId());
return false;
}
return scheduler.checkAccess(callerUGI, acl, targetQueue);
} else {
// Any other scheduler just try
return scheduler.checkAccess(callerUGI, acl, targetQueue);
}
}
} }

View File

@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyListOf;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
@ -33,6 +34,8 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.security.AccessControlException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumSet; import java.util.EnumSet;
@ -155,6 +158,8 @@ import org.junit.Test;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
public class TestClientRMService { public class TestClientRMService {
@ -572,10 +577,261 @@ public class TestClientRMService {
ApplicationId applicationId = ApplicationId applicationId =
BuilderUtils.newApplicationId(System.currentTimeMillis(), 0); BuilderUtils.newApplicationId(System.currentTimeMillis(), 0);
MoveApplicationAcrossQueuesRequest request = MoveApplicationAcrossQueuesRequest request =
MoveApplicationAcrossQueuesRequest.newInstance(applicationId, "newqueue"); MoveApplicationAcrossQueuesRequest.newInstance(applicationId,
"newqueue");
rmService.moveApplicationAcrossQueues(request); rmService.moveApplicationAcrossQueues(request);
} }
@Test
public void testMoveApplicationSubmitTargetQueue() throws Exception {
// move the application as the owner
ApplicationId applicationId = getApplicationId(1);
UserGroupInformation aclUGI = UserGroupInformation.getCurrentUser();
QueueACLsManager queueACLsManager = getQueueAclManager("allowed_queue",
QueueACL.SUBMIT_APPLICATIONS, aclUGI);
ApplicationACLsManager appAclsManager = getAppAclManager();
ClientRMService rmService = createClientRMServiceForMoveApplicationRequest(
applicationId, aclUGI.getShortUserName(), appAclsManager,
queueACLsManager);
// move as the owner queue in the acl
MoveApplicationAcrossQueuesRequest moveAppRequest =
MoveApplicationAcrossQueuesRequest.
newInstance(applicationId, "allowed_queue");
rmService.moveApplicationAcrossQueues(moveAppRequest);
// move as the owner queue not in the acl
moveAppRequest = MoveApplicationAcrossQueuesRequest.newInstance(
applicationId, "not_allowed");
try {
rmService.moveApplicationAcrossQueues(moveAppRequest);
Assert.fail("The request should fail with an AccessControlException");
} catch (YarnException rex) {
Assert.assertTrue("AccessControlException is expected",
rex.getCause() instanceof AccessControlException);
}
// ACL is owned by "moveuser", move is performed as a different user
aclUGI = UserGroupInformation.createUserForTesting("moveuser",
new String[]{});
queueACLsManager = getQueueAclManager("move_queue",
QueueACL.SUBMIT_APPLICATIONS, aclUGI);
appAclsManager = getAppAclManager();
ClientRMService rmService2 =
createClientRMServiceForMoveApplicationRequest(applicationId,
aclUGI.getShortUserName(), appAclsManager, queueACLsManager);
// access to the queue not OK: user not allowed in this queue
MoveApplicationAcrossQueuesRequest moveAppRequest2 =
MoveApplicationAcrossQueuesRequest.
newInstance(applicationId, "move_queue");
try {
rmService2.moveApplicationAcrossQueues(moveAppRequest2);
Assert.fail("The request should fail with an AccessControlException");
} catch (YarnException rex) {
Assert.assertTrue("AccessControlException is expected",
rex.getCause() instanceof AccessControlException);
}
// execute the move as the acl owner
// access to the queue OK: user allowed in this queue
aclUGI.doAs(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
return rmService2.moveApplicationAcrossQueues(moveAppRequest2);
}
});
}
@Test
public void testMoveApplicationAdminTargetQueue() throws Exception {
ApplicationId applicationId = getApplicationId(1);
UserGroupInformation aclUGI = UserGroupInformation.getCurrentUser();
QueueACLsManager queueAclsManager = getQueueAclManager("allowed_queue",
QueueACL.ADMINISTER_QUEUE, aclUGI);
ApplicationACLsManager appAclsManager = getAppAclManager();
ClientRMService rmService =
createClientRMServiceForMoveApplicationRequest(applicationId,
aclUGI.getShortUserName(), appAclsManager, queueAclsManager);
// user is admin move to queue in acl
MoveApplicationAcrossQueuesRequest moveAppRequest =
MoveApplicationAcrossQueuesRequest.newInstance(applicationId,
"allowed_queue");
rmService.moveApplicationAcrossQueues(moveAppRequest);
// user is admin move to queue not in acl
moveAppRequest = MoveApplicationAcrossQueuesRequest.newInstance(
applicationId, "not_allowed");
try {
rmService.moveApplicationAcrossQueues(moveAppRequest);
Assert.fail("The request should fail with an AccessControlException");
} catch (YarnException rex) {
Assert.assertTrue("AccessControlException is expected",
rex.getCause() instanceof AccessControlException);
}
// ACL is owned by "moveuser", move is performed as a different user
aclUGI = UserGroupInformation.createUserForTesting("moveuser",
new String[]{});
queueAclsManager = getQueueAclManager("move_queue",
QueueACL.ADMINISTER_QUEUE, aclUGI);
appAclsManager = getAppAclManager();
ClientRMService rmService2 =
createClientRMServiceForMoveApplicationRequest(applicationId,
aclUGI.getShortUserName(), appAclsManager, queueAclsManager);
// no access to this queue
MoveApplicationAcrossQueuesRequest moveAppRequest2 =
MoveApplicationAcrossQueuesRequest.
newInstance(applicationId, "move_queue");
try {
rmService2.moveApplicationAcrossQueues(moveAppRequest2);
Assert.fail("The request should fail with an AccessControlException");
} catch (YarnException rex) {
Assert.assertTrue("AccessControlException is expected",
rex.getCause() instanceof AccessControlException);
}
// execute the move as the acl owner
// access to the queue OK: user allowed in this queue
aclUGI.doAs(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
return rmService2.moveApplicationAcrossQueues(moveAppRequest2);
}
});
}
@Test (expected = YarnException.class)
public void testNonExistingQueue() throws Exception {
ApplicationId applicationId = getApplicationId(1);
UserGroupInformation aclUGI = UserGroupInformation.getCurrentUser();
QueueACLsManager queueAclsManager = getQueueAclManager();
ApplicationACLsManager appAclsManager = getAppAclManager();
ClientRMService rmService =
createClientRMServiceForMoveApplicationRequest(applicationId,
aclUGI.getShortUserName(), appAclsManager, queueAclsManager);
MoveApplicationAcrossQueuesRequest moveAppRequest =
MoveApplicationAcrossQueuesRequest.newInstance(applicationId,
"unknown_queue");
rmService.moveApplicationAcrossQueues(moveAppRequest);
}
/**
* Create an instance of ClientRMService for testing
* moveApplicationAcrossQueues requests.
* @param applicationId the application
* @return ClientRMService
*/
private ClientRMService createClientRMServiceForMoveApplicationRequest(
ApplicationId applicationId, String appOwner,
ApplicationACLsManager appAclsManager, QueueACLsManager queueAclsManager)
throws IOException {
RMApp app = mock(RMApp.class);
when(app.getUser()).thenReturn(appOwner);
when(app.getState()).thenReturn(RMAppState.RUNNING);
ConcurrentHashMap<ApplicationId, RMApp> apps = new ConcurrentHashMap<>();
apps.put(applicationId, app);
RMContext rmContext = mock(RMContext.class);
when(rmContext.getRMApps()).thenReturn(apps);
RMAppManager rmAppManager = mock(RMAppManager.class);
return new ClientRMService(rmContext, null, rmAppManager, appAclsManager,
queueAclsManager, null);
}
/**
* Plain application acl manager that always returns true.
* @return ApplicationACLsManager
*/
private ApplicationACLsManager getAppAclManager() {
ApplicationACLsManager aclsManager = mock(ApplicationACLsManager.class);
when(aclsManager.checkAccess(
any(UserGroupInformation.class),
any(ApplicationAccessType.class),
any(String.class),
any(ApplicationId.class))).thenReturn(true);
return aclsManager;
}
/**
* Generate the Queue acl.
* @param allowedQueue the queue to allow the move to
* @param queueACL the acl to check: submit app or queue admin
* @param aclUser the user to check
* @return QueueACLsManager
*/
private QueueACLsManager getQueueAclManager(String allowedQueue,
QueueACL queueACL, UserGroupInformation aclUser) throws IOException {
// ACL that checks the queue is allowed
QueueACLsManager queueACLsManager = mock(QueueACLsManager.class);
when(queueACLsManager.checkAccess(
any(UserGroupInformation.class),
any(QueueACL.class),
any(RMApp.class),
any(String.class),
anyListOf(String.class))).thenAnswer(new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocationOnMock) {
final UserGroupInformation user =
(UserGroupInformation) invocationOnMock.getArguments()[0];
final QueueACL acl =
(QueueACL) invocationOnMock.getArguments()[1];
return (queueACL.equals(acl) &&
aclUser.getShortUserName().equals(user.getShortUserName()));
}
});
when(queueACLsManager.checkAccess(
any(UserGroupInformation.class),
any(QueueACL.class),
any(RMApp.class),
any(String.class),
anyListOf(String.class),
any(String.class))).thenAnswer(new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocationOnMock) {
final UserGroupInformation user =
(UserGroupInformation) invocationOnMock.getArguments()[0];
final QueueACL acl = (QueueACL) invocationOnMock.getArguments()[1];
final String queue = (String) invocationOnMock.getArguments()[5];
return (allowedQueue.equals(queue) && queueACL.equals(acl) &&
aclUser.getShortUserName().equals(user.getShortUserName()));
}
});
return queueACLsManager;
}
/**
* QueueACLsManager that always returns false when a target queue is passed
* in and true for other checks to simulate a missing queue.
* @return QueueACLsManager
*/
private QueueACLsManager getQueueAclManager() {
QueueACLsManager queueACLsManager = mock(QueueACLsManager.class);
when(queueACLsManager.checkAccess(
any(UserGroupInformation.class),
any(QueueACL.class),
any(RMApp.class),
any(String.class),
anyListOf(String.class),
any(String.class))).thenReturn(false);
when(queueACLsManager.checkAccess(
any(UserGroupInformation.class),
any(QueueACL.class),
any(RMApp.class),
any(String.class),
anyListOf(String.class))).thenReturn(true);
return queueACLsManager;
}
@Test @Test
public void testGetQueueInfo() throws Exception { public void testGetQueueInfo() throws Exception {
YarnScheduler yarnScheduler = mock(YarnScheduler.class); YarnScheduler yarnScheduler = mock(YarnScheduler.class);