From 839868aa64c85864a8b3d9609c97fe3d2740609a Mon Sep 17 00:00:00 2001 From: Tyler Ouyang Date: Tue, 14 May 2024 16:54:23 -0700 Subject: [PATCH] Add authz to hosts endpoints commit-id:ef160c01 --- .../deployservice/dao/EnvironDAO.java | 13 ++ .../deployservice/db/DBEnvironDAOImpl.java | 20 +++ .../deployservice/handler/EnvironHandler.java | 26 +++- .../db/DBEnvironDAOImplTest.java | 125 ++++++++++++++++++ .../handler/EnvironHandlerTest.java | 58 ++++++-- .../teletraan/resource/EnvAgents.java | 4 +- .../teletraan/resource/EnvDeploys.java | 1 - .../teletraan/resource/EnvHosts.java | 15 +-- .../pinterest/teletraan/resource/Hosts.java | 14 +- .../teletraan/security/HostPathExtractor.java | 57 ++++++++ ...eletraanAuthZResourceExtractorFactory.java | 4 + .../security/HostPathExtractorTest.java | 90 +++++++++++++ ...raanAuthZResourceExtractorFactoryTest.java | 10 ++ .../security/bean/AuthZResource.java | 4 +- 14 files changed, 417 insertions(+), 24 deletions(-) create mode 100644 deploy-service/common/src/test/java/com/pinterest/deployservice/db/DBEnvironDAOImplTest.java create mode 100644 deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/security/HostPathExtractor.java create mode 100644 deploy-service/teletraanservice/src/test/java/com/pinterest/teletraan/security/HostPathExtractorTest.java diff --git a/deploy-service/common/src/main/java/com/pinterest/deployservice/dao/EnvironDAO.java b/deploy-service/common/src/main/java/com/pinterest/deployservice/dao/EnvironDAO.java index 94ff34208e..51af82c01d 100644 --- a/deploy-service/common/src/main/java/com/pinterest/deployservice/dao/EnvironDAO.java +++ b/deploy-service/common/src/main/java/com/pinterest/deployservice/dao/EnvironDAO.java @@ -62,6 +62,19 @@ public interface EnvironDAO { List getEnvsByHost(String host) throws Exception; + /** + * Retrieves the main environment ID for the specified host ID. + * + *

The main environment is where the cluster that the host belongs to is created. + * In case such an environment does not exist, the method will attempt to retrieve the + * environment that matches the first group that's known to Teletraan for the specified host. + * + * @param hostId The ID of the host. + * @return The bean represents the main environment for the specified host ID. + * @throws SQLException if an error occurs while retrieving the main environment ID. + */ + EnvironBean getMainEnvByHostId(String hostId) throws SQLException; + List getEnvsByGroups(Collection groups) throws Exception; List getCurrentDeployIds() throws Exception; diff --git a/deploy-service/common/src/main/java/com/pinterest/deployservice/db/DBEnvironDAOImpl.java b/deploy-service/common/src/main/java/com/pinterest/deployservice/db/DBEnvironDAOImpl.java index be46e797cb..3879817a78 100644 --- a/deploy-service/common/src/main/java/com/pinterest/deployservice/db/DBEnvironDAOImpl.java +++ b/deploy-service/common/src/main/java/com/pinterest/deployservice/db/DBEnvironDAOImpl.java @@ -108,6 +108,20 @@ public class DBEnvironDAOImpl implements EnvironDAO { private static final String GET_ENV_BY_CONSTRAINT_ID = "SELECT * FROM environs WHERE deploy_constraint_id = ?"; private static final String DELETE_DEPLOY_CONSTRAINT = "UPDATE environs SET deploy_constraint_id=null WHERE env_name=? AND stage_name=?"; + private static final String GET_MAIN_ENV_BY_HOST_ID = "SELECT e.* " + + "FROM hosts_and_agents ha " + + "JOIN environs e ON ha.auto_scaling_group = e.cluster_name " + + "WHERE ha.host_id = ? " + + "UNION " + + "(SELECT e.* " + + "FROM hosts h " + + "JOIN environs e ON h.group_name = e.cluster_name " + + "WHERE h.host_id = ? AND NOT EXISTS ( " + + " SELECT 1 " + + " FROM hosts_and_agents ha " + + " JOIN environs e ON ha.auto_scaling_group = e.cluster_name " + + " WHERE ha.host_id = ? )" + + "ORDER BY h.create_date ASC LIMIT 1)"; private BasicDataSource dataSource; @@ -250,6 +264,12 @@ public List getEnvsByHost(String host) throws Exception { return new ArrayList(envSet); } + @Override + public EnvironBean getMainEnvByHostId(String hostId) throws SQLException { + ResultSetHandler h = new BeanHandler<>(EnvironBean.class); + return new QueryRunner(dataSource).query(GET_MAIN_ENV_BY_HOST_ID, h, hostId, hostId, hostId); + } + @Override public List getEnvsByGroups(Collection groups) throws Exception { ResultSetHandler> h = new BeanListHandler<>(EnvironBean.class); diff --git a/deploy-service/common/src/main/java/com/pinterest/deployservice/handler/EnvironHandler.java b/deploy-service/common/src/main/java/com/pinterest/deployservice/handler/EnvironHandler.java index 891fca5103..3e44121fca 100644 --- a/deploy-service/common/src/main/java/com/pinterest/deployservice/handler/EnvironHandler.java +++ b/deploy-service/common/src/main/java/com/pinterest/deployservice/handler/EnvironHandler.java @@ -26,14 +26,18 @@ import com.pinterest.deployservice.dao.PromoteDAO; import com.pinterest.deployservice.dao.ScheduleDAO; -import com.pinterest.deployservice.bean.ScheduleState; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.sql.SQLException; import java.util.*; +import javax.ws.rs.NotAllowedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.WebApplicationException; + public class EnvironHandler { private static final Logger LOG = LoggerFactory.getLogger(EnvironHandler.class); @@ -522,4 +526,24 @@ public void stopServiceOnHosts(Collection hostIds, boolean replaceHost) stopServiceOnHost(hostId, replaceHost); } } + + public void ensureHostsOwnedByEnv(EnvironBean environBean, Collection hostIds) + throws WebApplicationException { + for (String hostId : hostIds) { + try { + EnvironBean mainEnv = environDAO.getMainEnvByHostId(hostId); + if (mainEnv == null) { + throw new NotFoundException( + String.format("No main environment found for host %s, refuse to proceed", hostId)); + } + if (!mainEnv.getEnv_id().equals(environBean.getEnv_id())) { + throw new NotAllowedException(String.format("%s/%s is not the owning environment of host %s", + environBean.getEnv_name(), environBean.getStage_name(), hostId)); + } + } catch (SQLException e) { + throw new WebApplicationException(String.format("Failed to get main environment for host %s", hostId), + e); + } + } + } } diff --git a/deploy-service/common/src/test/java/com/pinterest/deployservice/db/DBEnvironDAOImplTest.java b/deploy-service/common/src/test/java/com/pinterest/deployservice/db/DBEnvironDAOImplTest.java new file mode 100644 index 0000000000..29524045c2 --- /dev/null +++ b/deploy-service/common/src/test/java/com/pinterest/deployservice/db/DBEnvironDAOImplTest.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2024 Pinterest, Inc. + * + * Licensed 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 com.pinterest.deployservice.db; + +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Instant; + +import com.pinterest.deployservice.bean.BeanUtils; +import com.pinterest.deployservice.bean.EnvironBean; +import com.pinterest.deployservice.bean.HostAgentBean; +import com.pinterest.deployservice.bean.HostBean; +import com.pinterest.deployservice.dao.EnvironDAO; +import com.pinterest.deployservice.dao.HostAgentDAO; +import com.pinterest.deployservice.dao.HostDAO; +import com.pinterest.deployservice.fixture.EnvironBeanFixture; +import org.apache.commons.dbcp.BasicDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DBEnvironDAOImplTest { + private static final String HOST_ID = "host123"; + private static final String TEST_CLUSTER = "test-cluster"; + private static BasicDataSource dataSource; + private static HostAgentDAO hostAgentDAO; + private static HostDAO hostDAO; + private EnvironDAO sut; + + @BeforeAll + static void setUpAll() throws Exception { + dataSource = DBUtils.createTestDataSource(); + hostAgentDAO = new DBHostAgentDAOImpl(dataSource); + hostDAO = new DBHostDAOImpl(dataSource); + } + + @BeforeEach + void setUp() { + sut = new DBEnvironDAOImpl(dataSource); + } + + @AfterEach + void tearDown() throws Exception { + DBUtils.truncateAllTables(dataSource); + } + + @Test + void testGetMainEnvByHostId_happyPath() throws Exception { + EnvironBean expectedEnvBean = EnvironBeanFixture.createRandomEnvironBean(); + expectedEnvBean.setCluster_name(TEST_CLUSTER); + sut.insert(expectedEnvBean); + + HostAgentBean hostAgentBean = new HostAgentBean(); + hostAgentBean.setHost_id(HOST_ID); + hostAgentBean.setAuto_scaling_group(TEST_CLUSTER); + hostAgentDAO.insert(hostAgentBean); + + HostBean hostBean = BeanUtils.createHostBean(Instant.now()); + hostBean.setHost_id(HOST_ID); + hostBean.setGroup_name(TEST_CLUSTER + "sidecar"); + hostDAO.insert(hostBean); + + EnvironBean actualEnvironBean = sut.getMainEnvByHostId(HOST_ID); + assertEquals(expectedEnvBean.getEnv_name(), actualEnvironBean.getEnv_name()); + assertEquals(expectedEnvBean.getStage_name(), actualEnvironBean.getStage_name()); + assertEquals(TEST_CLUSTER, actualEnvironBean.getCluster_name()); + + EnvironBean nullEnvironBean = sut.getMainEnvByHostId("random-host-id"); + assertNull(nullEnvironBean); + } + + @Test + void testGetMainEnvByHostId_noHost() throws Exception { + EnvironBean actualEnvironBean = sut.getMainEnvByHostId(HOST_ID); + assertNull(actualEnvironBean); + } + + @Test + void testGetMainEnvByHostId_noEnv() throws Exception { + HostAgentBean hostAgentBean = new HostAgentBean(); + hostAgentBean.setHost_id(HOST_ID); + hostAgentBean.setAuto_scaling_group(TEST_CLUSTER); + hostAgentDAO.insert(hostAgentBean); + + EnvironBean actualEnvironBean = sut.getMainEnvByHostId(HOST_ID); + assertNull(actualEnvironBean); + } + + @Test + void testGetMainEnvByHostId_noHostAgent() throws Exception { + EnvironBean expectedEnvBean = EnvironBeanFixture.createRandomEnvironBean(); + expectedEnvBean.setCluster_name(TEST_CLUSTER); + sut.insert(expectedEnvBean); + + HostBean hostBean = BeanUtils.createHostBean(Instant.now()); + hostBean.setHost_id(HOST_ID); + hostBean.setGroup_name(TEST_CLUSTER); + hostDAO.insert(hostBean); + + HostBean hostBean2 = BeanUtils.createHostBean(Instant.now()); + hostBean.setHost_id(HOST_ID); + hostBean.setGroup_name(TEST_CLUSTER + "2"); + hostDAO.insert(hostBean2); + + EnvironBean actualEnvironBean = sut.getMainEnvByHostId(HOST_ID); + assertEquals(expectedEnvBean.getEnv_name(), actualEnvironBean.getEnv_name()); + assertEquals(expectedEnvBean.getStage_name(), actualEnvironBean.getStage_name()); + assertEquals(TEST_CLUSTER, actualEnvironBean.getCluster_name()); + } +} diff --git a/deploy-service/common/src/test/java/com/pinterest/deployservice/handler/EnvironHandlerTest.java b/deploy-service/common/src/test/java/com/pinterest/deployservice/handler/EnvironHandlerTest.java index 86b66ac2dc..b414819866 100644 --- a/deploy-service/common/src/test/java/com/pinterest/deployservice/handler/EnvironHandlerTest.java +++ b/deploy-service/common/src/test/java/com/pinterest/deployservice/handler/EnvironHandlerTest.java @@ -1,12 +1,24 @@ package com.pinterest.deployservice.handler; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -import org.junit.Before; -import org.junit.Test; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; + +import javax.ws.rs.NotAllowedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.WebApplicationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import com.pinterest.deployservice.ServiceContext; @@ -18,7 +30,7 @@ import com.pinterest.deployservice.dao.EnvironDAO; import com.pinterest.deployservice.dao.HostDAO; -public class EnvironHandlerTest { +class EnvironHandlerTest { private final static String DEFAULT_HOST_ID = "hostId"; private EnvironHandler environHandler; @@ -26,6 +38,7 @@ public class EnvironHandlerTest { private HostDAO mockHostDAO; private AgentDAO mockAgentDAO; private EnvironDAO environDAO; + private List hostIds = Arrays.asList("hostId1", "hostId2"); private ServiceContext createMockServiceContext() throws Exception { mockHostDAO = mock(HostDAO.class); @@ -39,13 +52,13 @@ private ServiceContext createMockServiceContext() throws Exception { return serviceContext; } - @Before - public void setUp() throws Exception { + @BeforeEach + void setUp() throws Exception { environHandler = new EnvironHandler(createMockServiceContext()); } @Test - public void stopServiceOnHost_withReplaceHost_hostBeanStateIsPendingTerminate() throws Exception { + void stopServiceOnHost_withReplaceHost_hostBeanStateIsPendingTerminate() throws Exception { ArgumentCaptor argument = ArgumentCaptor.forClass(HostBean.class); environHandler.stopServiceOnHost(DEFAULT_HOST_ID, true); @@ -54,7 +67,7 @@ public void stopServiceOnHost_withReplaceHost_hostBeanStateIsPendingTerminate() } @Test - public void stopServiceOnHost_withoutReplaceHost_hostBeanStateIsPendingTerminateNoReplace() throws Exception { + void stopServiceOnHost_withoutReplaceHost_hostBeanStateIsPendingTerminateNoReplace() throws Exception { ArgumentCaptor argument = ArgumentCaptor.forClass(HostBean.class); environHandler.stopServiceOnHost(DEFAULT_HOST_ID, false); @@ -63,7 +76,7 @@ public void stopServiceOnHost_withoutReplaceHost_hostBeanStateIsPendingTerminate } @Test - public void updateStage_type_enables_private_build() throws Exception { + void updateStage_type_enables_private_build() throws Exception { ArgumentCaptor argument = ArgumentCaptor.forClass(EnvironBean.class); EnvironBean envBean = new EnvironBean(); envBean.setStage_type(EnvType.DEV); @@ -71,4 +84,31 @@ public void updateStage_type_enables_private_build() throws Exception { verify(environDAO).insert(argument.capture()); assertEquals(true, argument.getValue().getAllow_private_build()); } + + @Test + void ensureHostsOwnedByEnv_noMainEnv() throws Exception { + assertThrows(NotFoundException.class, () -> environHandler.ensureHostsOwnedByEnv(new EnvironBean(), hostIds)); + } + + @Test + void ensureHostsOwnedByEnv_differentMainEnv() throws Exception { + EnvironBean envBean = new EnvironBean(); + envBean.setEnv_id("envId"); + when(environDAO.getMainEnvByHostId(anyString())).thenReturn(envBean); + assertThrows(NotAllowedException.class, () -> environHandler.ensureHostsOwnedByEnv(new EnvironBean(), hostIds)); + } + + @Test + void ensureHostsOwnedByEnv_sameMainEnv() throws Exception { + EnvironBean envBean = new EnvironBean(); + envBean.setEnv_id("envId"); + when(environDAO.getMainEnvByHostId(anyString())).thenReturn(envBean); + assertDoesNotThrow(() -> environHandler.ensureHostsOwnedByEnv(envBean, hostIds)); + } + + @Test + void ensureHostsOwnedByEnv_sqlException() throws Exception { + when(environDAO.getMainEnvByHostId(anyString())).thenThrow(SQLException.class); + assertThrows(WebApplicationException.class, () -> environHandler.ensureHostsOwnedByEnv(new EnvironBean(), hostIds)); + } } diff --git a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvAgents.java b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvAgents.java index ca3ec3c73e..5facd2170c 100644 --- a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvAgents.java +++ b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvAgents.java @@ -98,7 +98,7 @@ public AgentErrorBean getAgentError( value = "Update host agent", notes = "Updates host agent specified by given environment name, stage name, and host id with given " + "agent object") - @RolesAllowed(TeletraanPrincipalRole.Names.WRITE) + @RolesAllowed(TeletraanPrincipalRole.Names.EXECUTE) @ResourceAuthZInfo(type = AuthZResource.Type.ENV_STAGE, idLocation = ResourceAuthZInfo.Location.PATH) public void update( @Context SecurityContext sc, @@ -118,7 +118,7 @@ public void update( @ApiOperation( value = "Reset failed deploys", notes = "Resets failing deploys given an environment name, stage name, and deploy id") - @RolesAllowed(TeletraanPrincipalRole.Names.WRITE) + @RolesAllowed(TeletraanPrincipalRole.Names.EXECUTE) @ResourceAuthZInfo(type = AuthZResource.Type.ENV_STAGE, idLocation = ResourceAuthZInfo.Location.PATH) public void resetFailedDeploys( @Context SecurityContext sc, diff --git a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvDeploys.java b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvDeploys.java index 381efe524e..d3b58bc940 100644 --- a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvDeploys.java +++ b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvDeploys.java @@ -154,7 +154,6 @@ public Response action( response = Response.class) @RolesAllowed(TeletraanPrincipalRole.Names.EXECUTE) @ResourceAuthZInfo(type = AuthZResource.Type.ENV_STAGE, idLocation = ResourceAuthZInfo.Location.PATH) - // TODO: sidecar owners can perform actions on non-sidecar agents public void update( @Context SecurityContext sc, @ApiParam(value = "Environment name", required = true)@PathParam("envName") String envName, diff --git a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvHosts.java b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvHosts.java index 1a15f6cfb6..e21ee3610e 100644 --- a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvHosts.java +++ b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/EnvHosts.java @@ -16,8 +16,6 @@ package com.pinterest.teletraan.resource; - -import com.google.common.base.Optional; import com.pinterest.deployservice.bean.EnvironBean; import com.pinterest.deployservice.bean.HostBean; import com.pinterest.deployservice.bean.TeletraanPrincipalRole; @@ -28,7 +26,6 @@ import com.pinterest.deployservice.handler.EnvironHandler; import com.pinterest.teletraan.TeletraanServiceContext; import com.pinterest.teletraan.universal.security.ResourceAuthZInfo; -import com.pinterest.teletraan.universal.security.ResourceAuthZInfo.Location; import com.pinterest.teletraan.universal.security.bean.AuthZResource; import io.swagger.annotations.Api; @@ -38,6 +35,7 @@ import org.slf4j.LoggerFactory; import java.util.Collection; +import java.util.Optional; import javax.annotation.security.PermitAll; import javax.annotation.security.RolesAllowed; @@ -100,8 +98,7 @@ public Collection getHostByHostName( @DELETE @RolesAllowed(TeletraanPrincipalRole.Names.DELETE) - @ResourceAuthZInfo(type = AuthZResource.Type.ENV_STAGE, idLocation = Location.PATH) - // TODO: this allows sidecar owners to stop hosts + @ResourceAuthZInfo(type = AuthZResource.Type.HOST, idLocation = ResourceAuthZInfo.Location.PATH) public void stopServiceOnHost(@Context SecurityContext sc, @PathParam("envName") String envName, @PathParam("stageName") String stageName, @@ -110,10 +107,12 @@ public void stopServiceOnHost(@Context SecurityContext sc, throws Exception { String operator = sc.getUserPrincipal().getName(); EnvironBean envBean = Utils.getEnvStage(environDAO, envName, stageName); - environHandler.stopServiceOnHosts(hostIds, replaceHost.or(true)); + environHandler.ensureHostsOwnedByEnv(envBean, hostIds); + environHandler.stopServiceOnHosts(hostIds, replaceHost.orElse(true)); configHistoryHandler.updateConfigHistory(envBean.getEnv_id(), Constants.TYPE_HOST_ACTION, String.format("STOP %s", hostIds.toString()), operator); - LOG.info(String.format("Successfully stopped %s/%s service on hosts %s by %s", envName, stageName, - hostIds.toString(), operator)); + LOG.info("Successfully stopped {}/{} service on hosts {} by {}", envName, stageName, + hostIds, operator); } + } diff --git a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/Hosts.java b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/Hosts.java index 9d561197ce..e4e05308ec 100644 --- a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/Hosts.java +++ b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/resource/Hosts.java @@ -18,9 +18,12 @@ import com.google.common.base.Optional; import com.pinterest.deployservice.bean.HostBean; import com.pinterest.deployservice.bean.HostState; +import com.pinterest.deployservice.bean.TeletraanPrincipalRole; import com.pinterest.deployservice.dao.HostDAO; import com.pinterest.deployservice.handler.EnvironHandler; import com.pinterest.teletraan.TeletraanServiceContext; +import com.pinterest.teletraan.universal.security.ResourceAuthZInfo; +import com.pinterest.teletraan.universal.security.bean.AuthZResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +31,7 @@ import io.swagger.annotations.*; import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; import javax.validation.Valid; import javax.ws.rs.*; import javax.ws.rs.core.Context; @@ -47,7 +51,6 @@ ) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -// TODO: CDP-7701 Add authorization to hosts endpoints public class Hosts { private static final Logger LOG = LoggerFactory.getLogger(Hosts.class); private HostDAO hostDAO; @@ -59,6 +62,11 @@ public Hosts(@Context TeletraanServiceContext context) { } @POST + @RolesAllowed(TeletraanPrincipalRole.Names.EXECUTE) + @ResourceAuthZInfo(type = AuthZResource.Type.SYSTEM) + @ApiOperation( + value = "Add a host", + notes = "Add a host to the system. Should be only called by Rodimus that's why it requires SYSTEM permission.") public void addHost(@Context SecurityContext sc, @Valid HostBean hostBean) throws Exception { String operator = sc.getUserPrincipal().getName(); @@ -75,6 +83,8 @@ public void addHost(@Context SecurityContext sc, @PUT @Path("/{hostId : [a-zA-Z0-9\\-_]+}") + @RolesAllowed(TeletraanPrincipalRole.Names.EXECUTE) + @ResourceAuthZInfo(type = AuthZResource.Type.HOST, idLocation = ResourceAuthZInfo.Location.PATH) public void updateHost(@Context SecurityContext sc, @PathParam("hostId") String hostId, @Valid HostBean hostBean) throws Exception { @@ -87,6 +97,8 @@ public void updateHost(@Context SecurityContext sc, @DELETE @Path("/{hostId : [a-zA-Z0-9\\-_]+}") + @RolesAllowed(TeletraanPrincipalRole.Names.EXECUTE) + @ResourceAuthZInfo(type = AuthZResource.Type.HOST, idLocation = ResourceAuthZInfo.Location.PATH) public void stopHost(@Context SecurityContext sc, @PathParam("hostId") String hostId, @ApiParam(value = "Replace the host or not") @QueryParam("replaceHost") Optional replaceHost) diff --git a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/security/HostPathExtractor.java b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/security/HostPathExtractor.java new file mode 100644 index 0000000000..5690819869 --- /dev/null +++ b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/security/HostPathExtractor.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2024 Pinterest, Inc. + * + * Licensed 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 com.pinterest.teletraan.security; + +import com.pinterest.deployservice.ServiceContext; +import com.pinterest.deployservice.bean.EnvironBean; +import com.pinterest.deployservice.dao.EnvironDAO; +import com.pinterest.teletraan.universal.security.AuthZResourceExtractor; +import com.pinterest.teletraan.universal.security.bean.AuthZResource; +import java.sql.SQLException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.container.ContainerRequestContext; + +public class HostPathExtractor implements AuthZResourceExtractor { + private static final String HOST_ID = "hostId"; + private final EnvironDAO environDAO; + + public HostPathExtractor(ServiceContext context) { + this.environDAO = context.getEnvironDAO(); + } + + @Override + public AuthZResource extractResource(ContainerRequestContext requestContext) + throws ExtractionException { + String hostId = requestContext.getUriInfo().getPathParameters().getFirst(HOST_ID); + if (hostId == null) { + throw new ExtractionException("Failed to extract host id"); + } + + EnvironBean envBean; + try { + envBean = environDAO.getMainEnvByHostId(hostId); + } catch (SQLException e) { + throw new ExtractionException( + "Failed to get the main environment with host ID: " + hostId, e); + } + + if (envBean == null) { + throw new NotFoundException( + "Failed to get the main environment with host ID: " + hostId); + } + return new AuthZResource(envBean.getEnv_name(), envBean.getStage_name()); + } +} diff --git a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/security/TeletraanAuthZResourceExtractorFactory.java b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/security/TeletraanAuthZResourceExtractorFactory.java index cd6998ec84..81f44dee9f 100644 --- a/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/security/TeletraanAuthZResourceExtractorFactory.java +++ b/deploy-service/teletraanservice/src/main/java/com/pinterest/teletraan/security/TeletraanAuthZResourceExtractorFactory.java @@ -30,12 +30,14 @@ public class TeletraanAuthZResourceExtractorFactory implements AuthZResourceExtr private static final AuthZResourceExtractor HOTFIX_BODY_EXTRACTOR = new HotfixBodyExtractor(); private final AuthZResourceExtractor buildPathExtractor; private final AuthZResourceExtractor deployPathExtractor; + private final AuthZResourceExtractor hostPathExtractor; private final AuthZResourceExtractor hotfixPathExtractor; public TeletraanAuthZResourceExtractorFactory(ServiceContext serviceContext) { buildPathExtractor = new BuildPathExtractor(serviceContext); deployPathExtractor = new DeployPathExtractor(serviceContext); hotfixPathExtractor = new HotfixPathExtractor(serviceContext); + hostPathExtractor = new HostPathExtractor(serviceContext); } @Override @@ -53,6 +55,8 @@ public AuthZResourceExtractor create(ResourceAuthZInfo authZInfo) { return deployPathExtractor; case HOTFIX: return hotfixPathExtractor; + case HOST: + return hostPathExtractor; default: throw new UnsupportedResourceInfoException(authZInfo); } diff --git a/deploy-service/teletraanservice/src/test/java/com/pinterest/teletraan/security/HostPathExtractorTest.java b/deploy-service/teletraanservice/src/test/java/com/pinterest/teletraan/security/HostPathExtractorTest.java new file mode 100644 index 0000000000..d7a24c0fcc --- /dev/null +++ b/deploy-service/teletraanservice/src/test/java/com/pinterest/teletraan/security/HostPathExtractorTest.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2024 Pinterest, Inc. + * + * Licensed 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 com.pinterest.teletraan.security; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.pinterest.deployservice.ServiceContext; +import com.pinterest.deployservice.bean.EnvironBean; +import com.pinterest.deployservice.dao.EnvironDAO; +import com.pinterest.teletraan.universal.security.AuthZResourceExtractor.ExtractionException; +import com.pinterest.teletraan.universal.security.bean.AuthZResource; +import java.sql.SQLException; +import javax.ws.rs.NotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class HostPathExtractorTest extends BasePathExtractorTest { + private HostPathExtractor sut; + private ServiceContext serviceContext; + private EnvironDAO hostDAO; + + @BeforeEach + void setUp() { + super.setUp(); + hostDAO = mock(EnvironDAO.class); + serviceContext = new ServiceContext(); + serviceContext.setEnvironDAO(hostDAO); + sut = new HostPathExtractor(serviceContext); + } + + @Test + void testExtractResource_noPathParams_exception() { + assertThrows(ExtractionException.class, () -> sut.extractResource(context)); + } + + @Test + void testExtractResource_0HostId() { + pathParameters.add("param", "val"); + + assertThrows(ExtractionException.class, () -> sut.extractResource(context)); + } + + @Test + void testExtractResource() throws Exception { + String hostId = "testHostId"; + pathParameters.add("hostId", hostId); + + when(hostDAO.getMainEnvByHostId(hostId)).thenReturn(null); + assertThrows(NotFoundException.class, () -> sut.extractResource(context)); + + EnvironBean envBean = new EnvironBean(); + envBean.setEnv_name("host_env"); + envBean.setStage_name("host_stage"); + when(hostDAO.getMainEnvByHostId(hostId)).thenReturn(envBean); + + AuthZResource result = sut.extractResource(context); + + assertNotNull(result); + assertTrue(result.getName().contains(envBean.getEnv_name())); + assertTrue(result.getName().contains(envBean.getStage_name())); + assertEquals(AuthZResource.Type.ENV_STAGE, result.getType()); + } + + @Test + void testExtractResource_sqlException() throws Exception { + pathParameters.add("hostId", "someId"); + when(hostDAO.getMainEnvByHostId(any())).thenThrow(SQLException.class); + + assertThrows(ExtractionException.class, () -> sut.extractResource(context)); + } +} diff --git a/deploy-service/teletraanservice/src/test/java/com/pinterest/teletraan/security/TeletraanAuthZResourceExtractorFactoryTest.java b/deploy-service/teletraanservice/src/test/java/com/pinterest/teletraan/security/TeletraanAuthZResourceExtractorFactoryTest.java index 21fbe6c74a..331569f95c 100644 --- a/deploy-service/teletraanservice/src/test/java/com/pinterest/teletraan/security/TeletraanAuthZResourceExtractorFactoryTest.java +++ b/deploy-service/teletraanservice/src/test/java/com/pinterest/teletraan/security/TeletraanAuthZResourceExtractorFactoryTest.java @@ -120,6 +120,16 @@ void create_shouldReturnCorrectExtractorForEnvStagePathLocation() { assertTrue(extractor instanceof EnvStagePathExtractor); } + @Test + void create_shouldReturnCorrectExtractorForHostPathLocation() { + when(authZInfo.idLocation()).thenReturn(ResourceAuthZInfo.Location.PATH); + when(authZInfo.type()).thenReturn(AuthZResource.Type.HOST); + + AuthZResourceExtractor extractor = extractorFactory.create(authZInfo); + + assertTrue(extractor instanceof HostPathExtractor); + } + @Test void create_shouldThrowExceptionForUnsupportedLocation() { when(authZInfo.idLocation()).thenReturn(ResourceAuthZInfo.Location.NA); diff --git a/deploy-service/universal/src/main/java/com/pinterest/teletraan/universal/security/bean/AuthZResource.java b/deploy-service/universal/src/main/java/com/pinterest/teletraan/universal/security/bean/AuthZResource.java index 79caf1f8bc..d8a6aa6ee9 100644 --- a/deploy-service/universal/src/main/java/com/pinterest/teletraan/universal/security/bean/AuthZResource.java +++ b/deploy-service/universal/src/main/java/com/pinterest/teletraan/universal/security/bean/AuthZResource.java @@ -91,8 +91,8 @@ public enum Type { DEPLOY, /** For hotfix related resources. */ HOTFIX, - /** For Agent related resources. */ - AGENT, + /** For Host related resources. */ + HOST, } /**