diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ClientRMService.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ClientRMService.java index e9bd230..6c8b92f 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ClientRMService.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ClientRMService.java @@ -1186,7 +1186,27 @@ public MoveApplicationAcrossQueuesResponse moveApplicationAcrossQueues( + callerUGI.getShortUserName() + " cannot perform operation " + ApplicationAccessType.MODIFY_APP.name() + " on " + applicationId)); } - + + String targetQueue = request.getTargetQueue(); + try { + if (!accessToTargetQueueAllowed(callerUGI, application, targetQueue)) { + RMAuditLogger.logFailure(callerUGI.getShortUserName(), + AuditConstants.MOVE_APP_REQUEST, + "User doesn't have permissions to move application to queue " + + targetQueue, "ClientRMService", + AuditConstants.UNAUTHORIZED_USER, applicationId); + throw RPCUtil.getRemoteException(new AccessControlException("User " + + callerUGI.getShortUserName() + + " doesn't have permissions to move application to queue " + + targetQueue + " on " + applicationId)); + } + } catch (IOException ioe) { + RMAuditLogger.logFailure(callerUGI.getShortUserName(), + AuditConstants.MOVE_APP_REQUEST, "UNKNOWN", "ClientRMService", + "Target queue does not exist " + targetQueue); + throw RPCUtil.getRemoteException(ioe); + } + // Moves only allowed when app is in a state that means it is tracked by // the scheduler if (EnumSet.of(RMAppState.NEW, RMAppState.NEW_SAVING, RMAppState.FAILED, @@ -1201,7 +1221,7 @@ public MoveApplicationAcrossQueuesResponse moveApplicationAcrossQueues( SettableFuture future = SettableFuture.create(); this.rmContext.getDispatcher().getEventHandler().handle( - new RMAppMoveEvent(applicationId, request.getTargetQueue(), future)); + new RMAppMoveEvent(applicationId, targetQueue, future)); try { Futures.get(future, YarnException.class); @@ -1219,6 +1239,24 @@ public MoveApplicationAcrossQueuesResponse moveApplicationAcrossQueues( 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) throws IOException { + 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 token) throws IOException { UserGroupInformation user = UserGroupInformation.getCurrentUser(); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/security/QueueACLsManager.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/security/QueueACLsManager.java index c9d55f1..2eba4fd 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/security/QueueACLsManager.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/security/QueueACLsManager.java @@ -32,7 +32,10 @@ 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.CapacityScheduler; +import org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FSQueue; +import org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler; +import java.io.IOException; import java.util.List; public class QueueACLsManager { @@ -80,4 +83,46 @@ public boolean checkAccess(UserGroupInformation callerUGI, QueueACL acl, 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 + * @param forwardedAddresses + * @param targetQueue the name of the queue to move the application to + * @return true if submission is allowed, false otherwise + */ + public boolean checkAccess(UserGroupInformation callerUGI, QueueACL acl, + RMApp app, String remoteAddress, List forwardedAddresses, + String targetQueue) throws IOException { + if (!isACLsEnable) { + return true; + } + if (scheduler instanceof CapacityScheduler) { + CSQueue queue = ((CapacityScheduler) scheduler).getQueue(targetQueue); + if (queue == null) { + throw new IOException("Queue " + targetQueue + " does not exist."); + } + return authorizer.checkPermission( + new AccessRequest(queue.getPrivilegedEntity(), callerUGI, + SchedulerUtils.toAccessType(acl), + app.getApplicationId().toString(), app.getName(), + remoteAddress, forwardedAddresses)); + } + if (scheduler instanceof FairScheduler) { + FSQueue queue = ((FairScheduler) scheduler).getQueueManager(). + getQueue(targetQueue); + if (queue == null) { + throw new IOException("Queue " + targetQueue + " does not exist."); + } + return scheduler.checkAccess(callerUGI, acl, targetQueue); + } + // Any other scheduler just try + return scheduler.checkAccess(callerUGI, acl, targetQueue); + } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/TestClientRMService.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/TestClientRMService.java index cee9086..5c1a792 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/TestClientRMService.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/TestClientRMService.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyListOf; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doReturn; @@ -33,6 +34,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.net.InetSocketAddress; +import java.security.AccessControlException; +import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; @@ -130,6 +133,7 @@ import org.apache.hadoop.yarn.server.resourcemanager.rmapp.RMApp; import org.apache.hadoop.yarn.server.resourcemanager.rmapp.RMAppEvent; import org.apache.hadoop.yarn.server.resourcemanager.rmapp.RMAppImpl; +import org.apache.hadoop.yarn.server.resourcemanager.rmapp.RMAppMoveEvent; import org.apache.hadoop.yarn.server.resourcemanager.rmapp.RMAppState; import org.apache.hadoop.yarn.server.resourcemanager.rmapp.attempt.RMAppAttempt; import org.apache.hadoop.yarn.server.resourcemanager.rmapp.attempt.RMAppAttemptImpl; @@ -570,11 +574,276 @@ public void testMoveAbsentApplication() throws YarnException { ApplicationId applicationId = BuilderUtils.newApplicationId(System.currentTimeMillis(), 0); MoveApplicationAcrossQueuesRequest request = - MoveApplicationAcrossQueuesRequest.newInstance(applicationId, "newqueue"); + MoveApplicationAcrossQueuesRequest.newInstance(applicationId, + "newqueue"); 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() { + @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() { + @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 = getThrowingQueueAclManager(); + 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); + ConcurrentHashMap apps = new ConcurrentHashMap<>(); + apps.put(applicationId, app); + + RMContext rmContext = mock(RMContext.class); + when(rmContext.getRMApps()).thenReturn(apps); + Dispatcher dispatcher = mock(Dispatcher.class); + when(rmContext.getDispatcher()).thenReturn(dispatcher); + when(rmContext.getDispatcher().getEventHandler()).thenReturn(event -> { + if (event instanceof RMAppMoveEvent) { + // signal move event is processed successfully + ((RMAppMoveEvent) event).getResult().set(null); + } + }); + + return new ClientRMService(rmContext, null, null, 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 application acl manager + * @param appAccess the acl to check view or modify + * @param aclUser the user to check + * @param appOwner the application owner + * @return ApplicationACLsManager + */ + private ApplicationACLsManager getAppAclManager(ApplicationAccessType appAccess, + UserGroupInformation aclUser, String appOwner) { + ApplicationACLsManager aclsManager = mock(ApplicationACLsManager.class); + when(aclsManager.checkAccess( + any(UserGroupInformation.class), + any(ApplicationAccessType.class), + any(String.class), + any(ApplicationId.class))).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + UserGroupInformation user = (UserGroupInformation) args[0]; + return appOwner.equals(args[2]) || + (appAccess.equals(args[1]) && + aclUser.getShortUserName().equals(user.getShortUserName())); + }); + return aclsManager; + } + + /** + * Generate the Queue acl + * @param allowedQueue the queue to allow the move to + * @param acl the acl to check: submit app or queue admin + * @param aclUser the user to check + * @return QueueACLsManager + */ + private QueueACLsManager getQueueAclManager(String allowedQueue, + QueueACL acl, 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(invocation -> { + Object[] args = invocation.getArguments(); + UserGroupInformation user = (UserGroupInformation) args[0]; + return (acl.equals(args[1]) && + 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(invocation -> { + Object[] args = invocation.getArguments(); + UserGroupInformation user = (UserGroupInformation) args[0]; + return (allowedQueue.equals(args[5]) && acl.equals(args[1]) && + aclUser.getShortUserName().equals(user.getShortUserName())); + }); + + return queueACLsManager; + } + + /** + * Queue acl manager that throws to simulate a missing queue + * @return QueueACLsManager + */ + private QueueACLsManager getThrowingQueueAclManager() throws IOException { + 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))).thenThrow( + new IOException("Queue does not exist.")); + return queueACLsManager; + } + + @Test public void testGetQueueInfo() throws Exception { YarnScheduler yarnScheduler = mock(YarnScheduler.class); RMContext rmContext = mock(RMContext.class);