diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/JettyUtils.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/JettyUtils.java new file mode 100644 index 00000000000..29c0930968e --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/JettyUtils.java @@ -0,0 +1,35 @@ +/** + * 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.hadoop.http; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Contains utility methods and constants relating to Jetty. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class JettyUtils { + public static final String UTF_8 = "charset=utf-8"; + public static final int HEADER_SIZE = 1024 * 64; + + private JettyUtils() { + } +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWSConsts.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWSConsts.java new file mode 100644 index 00000000000..c60acdcc6ed --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWSConsts.java @@ -0,0 +1,231 @@ +/** + * 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.hadoop.yarn.server.resourcemanager.webapp; + +/** + * Constants for {@code RMWebServiceProtocol}. + */ +public final class RMWSConsts { + + public static final String EMPTY = ""; + public static final String ANY = "*"; + + public static final String FORWARDED_FOR = "X-Forwarded-For"; + + // ----------------Paths for RMWebServiceProtocol---------------- + + /** Path for {@code RMWebServiceProtocol}. */ + public static final String RM_WEB_SERVICE_PATH = "/ws/v1/cluster"; + + /** Path for {@code RMWebServiceProtocol#getClusterInfo}. */ + public static final String INFO = "/info"; + + /** Path for {@code RMWebServiceProtocol#getClusterUserInfo}. */ + public static final String CLUSTER_USER_INFO = "/userinfo"; + + /** Path for {@code RMWebServiceProtocol#getClusterMetricsInfo}. */ + public static final String METRICS = "/metrics"; + + /** Path for {@code RMWebServiceProtocol#getSchedulerInfo}. */ + public static final String SCHEDULER = "/scheduler"; + + /** Path for {@code RMWebServices#updateSchedulerConfiguration}. */ + public static final String SCHEDULER_CONF = "/scheduler-conf"; + + /** Path for {@code RMWebServiceProtocol#dumpSchedulerLogs}. */ + public static final String SCHEDULER_LOGS = "/scheduler/logs"; + + /** Path for {@code RMWebServiceProtocol#getNodes}. */ + public static final String NODES = "/nodes"; + + /** Path for {@code RMWebServiceProtocol#getNode}. */ + public static final String NODES_NODEID = "/nodes/{nodeId}"; + + /** + * Path for {@code RMWebServiceProtocol#getApps} and + * {@code RMWebServiceProtocol#getApp}. + */ + public static final String APPS = "/apps"; + + /** Path for {@code RMWebServiceProtocol#getActivities}. */ + public static final String SCHEDULER_ACTIVITIES = "/scheduler/activities"; + + /** Path for {@code RMWebServiceProtocol#getAppActivities}. */ + public static final String SCHEDULER_APP_ACTIVITIES = + "/scheduler/app-activities"; + + /** Path for {@code RMWebServiceProtocol#getAppStatistics}. */ + public static final String APP_STATISTICS = "/appstatistics"; + + /** Path for {@code RMWebServiceProtocol#getApp}. */ + public static final String APPS_APPID = "/apps/{appid}"; + + /** Path for {@code RMWebServiceProtocol#getAppAttempts}. */ + public static final String APPS_APPID_APPATTEMPTS = + "/apps/{appid}/appattempts"; + + /** Path for {@code WebServices#getAppAttempt}. */ + public static final String APPS_APPID_APPATTEMPTS_APPATTEMPTID = + "/apps/{appid}/appattempts/{appattemptid}"; + + /** Path for {@code WebServices#getContainers}. */ + public static final String APPS_APPID_APPATTEMPTS_APPATTEMPTID_CONTAINERS = + "/apps/{appid}/appattempts/{appattemptid}/containers"; + + /** Path for {@code RMWebServiceProtocol#getNodeToLabels}. */ + public static final String GET_NODE_TO_LABELS = "/get-node-to-labels"; + + /** Path for {@code RMWebServiceProtocol#getLabelsToNodes}. */ + public static final String LABEL_MAPPINGS = "/label-mappings"; + + /** Path for {@code RMWebServiceProtocol#replaceLabelsOnNodes}. */ + public static final String REPLACE_NODE_TO_LABELS = "/replace-node-to-labels"; + + /** Path for {@code RMWebServiceProtocol#replaceLabelsOnNode}. */ + public static final String NODES_NODEID_REPLACE_LABELS = + "/nodes/{nodeId}/replace-labels"; + + /** Path for {@code RMWebServiceProtocol#getClusterNodeLabels}. */ + public static final String GET_NODE_LABELS = "/get-node-labels"; + + /** Path for {@code RMWebServiceProtocol#addToClusterNodeLabels}. */ + public static final String ADD_NODE_LABELS = "/add-node-labels"; + + /** Path for {@code RMWebServiceProtocol#removeFromCluserNodeLabels}. */ + public static final String REMOVE_NODE_LABELS = "/remove-node-labels"; + + /** Path for {@code RMWebServiceProtocol#getLabelsOnNode}. */ + public static final String NODES_NODEID_GETLABELS = + "/nodes/{nodeId}/get-labels"; + + /** + * Path for {@code RMWebServiceProtocol#getAppPriority} and + * {@code RMWebServiceProtocol#updateApplicationPriority}. + */ + public static final String APPS_APPID_PRIORITY = "/apps/{appid}/priority"; + + /** + * Path for {@code RMWebServiceProtocol#getAppQueue} and + * {@code RMWebServiceProtocol#updateAppQueue}. + */ + public static final String APPS_APPID_QUEUE = "/apps/{appid}/queue"; + + /** Path for {@code RMWebServiceProtocol#createNewApplication}. */ + public static final String APPS_NEW_APPLICATION = "/apps/new-application"; + + /** + * Path for {@code RMWebServiceProtocol#getAppState} and + * {@code RMWebServiceProtocol#updateAppState}. + */ + public static final String APPS_APPID_STATE = "/apps/{appid}/state"; + + /** + * Path for {@code RMWebServiceProtocol#postDelegationToken} and + * {@code RMWebServiceProtocol#cancelDelegationToken}. + */ + public static final String DELEGATION_TOKEN = "/delegation-token"; + + /** Path for {@code RMWebServiceProtocol#postDelegationTokenExpiration}. */ + public static final String DELEGATION_TOKEN_EXPIRATION = + "/delegation-token/expiration"; + + /** Path for {@code RMWebServiceProtocol#createNewReservation}. */ + public static final String RESERVATION_NEW = "/reservation/new-reservation"; + + /** Path for {@code RMWebServiceProtocol#submitReservation}. */ + public static final String RESERVATION_SUBMIT = "/reservation/submit"; + + /** Path for {@code RMWebServiceProtocol#updateReservation}. */ + public static final String RESERVATION_UPDATE = "/reservation/update"; + + /** Path for {@code RMWebServiceProtocol#deleteReservation}. */ + public static final String RESERVATION_DELETE = "/reservation/delete"; + + /** Path for {@code RMWebServiceProtocol#listReservation}. */ + public static final String RESERVATION_LIST = "/reservation/list"; + + /** Path for {@code RMWebServiceProtocol#getAppTimeout}. */ + public static final String APPS_TIMEOUTS_TYPE = + "/apps/{appid}/timeouts/{type}"; + + /** + * Path for {@code RMWebServiceProtocol#getAppTimeouts}. + */ + public static final String APPS_TIMEOUTS = "/apps/{appid}/timeouts"; + + /** + * Path for {@code RMWebServiceProtocol#updateApplicationTimeout}. + */ + public static final String APPS_TIMEOUT = "/apps/{appid}/timeout"; + + /** + * Path for {@code RouterWebServices#getContainer}. + */ + public static final String GET_CONTAINER = + "/apps/{appid}/appattempts/{appattemptid}/containers/{containerid}"; + + /** + * Path for {code checkUserAccessToQueue#} + */ + public static final String CHECK_USER_ACCESS_TO_QUEUE = + "/queues/{queue}/access"; + + // ----------------QueryParams for RMWebServiceProtocol---------------- + + public static final String TIME = "time"; + public static final String STATES = "states"; + public static final String NODEID = "nodeId"; + public static final String STATE = "state"; + public static final String FINAL_STATUS = "finalStatus"; + public static final String USER = "user"; + public static final String QUEUE = "queue"; + public static final String QUEUES = "queues"; + public static final String LIMIT = "limit"; + public static final String STARTED_TIME_BEGIN = "startedTimeBegin"; + public static final String STARTED_TIME_END = "startedTimeEnd"; + public static final String FINISHED_TIME_BEGIN = "finishedTimeBegin"; + public static final String FINISHED_TIME_END = "finishedTimeEnd"; + public static final String APPLICATION_TYPES = "applicationTypes"; + public static final String APPLICATION_TAGS = "applicationTags"; + public static final String APP_ID = "appId"; + public static final String MAX_TIME = "maxTime"; + public static final String APPATTEMPTID = "appattemptid"; + public static final String APPID = "appid"; + public static final String LABELS = "labels"; + public static final String RESERVATION_ID = "reservation-id"; + public static final String START_TIME = "start-time"; + public static final String END_TIME = "end-time"; + public static final String INCLUDE_RESOURCE = "include-resource-allocations"; + public static final String TYPE = "type"; + public static final String CONTAINERID = "containerid"; + public static final String APPATTEMPTS = "appattempts"; + public static final String TIMEOUTS = "timeouts"; + public static final String PRIORITY = "priority"; + public static final String TIMEOUT = "timeout"; + public static final String ATTEMPTS = "appattempts"; + public static final String GET_LABELS = "get-labels"; + public static final String DESELECTS = "deSelects"; + public static final String CONTAINERS = "containers"; + public static final String QUEUE_ACL_TYPE = "queue-acl-type"; + + private RMWSConsts() { + // not called + } + +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWebServices.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWebServices.java index 774692209d1..9f872d29383 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWebServices.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWebServices.java @@ -39,6 +39,7 @@ import javax.servlet.http.HttpServletResponse; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; @@ -57,6 +58,7 @@ import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonConfigurationKeys; +import org.apache.hadoop.http.JettyUtils; import org.apache.hadoop.io.DataOutputBuffer; import org.apache.hadoop.io.Text; import org.apache.hadoop.security.Credentials; @@ -167,6 +169,12 @@ public RMWebServices(final ResourceManager rm, Configuration conf) { this.conf = conf; } + RMWebServices(ResourceManager rm, Configuration conf, + HttpServletResponse response) { + this(rm, conf); + this.response = response; + } + protected Boolean hasAccess(RMApp app, HttpServletRequest hsr) { // Check for the authorization. UserGroupInformation callerUGI = getCallerUserGroupInformation(hsr, true); @@ -1496,4 +1504,56 @@ public CancelDelegationTokenResponse run() throws IOException, } return token; } + + + @GET + @Path(RMWSConsts.CHECK_USER_ACCESS_TO_QUEUE) + @Produces({ MediaType.APPLICATION_JSON + "; " + JettyUtils.UTF_8, + MediaType.APPLICATION_XML + "; " + JettyUtils.UTF_8 }) + public Response checkUserAccessToQueue( + @PathParam(RMWSConsts.QUEUE) String queue, + @QueryParam(RMWSConsts.USER) String username, + @QueryParam(RMWSConsts.QUEUE_ACL_TYPE) + @DefaultValue("SUBMIT_APPLICATIONS") String queueAclType, + @Context HttpServletRequest hsr) throws AuthorizationException { + init(); + + // Check if the specified queue acl is valid. + QueueACL queueACL; + try { + queueACL = QueueACL.valueOf(queueAclType); + } catch (IllegalArgumentException e) { + return Response.status(Status.BAD_REQUEST).entity( + "Specified queueAclType=" + queueAclType + + " is not a valid type, valid queue acl types={" + + "SUBMIT_APPLICATIONS/ADMINISTER_QUEUE}").build(); + } + + // For the user who invokes this REST call, he/she should have admin access + // to the queue. Otherwise we will reject the call. + UserGroupInformation callerUGI = getCallerUserGroupInformation(hsr, true); + if (callerUGI != null && !this.rm.getResourceScheduler().checkAccess( + callerUGI, QueueACL.ADMINISTER_QUEUE, queue)) { + return Response.status(Status.FORBIDDEN).entity( + "User=" + callerUGI.getUserName() + " doesn't haven access to queue=" + + queue + " so it cannot check ACLs for other users.") + .build(); + } + + // Create UGI for the to-be-checked user. + UserGroupInformation user = UserGroupInformation.createRemoteUser(username); + if (user == null) { + return Response.status(Status.FORBIDDEN).entity( + "Failed to retrieve UserGroupInformation for user=" + username) + .build(); + } + + if (!this.rm.getResourceScheduler().checkAccess(user, queueACL, queue)) { + return Response.status(Status.FORBIDDEN).entity( + "User=" + username + " doesn't have access to queue=" + queue + + " with acl-type=" + queueAclType).build(); + } + + return Response.status(Status.OK).build(); + } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/TestRMWebServices.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/TestRMWebServices.java index bd31ff371ef..54929f29fd7 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/TestRMWebServices.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/TestRMWebServices.java @@ -21,16 +21,23 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.StringReader; - +import java.security.Principal; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.MediaType; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import com.google.common.collect.ImmutableSet; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.service.Service.STATE; import org.apache.hadoop.util.VersionInfo; +import org.apache.hadoop.yarn.api.records.QueueACL; import org.apache.hadoop.yarn.api.records.QueueState; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.server.resourcemanager.ClusterMetrics; @@ -44,9 +51,11 @@ import org.apache.hadoop.yarn.webapp.WebServicesTestUtils; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; +import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.mortbay.jetty.Response; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -590,4 +599,83 @@ public void verifyClusterSchedulerFifoGeneric(String type, String state, } + private HttpServletRequest mockHttpServletRequestByUserName(String username) { + HttpServletRequest mockHsr = mock(HttpServletRequest.class); + when(mockHsr.getRemoteUser()).thenReturn(username); + Principal principal = mock(Principal.class); + when(principal.getName()).thenReturn(username); + when(mockHsr.getUserPrincipal()).thenReturn(principal); + return mockHsr; + } + + @Test + public void testCheckUserAccessToQueue() throws Exception { + + ResourceManager mockRM = mock(ResourceManager.class); + Configuration conf = new YarnConfiguration(); + + // Inject a mock scheduler implementation. + // Only admin user has ADMINISTER_QUEUE access. + // For SUBMIT_APPLICATION ACL, both of admin/yarn user have acess + ResourceScheduler mockScheduler = new FifoScheduler() { + @Override + public synchronized boolean checkAccess(UserGroupInformation callerUGI, + QueueACL acl, String queueName) { + if (acl == QueueACL.ADMINISTER_QUEUE) { + if (callerUGI.getUserName().equals("admin")) { + return true; + } + } else { + if (ImmutableSet.of("admin", "yarn").contains(callerUGI.getUserName())) { + return true; + } + } + return false; + } + }; + + when(mockRM.getResourceScheduler()).thenReturn(mockScheduler); + + RMWebServices webSvc = + new RMWebServices(mockRM, conf, mock(HttpServletResponse.class)); + + // Case 1: Only queue admin user can access other user's information + HttpServletRequest mockHsr = mockHttpServletRequestByUserName("non-admin"); + Assert.assertEquals(webSvc.checkUserAccessToQueue("queue", "jack", + QueueACL.SUBMIT_APPLICATIONS.name(), mockHsr).getStatus(), + Response.SC_FORBIDDEN); + + // Case 2: request an unknown ACL causes BAD_REQUEST + mockHsr = mockHttpServletRequestByUserName("admin"); + Assert.assertEquals(webSvc.checkUserAccessToQueue("queue", "jack", + "XYZ_ACL", mockHsr).getStatus(), Response.SC_BAD_REQUEST); + + // Case 3: get FORBIDDEN for rejected ACL + mockHsr = mockHttpServletRequestByUserName("admin"); + Assert.assertEquals(webSvc.checkUserAccessToQueue("queue", "jack", + QueueACL.SUBMIT_APPLICATIONS.name(), mockHsr).getStatus(), + Response.SC_FORBIDDEN); + Assert.assertEquals(webSvc.checkUserAccessToQueue("queue", "jack", + QueueACL.ADMINISTER_QUEUE.name(), mockHsr).getStatus(), + Response.SC_FORBIDDEN); + + // Case 4: get OK for listed ACLs + mockHsr = mockHttpServletRequestByUserName("admin"); + Assert.assertEquals(webSvc.checkUserAccessToQueue("queue", "admin", + QueueACL.SUBMIT_APPLICATIONS.name(), mockHsr).getStatus(), + Response.SC_OK); + Assert.assertEquals(webSvc.checkUserAccessToQueue("queue", "admin", + QueueACL.ADMINISTER_QUEUE.name(), mockHsr).getStatus(), + Response.SC_OK); + + // Case 5: get OK only for SUBMIT_APP acl for "yarn" user + mockHsr = mockHttpServletRequestByUserName("admin"); + Assert.assertEquals(webSvc.checkUserAccessToQueue("queue", "yarn", + QueueACL.SUBMIT_APPLICATIONS.name(), mockHsr).getStatus(), + Response.SC_OK); + Assert.assertEquals(webSvc.checkUserAccessToQueue("queue", "yarn", + QueueACL.ADMINISTER_QUEUE.name(), mockHsr).getStatus(), + Response.SC_FORBIDDEN); + } + }